-- Id: -- More information about this indicator can be found at: -- http://fxcodebase.com/ --+------------------------------------------------------------------+ --| Copyright © 2018, Gehtsoft USA LLC | --| http://fxcodebase.com | --+------------------------------------------------------------------+ --| Developed by : Mario Jemic | --| mario.jemic@gmail.com | --+------------------------------------------------------------------+ --| Support our efforts by donating | --| Patreon : https://goo.gl/GdXWeN | --| Paypal : https://goo.gl/9Rj74e | --| BitCoin : 15VCJTLaz12Amr7adHSBtL9v8XomURo9RF | --| BitCoin Cash : 1BEtS465S3Su438Kc58h2sqvVvHK9Mijtg | --| Ethereum : 0x8C110cD61538fb6d7A2B47858F0c0AaBd663068D | --| LiteCoin : LLU8PSY2vsq7B9kRELLZQcKf5nJQrdeqwD | --+------------------------------------------------------------------+ local Modules = {}; function Init() strategy:name("Trend Stop Close/Hedge Strategy"); strategy:description("v1 2018-10-14"); strategy.parameters:addGroup("Trading"); strategy.parameters:addBoolean("AllowLong", "Allow strategy to Close Long Positions", "", false); strategy.parameters:addBoolean("AllowShort", "Allow strategy to Close Short Positions", "", false); strategy.parameters:addString("action", "Action", "", "close"); strategy.parameters:addStringAlternative("action", "Close all positions", "", "close"); strategy.parameters:addStringAlternative("action", "Hedge", "", "hedge"); strategy.parameters:addString("account", "Account to trade on", "", ""); strategy.parameters:setFlag("account", core.FLAG_ACCOUNT); trading_logic:Init(strategy.parameters); strategy.parameters:addGroup("Alert"); signaler:Init(strategy.parameters); end local custom_id; local AllowLong, AllowShort; local action; local indicator; local Account; local Offer; function Prepare(name_only) for _, module in pairs(Modules) do module:Prepare(nameOnly); end instance:name(profile:id() .. "(" .. instance.bid:name() .. ")"); if name_only then return ; end action = instance.parameters.action; AllowShort = instance.parameters.AllowShort; AllowLong = instance.parameters.AllowLong; Account = instance.parameters.account; assert(core.indicators:findIndicator("TRENDSTOP") ~= nil, "Please download and install TRENDSTOP indicator!"); Offer = core.host:findTable("offers"):find("Instrument", instance.bid:instrument()).OfferID; indicator = core.indicators:create("TRENDSTOP", trading_logic.MainSource); trading_logic.DoTrading = EntryFunction; custom_id = profile:id() .. "_" .. instance.bid:name(); end function ExtUpdate(id, source, period) for _, module in pairs(Modules) do if module.BlockTrading ~= nil and module:BlockTrading(id, source, period) then return; end end for _, module in pairs(Modules) do if module.ExtUpdate ~= nil then module:ExtUpdate(id, source, period); end end end function ReleaseInstance() for _, module in pairs(Modules) do if module.ReleaseInstance ~= nil then module:ReleaseInstance(); end end end local last_serial; function EntryFunction(source, period) if last_serial == source.close:serial(period) then return; end indicator:update(core.UpdateLast); if core.crossesOver(source.close, indicator.DATA, period) and AllowShort then core.host:trace("Short"); if action == "close" then signaler:Signal("Short Position Closed"); exit("S"); else signaler:Signal("Hedging short positions"); if instance.parameters.position_cap then local buy_positions = trading:FindTrade() :WhenSide("B") :WhenCustomID(custom_id) :Count(); local positions = trading:FindTrade() :WhenCustomID(custom_id) :Count(); if positions >= instance.parameters.no_of_positions or buy_positions >= instance.parameters.no_of_buy_position then signaler:Signal("Positions limit has been reached"); return; end end HedgePositions("S"); core.host:execute("stop"); end last_serial = source.close:serial(period); elseif core.crossesUnder(source.close, indicator.DATA, period) and AllowLong then core.host:trace("Long"); if action == "close" then signaler:Signal("Long Position Closed"); exit("B"); else signaler:Signal("Hedging long positions"); if instance.parameters.position_cap then local sell_positions = trading:FindTrade() :WhenSide("S") :WhenCustomID(custom_id) :Count(); local positions = trading:FindTrade() :WhenCustomID(custom_id) :Count(); if positions >= instance.parameters.no_of_positions or sell_positions >= instance.parameters.no_of_sell_position then signaler:Signal("Positions limit has been reached"); return; end end HedgePositions("B"); core.host:execute("stop"); end last_serial = source.close:serial(period); end end function CalculateTotalAmount(offer_id, account_id, bs) local total = 0; local count=0; local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if row.OfferID == offer_id and row.AccountID == account_id and row.BS == bs then total = total + row.AmountK; count=count+row.Lot; end row = enum:next(); end return total,count; end function HedgePositions(side) local offers = core.host:findTable("offers"):enumerator(); local offer = offers:next(); while (offer ~= nil) do local offer_id = offer.OfferID; local enum = core.host:findTable("accounts"):enumerator(); local row = enum:next(); while (row ~= nil) do if row.Hedging == "Y" then local total_amount = CalculateTotalAmount(offer_id, row.AccountID, side); local base_size = core.host:execute("getTradingProperty", "baseUnitSize", instance.bid:instrument(), row.AccountID); if total_amount > 0 then if side == "B" then OpenMarket(offer_id, row.AccountID, "S", total_amount * base_size); else OpenMarket(offer_id, row.AccountID, "B", total_amount * base_size); end end end row = enum:next(); end offer = offers:next(); end end function OpenMarket(offer_id, account_id, bs, amount) local valuemap = core.valuemap(); valuemap.OrderType = "OM"; valuemap.OfferID = offer_id; valuemap.AcctID = account_id; valuemap.Quantity = amount; valuemap.BuySell = bs; local success, msg = terminal:execute(200, valuemap); assert(success, msg); end -- exit from the specified direction function exit(BuySell) if not(AllowLong) and not(AllowShort) then return ; end local enum, row, valuemap, success, msg; -- check whether we have at least one trade on the specified account -- in the specified direction for the specified instrument local count = 0; enum = core.host:findTable("trades"):enumerator(); row = enum:next(); while count == 0 and row ~= nil do if row.AccountID == Account and row.OfferID == Offer and row.BS == BuySell then count = count + 1; end row = enum:next(); end if count > 0 then valuemap = core.valuemap(); -- switch the direction since the order must be in oppsite direction if BuySell == "B" then BuySell = "S"; else BuySell = "B"; end valuemap.OrderType = "CM"; valuemap.OfferID = Offer; valuemap.AcctID = Account; valuemap.TradeID = "all"; valuemap.NetQtyFlag = "Y"; valuemap.BuySell = BuySell; valuemap.CustomID = CustomID; success, msg = terminal:execute(101, valuemap); if not(success) then terminal:alertMessage(instance.bid:instrument(), instance.bid[instance.bid:size() - 1], "Open order failed" .. msg, instance.bid:date(instance.bid:size() - 1)); end end end function ExtAsyncOperationFinished(cookie, success, message, message1, message2) for _, module in pairs(Modules) do if module.AsyncOperationFinished ~= nil then module:AsyncOperationFinished(cookie, success, message, message1, message2); end end end dofile(core.app_path() .. "\\strategies\\standard\\include\\helper.lua"); trading_logic = {}; -- public fields trading_logic.Name = "Trading logic"; trading_logic.Version = "1.2.3"; trading_logic.Debug = false; trading_logic.DoTrading = nil; trading_logic.DoExit = nil; trading_logic.MainSource = nil; --private fields trading_logic._ids_start = nil; trading_logic._trading_source_id = nil; function trading_logic:trace(str) if not self.Debug then return; end core.host:trace(self.Name .. ": " .. str); end function trading_logic:OnNewModule(module) end function trading_logic:RegisterModule(modules) for _, module in pairs(modules) do self:OnNewModule(module); module:OnNewModule(self); end modules[#modules + 1] = self; self._ids_start = (#modules) * 100; end function trading_logic:Init(parameters) parameters:addGroup("Price"); parameters:addBoolean("is_bid", "Price Type","", true); parameters:setFlag("is_bid", core.FLAG_BIDASK); parameters:addString("timeframe", "Time frame", "", "m5"); parameters:setFlag("timeframe", core.FLAG_PERIODS); parameters:addString("entry_execution_type", "Execution Type", "Once per bar close or on every tick", "Live"); parameters:addStringAlternative("entry_execution_type", "End of Turn", "", "EndOfTurn"); parameters:addStringAlternative("entry_execution_type", "Live", "", "Live"); end function trading_logic:GetLastPeriod(source_period, source, target) if source_period < 0 or target:size() < 2 then return nil; end local s1, e1 = core.getcandle(source:barSize(), source:date(source_period), -7, 0); local s2, e2 = core.getcandle(target:barSize(), target:date(NOW - 1), -7, 0); if e1 == e2 then return target:size() - 2; else return target:size() - 1; end end function trading_logic:GetPeriod(source_period, source, target) if source_period < 0 then return nil; end local source_date = source:date(source_period); local index = core.findDate(target, source_date, false); if index == -1 then return nil; end return index; end function trading_logic:Prepare(name_only) if name_only then return; end if instance.parameters.entry_execution_type == "Live" then self._trading_source_id = self._ids_start + 1; ExtSubscribe(self._trading_source_id, nil, "t1", instance.parameters.is_bid, "close"); self:trace("Trading on tick (live data)"); else self._trading_source_id = self._ids_start + 2; self:trace(string.format("Trading on %s bar", instance.parameters.timeframe)); end self.MainSource = ExtSubscribe(self._ids_start + 2, nil, instance.parameters.timeframe, instance.parameters.is_bid, "bar"); end function trading_logic:ExtUpdate(id, source, period) if id == self._trading_source_id and self.DoTrading ~= nil then local period2 = period; if source ~= self.MainSource then period2 = core.findDate(self.MainSource, source:date(period), false); if period2 == -1 then return; end end self.DoTrading(self.MainSource, period2); end if id == self._exit_source_id and self.DoExit ~= nil then local period2 = period; if source ~= self.MainSource then period2 = core.findDate(self.MainSource, source:date(period), false); if period2 == -1 then return; end end self.DoExit(self.MainSource, period2); end end trading_logic:RegisterModule(Modules); signaler = {}; signaler.Name = "Signaler"; signaler.Debug = false; signaler.Version = "1.3.0"; signaler._show_alert = nil; signaler._sound_file = nil; signaler._recurrent_sound = nil; signaler._email = nil; signaler._ids_start = nil; signaler._advanced_alert_timer = nil; signaler._tz = nil; signaler._alerts = {}; function signaler:trace(str) if not self.Debug then return; end core.host:trace(self.Name .. ": " .. str); end function signaler:OnNewModule(module) end function signaler:RegisterModule(modules) for _, module in pairs(modules) do self:OnNewModule(module); module:OnNewModule(self); end modules[#modules + 1] = self; self._ids_start = (#modules) * 100; end function signaler:ToJSON(item) local json = {}; function json:AddStr(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":\"%s\"", separator, tostring(name), tostring(value)); end function json:AddNumber(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%f", separator, tostring(name), value or 0); end function json:AddBool(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%s", separator, tostring(name), value and "true" or "false"); end function json:ToString() return "{" .. (self.str or "") .. "}"; end local first = true; for idx,t in pairs(item) do local stype = type(t) if stype == "number" then json:AddNumber(idx, t); elseif stype == "string" then json:AddStr(idx, t); elseif stype == "boolean" then json:AddBool(idx, t); elseif stype == "function" or stype == "table" then --do nothing else core.host:trace(tostring(idx) .. " " .. tostring(stype)); end end return json:ToString(); end function signaler:ArrayToJSON(arr) local str = "["; for i, t in ipairs(self._alerts) do local json = self:ToJSON(t); if str == "[" then str = str .. json; else str = str .. "," .. json; end end return str .. "]"; end function signaler:AsyncOperationFinished(cookie, success, message, message1, message2) if cookie == self._advanced_alert_timer and #self._alerts > 0 and (self.last_req == nil or not self.last_req:loading()) then if self._advanced_alert_key == nil then return; end local data = self:ArrayToJSON(self._alerts); self._alerts = {}; self.last_req = http_lua.createRequest(); local query = string.format('{"Key":"%s","StrategyName":"%s","Platform":"FXTS2","Notifications":%s}', self._advanced_alert_key, string.gsub(self.StrategyName or "", '"', '\\"'), data); self.last_req:setRequestHeader("Content-Type", "application/json"); self.last_req:setRequestHeader("Content-Length", tostring(string.len(query))); self.last_req:start("http://profitrobots.com/api/v1/notification", "POST", query); end end function signaler:FormatEmail(source, period, message) --format email subject local subject = message .. "(" .. source:instrument() .. ")"; --format email text local delim = "\013\010"; local signalDescr = "Signal: " .. (self.StrategyName or ""); local symbolDescr = "Symbol: " .. source:instrument(); local messageDescr = "Message: " .. message; local ttime = core.dateToTable(core.host:execute("convertTime", core.TZ_EST, self._ToTime, source:date(period))); local dateDescr = string.format("Time: %02i/%02i %02i:%02i", ttime.month, ttime.day, ttime.hour, ttime.min); local priceDescr = "Price: " .. source[period]; local text = "You have received this message because the following signal alert was received:" .. delim .. signalDescr .. delim .. symbolDescr .. delim .. messageDescr .. delim .. dateDescr .. delim .. priceDescr; return subject, text; end function signaler:Signal(label, source) if source == nil then source = instance.bid; if instance.bid == nil then local pane = core.host.Window.CurrentPane; source = pane.Data:getStream(0); else source = instance.bid; end end if self._show_alert then terminal:alertMessage(source:instrument(), source[NOW], label, source:date(NOW)); end if self._sound_file ~= nil then terminal:alertSound(self._sound_file, self._recurrent_sound); end if self._email ~= nil then terminal:alertEmail(self._email, profile:id().. " : " .. label, self:FormatEmail(source, NOW, label)); end if self._advanced_alert_key ~= nil then self:AlertTelegram(label, source:instrument(), source:barSize()); end if self._show_popup == true then local subject, text = self:FormatEmail(source, NOW, label); core.host:execute("prompt", self._ids_start + 2, subject, text); end end function signaler:AlertTelegram(message, instrument, timeframe) if core.host.Trading:getTradingProperty("isSimulation") then return; end local alert = {}; alert.Text = message or ""; alert.Instrument = instrument or ""; alert.TimeFrame = timeframe or ""; self._alerts[#self._alerts + 1] = alert; end function signaler:Init(parameters) parameters:addInteger("signaler_ToTime", "Convert the date to", "", 6) parameters:addIntegerAlternative("signaler_ToTime", "EST", "", 1) parameters:addIntegerAlternative("signaler_ToTime", "UTC", "", 2) parameters:addIntegerAlternative("signaler_ToTime", "Local", "", 3) parameters:addIntegerAlternative("signaler_ToTime", "Server", "", 4) parameters:addIntegerAlternative("signaler_ToTime", "Financial", "", 5) parameters:addIntegerAlternative("signaler_ToTime", "Display", "", 6) parameters:addBoolean("signaler_show_alert", "Show Alert", "", true); parameters:addBoolean("signaler_play_sound", "Play Sound", "", false); parameters:addFile("signaler_sound_file", "Sound File", "", ""); parameters:setFlag("signaler_sound_file", core.FLAG_SOUND); parameters:addBoolean("signaler_recurrent_sound", "Recurrent Sound", "", true); parameters:addBoolean("signaler_send_email", "Send Email", "", false); parameters:addString("signaler_email", "Email", "", ""); parameters:setFlag("signaler_email", core.FLAG_EMAIL); if indicator ~= nil and strategy == nil then parameters:addBoolean("signaler_show_popup", "Show Popup", "", false); end parameters:addBoolean("use_advanced_alert", "Send Advanced Alert", "Telegram message or Channel post", false); parameters:addString("advanced_alert_key", "Advanced Alert Key", "You can get it via @profit_robots_bot Telegram bot", ""); end function signaler:Prepare(name_only) self._ToTime = instance.parameters.signaler_ToTime if self._ToTime == 1 then self._ToTime = core.TZ_EST elseif self._ToTime == 2 then self._ToTime = core.TZ_UTC elseif self._ToTime == 3 then self._ToTime = core.TZ_LOCAL elseif self._ToTime == 4 then self._ToTime = core.TZ_SERVER elseif self._ToTime == 5 then self._ToTime = core.TZ_FINANCIAL elseif self._ToTime == 6 then self._ToTime = core.TZ_TS end if instance.parameters.signaler_play_sound then self._sound_file = instance.parameters.signaler_sound_file; assert(self._sound_file ~= "", "Sound file must be chosen"); end self._show_alert = instance.parameters.signaler_show_alert; self._recurrent_sound = instance.parameters.signaler_recurrent_sound; self._show_popup = instance.parameters.signaler_show_popup; if instance.parameters.signaler_send_email then self._email = instance.parameters.signaler_email; assert(self._email ~= "", "E-mail address must be specified"); end --do what you usually do in prepare if name_only then return; end if instance.parameters.advanced_alert_key ~= "" and instance.parameters.use_advanced_alert then self._advanced_alert_key = instance.parameters.advanced_alert_key; require("http_lua"); self._advanced_alert_timer = self._ids_start + 1; core.host:execute("setTimer", self._advanced_alert_timer, 1); end end signaler:RegisterModule(Modules); trading = {}; trading.Name = "Trading"; trading.Version = "4.8"; trading.Debug = false; trading.AddAmountParameter = true; trading.AddStopParameter = true; trading.AddLimitParameter = true; trading._ids_start = nil; trading._signaler = nil; trading._account = nil; trading._amount = 1; trading._all_modules = {}; trading._limit = nil; trading._stop = nil; trading._trailing_stop = nil; trading._request_id = {}; trading._waiting_requests = {}; trading._used_stop_orders = {}; trading._used_limit_orders = {}; function trading:trace(str) if not self.Debug then return; end core.host:trace(self.Name .. ": " .. str); end function trading:RegisterModule(modules) for _, module in pairs(modules) do self:OnNewModule(module); module:OnNewModule(self); end modules[#modules + 1] = self; self._ids_start = (#modules) * 100; end function trading:Init(parameters) parameters:addBoolean("position_cap", "Position Cap", "", false); parameters:addInteger("no_of_positions", "No of open positions", "", 1); parameters:addInteger("no_of_buy_position", "Max # of buy positions", "", 1); parameters:addInteger("no_of_sell_position", "Max # of sell positions", "", 1); end function trading:Prepare(name_only) --do what you usually do in prepare if name_only then return; end self._account = instance.parameters.account; if self.AddAmountParameter then self._amount = instance.parameters.amount; end if instance.parameters.set_limit then self._limit = instance.parameters.limit; end if instance.parameters.set_stop then self._stop = instance.parameters.stop; if instance.parameters.use_trailing then self._trailing_stop = instance.parameters.trailing; end end end function trading:OnNewModule(module) if module.Name == "Signaler" then self._signaler = module; end self._all_modules[#self._all_modules + 1] = module; end function trading:AsyncOperationFinished(cookie, success, message, message1, message2) local res = self._waiting_requests[cookie]; if res ~= nil then res.Finished = true; res.Success = success; res.Error = not success and message or nil; if self._signaler ~= nil then self._signaler:Signal(error_message); else self:trace(error_message); end self._waiting_requests[cookie] = nil; elseif cookie == self._order_update_id then for _, order in ipairs(self._monitored_orders) do if order.RequestID == message2 then order.FixStatus = message1; end end elseif cookie == self._ids_start + 2 then if not success then if self._signaler ~= nil then self._signaler:Signal("Close order failed: " .. message); else self:trace("Close order failed: " .. message); end end end end function trading:calculateAmount() return self._amount; end function trading:getOppositeSide(side) if side == "B" then return "S"; end return "B"; end function trading:getId() for id = self._ids_start, self._ids_start + 100 do if self._waiting_requests[id] == nil then return id; end end return self._ids_start; end function trading:CreateStopOrder(trade, stop_rate, trailing) local valuemap = core.valuemap(); valuemap.Command = "CreateOrder"; valuemap.OfferID = trade.OfferID; valuemap.Rate = stop_rate; if trade.BS == "B" then valuemap.BuySell = "S"; else valuemap.BuySell = "B"; end local can_close = core.host:execute("getTradingProperty", "canCreateMarketClose", trade.Instrument, trade.AccountID); if can_close then valuemap.OrderType = "S"; valuemap.AcctID = trade.AccountID; valuemap.TradeID = trade.TradeID; valuemap.Quantity = trade.Lot; valuemap.TrailUpdatePips = trailing; else valuemap.OrderType = "SE" valuemap.AcctID = trade.AccountID; valuemap.NetQtyFlag = "Y" end local id = self:getId(); local success, msg = terminal:execute(id, valuemap); if not(success) then local message = "Failed create stop " .. msg; self:trace(message); if self._signaler ~= nil then self._signaler:Signal(message); end local res = {}; res.Finished = true; res.Success = false; res.Error = message; return res; end local res = {}; res.Finished = false; res.RequestID = msg; self._waiting_requests[id] = res; self._request_id[trade.TradeID] = msg; return res; end function trading:CreateLimitOrder(trade, limit_rate) local valuemap = core.valuemap(); valuemap.Command = "CreateOrder"; valuemap.OfferID = trade.OfferID; valuemap.Rate = limit_rate; if trade.BS == "B" then valuemap.BuySell = "S"; else valuemap.BuySell = "B"; end local can_close = core.host:execute("getTradingProperty", "canCreateMarketClose", trade.Instrument, trade.AccountID); if can_close then valuemap.OrderType = "L"; valuemap.AcctID = trade.AccountID; valuemap.TradeID = trade.TradeID; valuemap.Quantity = trade.Lot; else valuemap.OrderType = "LE" valuemap.AcctID = trade.AccountID; valuemap.NetQtyFlag = "Y" end local success, msg = terminal:execute(200, valuemap); if not(success) then terminal:alertMessage(trade.Instrument, limit_rate, "Failed create limit " .. msg, core.now()); else self._request_id[trade.TradeID] = msg; end end function trading:ChangeOrder(order, rate, trailing) local min_change = core.host:findTable("offers"):find("Instrument", order.Instrument).PointSize; if math.abs(rate - order.Rate) > min_change then self:trace(string.format("Changing an order to %s", tostring(rate))); -- stop exists local valuemap = core.valuemap(); valuemap.Command = "EditOrder"; valuemap.AcctID = order.AccountID; valuemap.OrderID = order.OrderID; valuemap.TrailUpdatePips = trailing; valuemap.Rate = rate; local id = self:getId(); local success, msg = terminal:execute(id, valuemap); if not(success) then local message = "Failed change order " .. msg; self:trace(message); if self._signaler ~= nil then self._signaler:Signal(message); end local res = {}; res.Finished = true; res.Success = false; res.Error = message; return res; end local res = {}; res.Finished = false; res.RequestID = msg; self._waiting_requests[id] = res; return res; end local res = {}; res.Finished = true; res.Success = true; return res; end function trading:IsLimitOrderType(order_type) return order_type == "L" or order_type == "LE" or order_type == "LT" or order_type == "LTE"; end function trading:IsStopOrderType(order_type) return order_type == "S" or order_type == "SE" or order_type == "ST" or order_type == "STE"; end function trading:FindLimitOrder(trade) local can_close = core.host:execute("getTradingProperty", "canCreateMarketClose", trade.Instrument, trade.AccountID); if can_close then local order_id; if trade.LimitOrderID ~= nil and trade.LimitOrderID ~= "" then order_id = trade.LimitOrderID; self:trace("Using limit order id from the trade"); elseif self._request_id[trade.TradeID] ~= nil then self:trace("Searching limit order by request id: " .. tostring(self._request_id[trade.TradeID])); local order = core.host:findTable("orders"):find("RequestID", self._request_id[trade.TradeID]); if order ~= nil then order_id = order.OrderID; self._request_id[trade.TradeID] = nil; end end -- Check that order is stil exist if order_id ~= nil then return core.host:findTable("orders"):find("OrderID", order_id); end else local enum = core.host:findTable("orders"):enumerator(); local row = enum:next(); while (row ~= nil) do if row.ContingencyType == 3 and IsLimitOrderType(row.Type) and self._used_limit_orders[row.OrderID] ~= true then self._used_limit_orders[row.OrderID] = true; return row; end row = enum:next(); end end return nil; end function trading:FindStopOrder(trade) local can_close = core.host:execute("getTradingProperty", "canCreateMarketClose", trade.Instrument, trade.AccountID); if can_close then local order_id; if trade.StopOrderID ~= nil and trade.StopOrderID ~= "" then order_id = trade.StopOrderID; self:trace("Using stop order id from the trade"); elseif self._request_id[trade.TradeID] ~= nil then self:trace("Searching stop order by request id: " .. tostring(self._request_id[trade.TradeID])); local order = core.host:findTable("orders"):find("RequestID", self._request_id[trade.TradeID]); if order ~= nil then order_id = order.OrderID; self._request_id[trade.TradeID] = nil; end end -- Check that order is stil exist if order_id ~= nil then return core.host:findTable("orders"):find("OrderID", order_id); end else local enum = core.host:findTable("orders"):enumerator(); local row = enum:next(); while (row ~= nil) do if row.ContingencyType == 3 and self:IsStopOrderType(row.Type) and self._used_stop_orders[row.OrderID] ~= true then self._used_stop_orders[row.OrderID] = true; return row; end row = enum:next(); end end return nil; end function trading:MoveStop(trade, stop_rate, trailing) local order = self:FindStopOrder(trade); if order == nil then if trailing == 0 then trailing = nil; end return self:CreateStopOrder(trade, stop_rate, trailing); else if trailing == 0 then if order.TrlMinMove ~= 0 then trailing = order.TrlMinMove else trailing = nil; end end return self:ChangeOrder(order, stop_rate, trailing); end end function trading:MoveLimit(trade, limit_rate) self:trace("Searching for a limit"); local order = self:FindLimitOrder(trade); if order == nil then self:trace("Limit order not found, creating a new one"); return self:CreateLimitOrder(trade, limit_rate); else return self:ChangeOrder(order, limit_rate); end end function trading:RemoveStop(trade) self:trace("Searching for a stop"); local order = self:FindStopOrder(trade); if order == nil then self:trace("No stop"); return nil; end self:trace("Deleting order"); return self:DeleteOrder(order); end function trading:RemoveLimit(trade) self:trace("Searching for a limit"); local order = self:FindLimitOrder(trade); if order == nil then self:trace("No limit"); return nil; end self:trace("Deleting order"); return self:DeleteOrder(order); end function trading:DeleteOrder(order) self:trace(string.format("Deleting order %s", order.OrderID)); local valuemap = core.valuemap(); valuemap.Command = "DeleteOrder"; valuemap.OrderID = order.OrderID; local id = self:getId(); local success, msg = terminal:execute(id, valuemap); if not(success) then local message = "Delete order failed: " .. msg; self:trace(message); if self._signaler ~= nil then self._signaler:Signal(message); end local res = {}; res.Finished = true; res.Success = false; res.Error = message; return res; end local res = {}; res.Finished = false; res.RequestID = msg; self._waiting_requests[id] = res; return res; end function trading:GetCustomID(qtxt) if qtxt == nil then return nil; end local metadata = self:GetMetadata(qtxt); if metadata == nil then return qtxt; end return metadata.CustomID; end function trading:FindOrder() local search = {}; function search:WhenCustomID(custom_id) self.CustomID = custom_id; return self; end function search:WhenSide(bs) self.Side = bs; return self; end function search:WhenInstrument(instrument) self.Instrument = instrument; return self; end function search:WhenAccountID(account_id) self.AccountID = account_id; return self; end function search:WhenRate(rate) self.Rate = rate; return self; end function search:WhenOrderType(orderType) self.OrderType = orderType; return self; end function search:Do(action) local enum = core.host:findTable("orders"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then action(row); end row = enum:next(); end end function search:PassFilter(row) return (row.Instrument == self.Instrument or not self.Instrument) and (row.BS == self.Side or not self.Side) and (row.AccountID == self.AccountID or not self.AccountID) and (trading:GetCustomID(row.QTXT) == self.CustomID or not self.CustomID) and (row.Rate == self.Rate or not self.Rate) and (row.Type == self.OrderType or not self.OrderType); end function search:All() local enum = core.host:findTable("orders"):enumerator(); local row = enum:next(); local orders = {}; while (row ~= nil) do if self:PassFilter(row) then orders[#orders + 1] = row; end row = enum:next(); end return orders; end function search:First() local enum = core.host:findTable("orders"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then return row; end row = enum:next(); end return nil; end return search; end function trading:FindTrade() local search = {}; function search:WhenCustomID(custom_id) self.CustomID = custom_id; return self; end function search:WhenSide(bs) self.Side = bs; return self; end function search:WhenInstrument(instrument) self.Instrument = instrument; return self; end function search:WhenAccountID(account_id) self.AccountID = account_id; return self; end function search:WhenOpen(open) self.Open = open; return self; end function search:WhenOpenOrderReqID(open_order_req_id) self.OpenOrderReqID = open_order_req_id; return self; end function search:Do(action) local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then action(row); end row = enum:next(); end end function search:PassFilter(row) return (row.Instrument == self.Instrument or not self.Instrument) and (row.BS == self.Side or not self.Side) and (row.AccountID == self.AccountID or not self.AccountID) and (trading:GetCustomID(row.QTXT) == self.CustomID or not self.CustomID) and (row.Open == self.Open or not self.Open) and (row.OpenOrderReqID == self.OpenOrderReqID or not self.OpenOrderReqID); end function search:All() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); local trades = {}; while (row ~= nil) do if self:PassFilter(row) then trades[#trades + 1] = row; end row = enum:next(); end return trades; end function search:Any() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then return true; end row = enum:next(); end return false; end function search:Count() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); local count = 0; while (row ~= nil) do if self:PassFilter(row) then count = count + 1; end row = enum:next(); end return count; end function search:First() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then return row; end row = enum:next(); end return nil; end return search; end function trading:FindClosedTrade() local search = {}; function search:WhenCustomID(custom_id) self.CustomID = custom_id; return self; end function search:WhenSide(bs) self.Side = bs; return self; end function search:WhenInstrument(instrument) self.Instrument = instrument; return self; end function search:WhenAccountID(account_id) self.AccountID = account_id; return self; end function search:WhenOpenOrderReqID(open_order_req_id) self.OpenOrderReqID = open_order_req_id; return self; end function search:PassFilter(row) return (row.Instrument == self.Instrument or not self.Instrument) and (row.BS == self.Side or not self.Side) and (row.AccountID == self.AccountID or not self.AccountID) and (trading:GetCustomID(row.QTXT) == self.CustomID or not self.CustomID) and (row.OpenOrderReqID == self.OpenOrderReqID or not self.OpenOrderReqID); end function search:Any() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then return true; end row = enum:next(); end return false; end function search:All() local enum = core.host:findTable("trades"):enumerator(); local row = enum:next(); local trades = {}; while (row ~= nil) do if self:PassFilter(row) then trades[#trades + 1] = row; end row = enum:next(); end return trades; end function search:First() local enum = core.host:findTable("closed trades"):enumerator(); local row = enum:next(); while (row ~= nil) do if self:PassFilter(row) then return row; end row = enum:next(); end return nil; end return search; end function trading:ParialClose(trade, amount) -- not finished local account = core.host:findTable("accounts"):find("AccountID", trade.AccountID); local id = self:getId(); if account.Hedging == "Y" then local valuemap = core.valuemap(); valuemap.BuySell = trade.BS == "B" and "S" or "B"; valuemap.OrderType = "CM"; valuemap.OfferID = trade.OfferID; valuemap.AcctID = trade.AccountID; valuemap.TradeID = trade.TradeID; valuemap.Quantity = math.min(amount, trade.Lot); local success, msg = terminal:execute(id, valuemap); if success then local res = trading:ClosePartialSuccessResult(msg); self._waiting_requests[id] = res; return res; end return trading:ClosePartialFailResult(msg); end local valuemap = core.valuemap(); valuemap.OrderType = "OM"; valuemap.OfferID = trade.OfferID; valuemap.AcctID = trade.AccountID; valuemap.Quantity = math.min(amount, trade.Lot); valuemap.BuySell = trading:getOppositeSide(trade.BS); local success, msg = terminal:execute(id, valuemap); if success then local res = trading:ClosePartialSuccessResult(msg); self._waiting_requests[id] = res; return res; end return trading:ClosePartialFailResult(msg); end function trading:ClosePartialSuccessResult(msg) local res = {}; if msg ~= nil then res.Finished = false; else res.Finished = true; end res.RequestID = msg; function res:ToJSON() return trading:ObjectToJson(self); end return res; end function trading:ClosePartialFailResult(message) local res = {}; res.Finished = true; res.Success = false; res.Error = message; return res; end function trading:Close(trade) local valuemap = core.valuemap(); valuemap.BuySell = trade.BS == "B" and "S" or "B"; valuemap.OrderType = "CM"; valuemap.OfferID = trade.OfferID; valuemap.AcctID = trade.AccountID; valuemap.TradeID = trade.TradeID; valuemap.Quantity = trade.Lot; local success, msg = terminal:execute(self._ids_start + 3, valuemap); if not(success) then if self._signaler ~= nil then self._signaler:Signal("Close failed: " .. msg); end return false; end return true; end function trading:ObjectToJson(obj) local json = {}; function json:AddStr(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":\"%s\"", separator, tostring(name), tostring(value)); end function json:AddNumber(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%f", separator, tostring(name), value or 0); end function json:AddBool(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%s", separator, tostring(name), value and "true" or "false"); end function json:ToString() return "{" .. (self.str or "") .. "}"; end local first = true; for idx,t in pairs(obj) do local stype = type(t) if stype == "number" then json:AddNumber(idx, t); elseif stype == "string" then json:AddStr(idx, t); elseif stype == "boolean" then json:AddBool(idx, t); elseif stype == "function" or stype == "table" then --do nothing else core.host:trace(tostring(idx) .. " " .. tostring(stype)); end end return json:ToString(); end function trading:CreateEntryOrderSuccessResult(msg) local res = {}; if msg ~= nil then res.Finished = false; else res.Finished = true; end res.RequestID = msg; function res:IsOrderExecuted() return self.FixStatus ~= nil and self.FixStatus == "F"; end function res:GetOrder() if self._order == nil then self._order = core.host:findTable("orders"):find("RequestID", self.RequestID); if self._order == nil then return nil; end end if not self._order:refresh() then return nil; end return self._order; end function res:GetTrade() if self._trade == nil then self._trade = core.host:findTable("trades"):find("OpenOrderReqID", self.RequestID); if self._trade == nil then return nil; end end if not self._trade:refresh() then return nil; end return self._trade; end function res:GetClosedTrade() if self._closed_trade == nil then self._closed_trade = core.host:findTable("closed trades"):find("OpenOrderReqID", self.RequestID); if self._closed_trade == nil then return nil; end end if not self._closed_trade:refresh() then return nil; end return self._closed_trade; end function res:ToJSON() return trading:ObjectToJson(self); end return res; end function trading:CreateEntryOrderFailResult(message) local res = {}; res.Finished = true; res.Success = false; res.Error = message; function res:GetOrder() return nil; end function res:GetClosedTrade() return nil; end function res:IsOrderExecuted() return false; end return res; end function trading:EntryOrder(instrument) local builder = {}; builder.Offer = core.host:findTable("offers"):find("Instrument", instrument); builder.Instrument = instrument; builder.Parent = self; builder.valuemap = core.valuemap(); builder.valuemap.Command = "CreateOrder"; builder.valuemap.OfferID = builder.Offer.OfferID; builder.valuemap.AcctID = self._account; function builder:_GetBaseUnitSize() if self._base_size == nil then self._base_size = core.host:execute("getTradingProperty", "baseUnitSize", self.Instrument, self.valuemap.AcctID); end return self._base_size; end function builder:SetAccountID(accountID) self.valuemap.AcctID = accountID; return self; end function builder:SetDefaultAmount() self.valuemap.Quantity = self.Parent:calculateAmount() * self:_GetBaseUnitSize(); return self; end function builder:SetAmount(amount) self.valuemap.Quantity = amount * self:_GetBaseUnitSize(); return self; end function builder:SetPercentOfEquityAmount(percent) self._PercentOfEquityAmount = percent; return self; end function builder:SetSide(buy_sell) self.valuemap.BuySell = buy_sell; return self; end function builder:SetRate(rate) if self.valuemap.BuySell == "B" then self.valuemap.OrderType = self.Offer.Ask > rate and "LE" or "SE"; else self.valuemap.OrderType = self.Offer.Bid > rate and "SE" or "LE"; end self.valuemap.Rate = rate; return self; end function builder:SetPipLimit(limit_type, limit) self.valuemap.PegTypeLimit = limit_type or "M"; self.valuemap.PegPriceOffsetPipsLimit = self.valuemap.BuySell == "B" and limit or -limit; return self; end function builder:SetLimit(limit) self.valuemap.RateLimit = limit; return self; end function builder:SetPipStop(stop_type, stop, trailing_stop) self.valuemap.PegTypeStop = stop_type or "O"; self.valuemap.PegPriceOffsetPipsStop = self.valuemap.BuySell == "B" and -stop or stop; self.valuemap.TrailStepStop = trailing_stop; return self; end function builder:SetStop(stop, trailing_stop) self.valuemap.RateStop = stop; self.valuemap.TrailStepStop = trailing_stop; return self; end function builder:UseDefaultCustomId() self.valuemap.CustomID = self.Parent.CustomID; return self; end function builder:SetCustomID(custom_id) self.valuemap.CustomID = custom_id; return self; end function builder:GetValueMap() return self.valuemap; end function builder:AddMetadata(id, val) if self._metadata == nil then self._metadata = {}; end self._metadata[id] = val; return self; end function builder:Execute() local desc = string.format("Creating %s %s for %s at %f", self.valuemap.BuySell, self.valuemap.OrderType, self.Instrument, self.valuemap.Rate); if self._metadata ~= nil then self._metadata.CustomID = self.valuemap.CustomID; self.valuemap.CustomID = trading:ObjectToJson(self._metadata); end if self.valuemap.RateStop ~= nil then desc = desc .. " stop " .. self.valuemap.RateStop; end if self.valuemap.RateLimit ~= nil then desc = desc .. " limit " .. self.valuemap.RateLimit; end self.Parent:trace(desc); if self._PercentOfEquityAmount ~= nil then local equity = core.host:findTable("accounts"):find("AccountID", self.valuemap.AcctID).Equity; local affordable_loss = equity * self._PercentOfEquityAmount / 100.0; local stop = math.abs(self.valuemap.RateStop - self.valuemap.Rate) / self.Offer.PointSize; local possible_loss = self.Offer.PipCost * stop; self.valuemap.Quantity = math.floor(affordable_loss / possible_loss) * self:_GetBaseUnitSize(); end for _, module in pairs(self.Parent._all_modules) do if module.BlockOrder ~= nil and module:BlockOrder(self.valuemap) then self.Parent:trace("Creation of order blocked by " .. module.Name); return trading:CreateEntryOrderFailResult("Creation of order blocked by " .. module.Name); end end for _, module in pairs(self.Parent._all_modules) do if module.OnOrder ~= nil then module:OnOrder(self.valuemap); end end local id = self.Parent:getId(); local success, msg = terminal:execute(id, self.valuemap); if not(success) then local message = "Open order failed: " .. msg; self.Parent:trace(message); if self.Parent._signaler ~= nil then self.Parent._signaler:Signal(message); end return trading:CreateEntryOrderFailResult(message); end local res = trading:CreateEntryOrderSuccessResult(msg); self.Parent._waiting_requests[id] = res; return res; end return builder; end function trading:StoreMarketOrderResults(res) local str = "["; for i, t in ipairs(res) do local json = t:ToJSON(); if str == "[" then str = str .. json; else str = str .. "," .. json; end end return str .. "]"; end function trading:RestoreMarketOrderResults(str) local results = {}; local position = 2; local result; while (position < str:len()) do local ch = string.sub(str, position, position); if ch == "{" then result = trading:CreateMarketOrderSuccessResult(); position = position + 1; elseif ch == "}" then results[#results + 1] = result; result = nil; position = position + 1; elseif ch == "," then position = position + 1; else local name, value = string.match(str, '"([^"]+)":("?[^,}]+"?)', position); if value == "false" then result[name] = false; position = position + name:len() + 8; elseif value == "true" then result[name] = true; position = position + name:len() + 7; else if string.sub(value, 1, 1) == "\"" then result[name] = value; value:sub(2, value:len() - 1); position = position + name:len() + 3 + value:len(); else result[name] = tonumber(value); position = position + name:len() + 3 + value:len(); end end end end return results; end function trading:CreateMarketOrderSuccessResult(msg) local res = {}; if msg ~= nil then res.Finished = false; else res.Finished = true; end res.RequestID = msg; function res:GetTrade() if self._trade == nil then self._trade = core.host:findTable("trades"):find("OpenOrderReqID", self.RequestID); if self._trade == nil then return nil; end end if not self._trade:refresh() then return nil; end return self._trade; end function res:GetClosedTrade() if self._closed_trade == nil then self._closed_trade = core.host:findTable("closed trades"):find("OpenOrderReqID", self.RequestID); if self._closed_trade == nil then return nil; end end if not self._closed_trade:refresh() then return nil; end return self._closed_trade; end function res:ToJSON() local json = {}; function json:AddStr(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":\"%s\"", separator, tostring(name), tostring(value)); end function json:AddNumber(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%f", separator, tostring(name), value or 0); end function json:AddBool(name, value) local separator = ""; if self.str ~= nil then separator = ","; else self.str = ""; end self.str = self.str .. string.format("%s\"%s\":%s", separator, tostring(name), value and "true" or "false"); end function json:ToString() return "{" .. (self.str or "") .. "}"; end local first = true; for idx,t in pairs(self) do local stype = type(t) if stype == "number" then json:AddNumber(idx, t); elseif stype == "string" then json:AddStr(idx, t); elseif stype == "boolean" then json:AddBool(idx, t); elseif stype == "function" or stype == "table" then --do nothing else core.host:trace(tostring(idx) .. " " .. tostring(stype)); end end return json:ToString(); end return res; end function trading:CreateMarketOrderFailResult(message) local res = {}; res.Finished = true; res.Success = false; res.Error = message; function res:GetTrade() return nil; end return res; end function trading:MarketOrder(instrument) local builder = {}; local offer = core.host:findTable("offers"):find("Instrument", instrument); builder.Instrument = instrument; builder.Parent = self; builder.valuemap = core.valuemap(); builder.valuemap.Command = "CreateOrder"; builder.valuemap.OrderType = "OM"; builder.valuemap.OfferID = offer.OfferID; builder.valuemap.AcctID = self._account; function builder:SetAccountID(accountID) self.valuemap.AcctID = accountID; return self; end function builder:SetAmount(amount) local base_size = core.host:execute("getTradingProperty", "baseUnitSize", self.Instrument, self.valuemap.AcctID); self.valuemap.Quantity = amount * base_size; return self; end function builder:SetDefaultAmount() local base_size = core.host:execute("getTradingProperty", "baseUnitSize", self.Instrument, self.Parent._account); self.valuemap.Quantity = self.Parent:calculateAmount() * base_size; return self; end function builder:SetSide(buy_sell) self.valuemap.BuySell = buy_sell; return self; end function builder:SetPipLimit(limit_type, limit) self.valuemap.PegTypeLimit = limit_type or "O"; self.valuemap.PegPriceOffsetPipsLimit = self.valuemap.BuySell == "B" and limit or -limit; return self; end function builder:SetLimit(limit) self.valuemap.RateLimit = limit; return self; end function builder:SetPipStop(stop_type, stop, trailing_stop) self.valuemap.PegTypeStop = stop_type or "O"; self.valuemap.PegPriceOffsetPipsStop = self.valuemap.BuySell == "B" and -stop or stop; self.valuemap.TrailStepStop = trailing_stop; return self; end function builder:SetStop(stop, trailing_stop) self.valuemap.RateStop = stop; self.valuemap.TrailStepStop = trailing_stop; return self; end function builder:SetCustomID(custom_id) self.valuemap.CustomID = custom_id; return self; end function builder:GetValueMap() return self.valuemap; end function builder:AddMetadata(id, val) if self._metadata == nil then self._metadata = {}; end self._metadata[id] = val; return self; end function builder:Execute() self.Parent:trace(string.format("Creating %s OM for %s", self.valuemap.BuySell, self.Instrument)); if self._metadata ~= nil then self._metadata.CustomID = self.valuemap.CustomID; self.valuemap.CustomID = trading:ObjectToJson(self._metadata); end for _, module in pairs(self.Parent._all_modules) do if module.BlockOrder ~= nil and module:BlockOrder(self.valuemap) then self.Parent:trace("Creation of order blocked by " .. module.Name); return trading:CreateMarketOrderFailResult("Creation of order blocked by " .. module.Name); end end for _, module in pairs(self.Parent._all_modules) do if module.OnOrder ~= nil then module:OnOrder(self.valuemap); end end local id = self.Parent:getId(); local success, msg = terminal:execute(id, self.valuemap); if not(success) then local message = "Open order failed: " .. msg; self.Parent:trace(message); if self.Parent._signaler ~= nil then self.Parent._signaler:Signal(message); end return trading:CreateMarketOrderFailResult(message); end local res = trading:CreateMarketOrderSuccessResult(msg); self.Parent._waiting_requests[id] = res; return res; end return builder; end function trading:JsonToObject(json) local position = 1; local result; local results; while (position < json:len() + 1) do local ch = string.sub(json, position, position); if ch == "{" then result = {}; position = position + 1; elseif ch == "}" then if results ~= nil then position = position + 1; results[#results + 1] = result; else return result; end elseif ch == "," then position = position + 1; elseif ch == "[" then position = position + 1; results = {}; elseif ch == "]" then return results; else if result == nil then return nil; end local name, value = string.match(json, '"([^"]+)":("?[^,}]+"?)', position); if value == "false" then result[name] = false; position = position + name:len() + 8; elseif value == "true" then result[name] = true; position = position + name:len() + 7; else if string.sub(value, 1, 1) == "\"" then result[name] = value; value:sub(2, value:len() - 1); position = position + name:len() + 3 + value:len(); else result[name] = tonumber(value); position = position + name:len() + 3 + value:len(); end end end end return nil; end function trading:GetMetadata(qtxt) if qtxt == "" then return nil; end local position = 1; local result; while (position < qtxt:len() + 1) do local ch = string.sub(qtxt, position, position); if ch == "{" then result = {}; position = position + 1; elseif ch == "}" then return result; elseif ch == "," then position = position + 1; else if result == nil then return nil; end local name, value = string.match(qtxt, '"([^"]+)":("?[^,}]+"?)', position); if value == "false" then result[name] = false; position = position + name:len() + 8; elseif value == "true" then result[name] = true; position = position + name:len() + 7; else if string.sub(value, 1, 1) == "\"" then result[name] = value; value:sub(2, value:len() - 1); position = position + name:len() + 3 + value:len(); else result[name] = tonumber(value); position = position + name:len() + 3 + value:len(); end end end end return nil; end function trading:GetTradeMetadata(trade) return self:GetMetadata(trade.QTXT); end trading:RegisterModule(Modules);