I have created a simple Strategy which buys when the MA moves up and sells when the MA moves down using FX Strategy Wizard (multiple trades are not allowed).
The problem I have is that emails are sent at the end of each period as the activation point is MA up or MA down.
I would prefer the email to only be sent when a new buy or sell trade occurs ie at the beginning of an MA up or MA down sequence.
I cannot find information relating directly to this on the forum and I am unable to transfer this type of behaviour from other strategies found here to my own.
Please advise where I can find information to correct this or feel free to alter the code to help me achieve this.
Many thanks in advance for any information or help you provide.
Robbie
- Code: Select all
-----------------------------------------------------------
-- Buy when MA moves 1 Up
-- Author: Robbie Young
-----------------------------------------------------------
local _gSubscription = {};
local _gUpdatePeriods = {};
local _gLastTime;
-----------------------------------------------------------
-- Standard strategy init handler for marketscope strategy
-----------------------------------------------------------
function Init()
strategy:name("1U");
strategy:description("Buy when MA moves 1 Up");
strategy:type(core.Both);
strategy.parameters:addString("Account", "Account", "", "");
strategy.parameters:setFlag("Account", core.FLAG_ACCOUNT);
strategy.parameters:addBoolean("AllowTrade", "Allow trade", "", true);
strategy.parameters:setFlag("AllowTrade", core.FLAG_ALLOW_TRADE);
strategy.parameters:addString("AllowedSide", "Allowed side", "Allowed side for trading or signaling, can be Sell, Buy or Both", "Both");
strategy.parameters:addStringAlternative("AllowedSide", "Both", "", "Both");
strategy.parameters:addStringAlternative("AllowedSide", "Buy", "", "Buy");
strategy.parameters:addStringAlternative("AllowedSide", "Sell", "", "Sell");
strategy.parameters:addBoolean("AllowMultiplePositions", "Allow multiple positions", "", false);
strategy.parameters:addString("tf", "tf", "The used timeframe", "H4");
strategy.parameters:setFlag("tf", core.FLAG_PERIODS);
strategy.parameters:addGroup("MovingAverage parameters")
strategy.parameters:addInteger("MovingAverage_N", "Number of periods", "The number of periods.", 100, 1, 10000);
strategy.parameters:addGroup("Notification")
strategy.parameters:addBoolean("SENDEMAIL", "Send Email", "", false);
strategy.parameters:addString("EMAIL", "Email address", "Note that to receive e-mails, SMTP settings must be defined (see Signals Options).", "");
strategy.parameters:setFlag("EMAIL", core.FLAG_EMAIL);
end
local mCID= "FXSW_STRATEGY";
local mAccount;
local mLotSize;
local mAllowTrade = false;
local mAllowedSide = "Both";
local mPlaySound = false;
local mReccurentSound = false;
local mSendEmail = false;
local mShowAlert = false;
local mEmail;
local mAllowMultiplePositions = true;
local tf;
local symbol;
local ClosingBid;
local id_ClosingBid = 101;
local MovingAverage_N;
local MovingAverage;
local MAUp = false;
local MADown = false;
-----------------------------------------------------------
-- Standard prepare handler for marketscope strategy
-----------------------------------------------------------
function Prepare(onlyName)
-- collect parameters
mAccount = instance.parameters.Account;
mLotSize = core.host:execute("getTradingProperty", "baseUnitSize", instance.bid:instrument(), mAccount);
mAllowTrade = instance.parameters.AllowTrade;
mAllowedSide = instance.parameters.AllowedSide;
mAllowMultiplePositions = instance.parameters.AllowMultiplePositions;
tf = instance.parameters.tf;
symbol = instance.bid:instrument();
MovingAverage_N = instance.parameters.MovingAverage_N;
mSendEmail = instance.parameters.SENDEMAIL;
mEmail = instance.parameters.EMAIL;
--set name
instance:name(profile:id() .. "(" .. instance.bid:instrument() .. "(" .. tf .. "))");
if onlyName then
return;
end
--datasources
ClosingBid = ExtSubscribe(id_ClosingBid, symbol, tf, true, "close");
--indicators
MovingAverage = core.indicators:create("MVA", ClosingBid, MovingAverage_N, 65535, 1, 1);
_gUpdatePeriods[MovingAverage.DATA] = _gUpdatePeriods[ClosingBid];
end
-----------------------------------------------------------
-- 'Event handler' that is called when a datasource is updated
-----------------------------------------------------------
function ExtUpdate(id, updatedSource, period)
if not checkReady("trades") or not checkReady("summary") then
return;
end
-- update indicators values
MovingAverage:update(core.UpdateLast);
-- update expressions
if id == id_ClosingBid then --Updates handler of 'ClosingBid' datasource ('tf' timeframe)
--Check that all data of used datasources is available
if canCalculate( MovingAverage.DATA , getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 1)) and canCalculate( MovingAverage.DATA , getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 2)) then
MAUp = MovingAverage.DATA[getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 1)] > MovingAverage.DATA[getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 2)];
end
--Check that all data of used datasources is available
if canCalculate( MovingAverage.DATA , getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 1)) and canCalculate( MovingAverage.DATA , getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 2)) then
MADown = MovingAverage.DATA[getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 1)] < MovingAverage.DATA[getClosedPeriod(MovingAverage.DATA, MovingAverage.DATA:size() - 1 - 2)];
end
end
-- processing of Activation points
if id==id_ClosingBid then --Updates handler of 'ClosingBid' datasource ('tf' timeframe)
--'Buy' activation point logic
if MAUp then
if mAllowTrade then close("S", core.host:findTable("offers"):find("Instrument", symbol).OfferID); end
if mAllowTrade then createTrueMarketOrder("B", 10, symbol, 0, false, 0); end
if mSendEmail then terminal:alertEmail(mEmail, "EURUSD Bought", "\nEURUSD Bought"); end
end
--'Sell' activation point logic
if MADown then
if mAllowTrade then close("B", core.host:findTable("offers"):find("Instrument", symbol).OfferID); end
if mAllowTrade then createTrueMarketOrder("S", 10, symbol, 0, false, 0); end
if mSendEmail then terminal:alertEmail(mEmail, "EURUSD Sold", "\nEURUSD Sold"); end
end
end
end
function checkReady(tableName)
return core.host:execute("isTableFilled", tableName);
end
-----------------------------------------------------------
--Enters to the market
-- side: B - BUY or S - SELL
-- amount: order amount
-- instrumentName: instrument of order
-- stop: 0, 1 or greater value
-- isTrailingStop: true/false
-- limit: 0, 1 or greater value
-----------------------------------------------------------
function createTrueMarketOrder(side, amount, instrumentName, stop, isTrailingStop, limit)
if not mAllowTrade then
return;
end
local offerId = core.host:findTable("offers"):find("Instrument", instrumentName).OfferID;
if not (mAllowedSide == "Both" or (mAllowedSide == "Buy" and side == "B") or (mAllowedSide == "Sell" and side == "S")) then
return;
end
if not mAllowMultiplePositions then
if (side == 'B' and countLongPositions(instrumentName) > 0) then
return;
elseif (side == 'S' and countShortPositions(instrumentName) > 0) then
return;
end
end
local valuemap;
valuemap = core.valuemap();
valuemap.Command = "CreateOrder";
valuemap.OrderType = "OM";
valuemap.OfferID = offerId;
valuemap.AcctID = mAccount;
valuemap.GTC = "FOK"; --Fill or Kill order to avoid partial execution
valuemap.Quantity = amount * mLotSize;
valuemap.BuySell = side;
valuemap.CustomID = mCID;
if stop >= 1 then
valuemap.PegTypeStop = "M";
if side == "B" then
valuemap.PegPriceOffsetPipsStop = - stop;
else
valuemap.PegPriceOffsetPipsStop = stop;
end
if isTrailingStop then
valuemap.TrailStepStop = 1;
end
end
if limit >= 1 then
valuemap.PegTypeLimit = "M";
if side == "B" then
valuemap.PegPriceOffsetPipsLimit = limit;
else
valuemap.PegPriceOffsetPipsLimit = -limit;
end
end
if (not canClose(instrumentName)) and (stop >= 1 or limit >= 1) then
valuemap.EntryLimitStop = 'Y'
end
success, msg = terminal:execute(200, valuemap);
assert(success, msg);
end
-----------------------------------------------------------
-- closes all positions of the specified direction (B for buy, S for sell)
-----------------------------------------------------------
function canClose(instrumentName)
return core.host:execute("getTradingProperty", "canCreateMarketClose", instrumentName, mAccount);
end
function close(side, offer)
local enum, row, valuemap;
enum = core.host:findTable("trades"):enumerator();
while true do
row = enum:next();
if row == nil then
break;
end
if row.AccountID == mAccount and
row.OfferID == offer and
row.BS == side and
row.QTXT == mCID then
-- if trade has to be closed
if canClose(row.Instrument) then
-- create a close market order when hedging is allowed
valuemap = core.valuemap();
valuemap.OrderType = "CM";
valuemap.OfferID = offer;
valuemap.AcctID = mAccount;
valuemap.Quantity = row.Lot;
valuemap.TradeID = row.TradeID;
valuemap.CustomID = mCID;
if row.BS == "B" then
valuemap.BuySell = "S";
else
valuemap.BuySell = "B";
end
success, msg = terminal:execute(200, valuemap);
assert(success, msg);
else
-- create an opposite market order when FIFO
valuemap = core.valuemap();
valuemap.OrderType = "OM";
valuemap.OfferID = offer;
valuemap.AcctID = mAccount;
valuemap.Quantity = row.Lot;
valuemap.CustomID = mCID;
if row.BS == "B" then
valuemap.BuySell = "S";
else
valuemap.BuySell = "B";
end
success, msg = terminal:execute(200, valuemap);
assert(success, msg);
end
end
end
end
-----------------------------------------------------------
--Handle command execution result
-----------------------------------------------------------
function ExtAsyncOperationFinished(cookie, success, message)
if cookie == 1 then
loaded = true;
elseif cookie == 200 then
assert(success, message);
end
end
-----------------------------------------------------------
-- Helper functions
-----------------------------------------------------------
function getOppositeSide(side)
if(side == "B") then
return "S";
else
return "B";
end
end
function getDSPeriod(ds, updatedSource, period)
local p;
if ds:isBar() then
p = core.findDate(ds.open, updatedSource:date(period), false);
else
p = core.findDate(ds, updatedSource:date(period), false);
end
if (p > ds:size() - 1) then
p = ds:size() - 1;
elseif (p < ds:first()) then
p = ds:first();
end
return p;
end
-----------------------------------------------------------
-- Allow to calculate last closed bar period for ds datasource
-- which has not tick frequency updates
-----------------------------------------------------------
function getClosedPeriod(ds, supposedPeriod)
--Check if datasource lastdate is closed on updatePeriod or shift supposedPeriod to -1
if _gUpdatePeriods[ds] == 't1' or _gUpdatePeriods[ds] == nil then
return supposedPeriod;
else
return supposedPeriod - 1;
end
end
-----------------------------------------------------------
--Helper functions to wrap the table's method call into the simple function call
-----------------------------------------------------------
function streamSize(stream)
return stream:size();
end
function streamHasData(stream, period)
return stream:hasData(period);
end
function canCalculate(stream, period)
return (period >= 0) and (period > stream:first()) and streamHasData(stream, period);
end
-----------------------------------------------------------
-- Helper functions to be sure that you work with a tick stream
-----------------------------------------------------------
function getTickStreamOfPriceType(stream, priceType)
if stream:isBar() then
if priceType == "open" then
return stream.open;
elseif priceType == "high" then
return stream.high;
elseif priceType == "low" then
return stream.low;
elseif priceType == "close" then
return stream.close;
elseif priceType == "typical" then
return stream.typical;
elseif priceType == "weighted" then
return stream.weighted;
elseif priceType == "volume" then
return stream.volume;
else
return stream.close;
end
else
return stream;
end
end
function selectStream(safeStream, subStream)
if safeStream:isBar() then
return subStream;
else
return safeStream;
end
end
---------------------------------------------------------
-- Subscription for updates by datasource timeframe
---------------------------------------------------------
-- subscribe for the price data
function ExtSubscribe(id, instrument, period, bid, type)
local sub = {};
if instrument == nil and period == "t1" then
if bid then
sub.stream = instance.bid;
else
sub.stream = instance.ask;
end
sub.tick = true;
sub.loaded = true;
sub.lastSerial = -1;
_gSubscription[id] = sub;
elseif instrument == nil then
sub.stream = core.host:execute("getHistory", id, instance.bid:instrument(), period, 0, 0, bid);
sub.tick = false;
sub.loaded = false;
sub.lastSerial = -1;
_gSubscription[id] = sub;
else
sub.stream = core.host:execute("getHistory", id, instrument, period, 0, 0, bid);
sub.tick = (period == "t1");
sub.loaded = false;
sub.lastSerial = -1;
_gSubscription[id] = sub;
end
_gUpdatePeriods[sub.stream] = period;
if sub.tick then
return sub.stream;
else
if type == "open" then
_gUpdatePeriods[sub.stream.open] = period;
return sub.stream.open;
elseif type == "high" then
_gUpdatePeriods[sub.stream.high] = period;
return sub.stream.high;
elseif type == "low" then
_gUpdatePeriods[sub.stream.low] = period;
return sub.stream.low;
elseif type == "close" then
_gUpdatePeriods[sub.stream.close] = period;
return sub.stream.close;
elseif type == "bar" then
_gUpdatePeriods[sub.stream.open] = period;
_gUpdatePeriods[sub.stream.high] = period;
_gUpdatePeriods[sub.stream.low] = period;
_gUpdatePeriods[sub.stream.close] = period;
_gUpdatePeriods[sub.stream.median] = period;
_gUpdatePeriods[sub.stream.typical] = period;
_gUpdatePeriods[sub.stream.volume] = period;
_gUpdatePeriods[sub.stream.weighted] = period;
return sub.stream;
else
assert(false, type .. " is unknown");
end
end
end
function AsyncOperationFinished(cookie, success, message)
local sub;
sub = _gSubscription[cookie];
if sub ~= nil then
sub.loaded = true;
if sub.stream:size() > 1 then
sub.lastSerial = sub.stream:serial(sub.stream:size() - 1);
end
else
-- unknown cookie
if ExtAsyncOperationFinished ~= nil then
ExtAsyncOperationFinished(cookie, success, message)
end
end
end
function Update()
if instance.bid:size() > 0 then
_gLastTime = instance.bid:date(instance.bid:size() - 1);
end
for k, v in pairs(_gSubscription) do
if v.loaded and v.stream:size() > 1 then
local s = v.stream:serial(v.stream:size() - 1);
local p;
if s ~= v.lastSerial then
if v.tick then
p = v.stream:size() - 1; -- the last tick
else
p = v.stream:size() - 2; -- the previous candle
end
ExtUpdate(k, v.stream, p);
v.lastSerial = s;
end
end
end
end
---------------------------------------------------------
-- Additional functions
---------------------------------------------------------
local mAccountRow = nil;
local mSummaries = {};
function checkAccountRow()
if mAccountRow == nil then
mAccountRow = core.host:findTable("accounts"):find("AccountID", mAccount);
else
mAccountRow:refresh();
end
end
function checkSummaryRow(sInstrument)
local sOfferID, summaryIter, summaryRow;
if mSummaries[sInstrument] ~= nil then
--try refresh
if not (mSummaries[sInstrument]:refresh()) then
mSummaries[sInstrument] = nil;
end
end
--re-read all cache
if mSummaries[sInstrument] == nil then
sOfferID = core.host:findTable("offers"):find("Instrument", sInstrument).OfferID;
summaryIter = core.host:findTable("summary"):enumerator();
summaryRow = summaryIter:next();
while summaryRow ~= nil do
if summaryRow.OfferID == sOfferID then
mSummaries[sInstrument] = summaryRow;
break;
end
summaryRow = summaryIter:next();
end
end
end
function getEquity()
checkAccountRow();
return mAccountRow.Equity;
end
function getBalance()
checkAccountRow();
return mAccountRow.Balance;
end
function getProfit()
checkAccountRow();
return mAccountRow.Equity - mAccountRow.Balance;
end
function getAmountK(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.AmountK;
end
end
function getGrossPL(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.GrossPL;
end
end
function getNetPL(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.NetPL;
end
end
function getSellAmountK(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.SellAmountK;
end
end
function getBuyAmountK(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.BuyAmountK;
end
end
function getBuyNetPLPip(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.BuyNetPLPip;
end
end
function getSellNetPLPip(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.SellNetPLPip;
end
end
function getBuyNetPL(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.BuyNetPL;
end
end
function getSellNetPL(sInstrument)
local res;
checkSummaryRow(sInstrument);
res = mSummaries[sInstrument];
if res == nil then
return 0;
else
return res.SellNetPL;
end
end
function countPositions(sInstrument)
local tradesIter, tradeRow, count, sOfferID;
count = 0;
if sInstrument ~= nil then
sOfferID = core.host:findTable("offers"):find("Instrument", sInstrument).OfferID;
end
tradesIter = core.host:findTable("trades"):enumerator();
tradeRow = tradesIter:next();
while tradeRow ~= nil do
if (sInstrument == nil or tradeRow.OfferID == sOfferID) then
count = count + 1;
end
tradeRow = tradesIter:next();
end
return count;
end
function countLongPositions(sInstrument)
local tradesIter, tradeRow, count, sOfferID;
count = 0;
if sInstrument ~= nil then
sOfferID = core.host:findTable("offers"):find("Instrument", sInstrument).OfferID;
end
tradesIter = core.host:findTable("trades"):enumerator();
tradeRow = tradesIter:next();
while tradeRow ~= nil do
if ((sInstrument==nil or tradeRow.OfferID == sOfferID) and tradeRow.BS == "B") then
count = count + 1;
end
tradeRow = tradesIter:next();
end
return count;
end
function countShortPositions(sInstrument)
local tradesIter, tradeRow, count, sOfferID;
count = 0;
if sInstrument ~= nil then
sOfferID = core.host:findTable("offers"):find("Instrument", sInstrument).OfferID;
end
tradesIter = core.host:findTable("trades"):enumerator();
tradeRow = tradesIter:next();
while tradeRow ~= nil do
if ((sInstrument == nil or tradeRow.OfferID == sOfferID) and tradeRow.BS == "S") then
count = count + 1;
end
tradeRow = tradesIter:next();
end
return count;
end
function getLastUpdateTime()
if (_gLastTime == nil) then
return 0;
else
return _gLastTime;
end
end
function time(hours, minutes, seconds)
local dtLast;
dtLast = core.dateToTable(_gLastTime);
if seconds == nil then
seconds = 0;
end
return core.datetime(dtLast.year, dtLast.month, dtLast.day, hours, minutes, seconds);
end
function isValidDate(checkDate)
if (checkDate < 1) then
return false;
else
return true;
end
end
function parseTime(sTime)
local iDelimHMPos = string.find(sTime, ":");
local h = tonumber(string.sub(sTime, 1, iDelimHMPos - 1));
local sTimeTile = string.sub(sTime, iDelimHMPos + 1);
local iDelimMSPos = string.find(sTimeTile, ":");
local m, s;
s = 0;
if iDelimMSPos == nil then
m = tonumber(sTimeTile);
else
m = tonumber(string.sub(sTimeTile, 1, iDelimMSPos - 1));
s = tonumber(string.sub(sTimeTile, iDelimMSPos + 1));
end
return time(h, m, s);
end
function getPipSize(sInstrument)
return core.host:findTable("offers"):find("Instrument", sInstrument).PointSize;
end