//+------------------------------------------------------------------+ //| SwapWindow.mqh | //| Intra-week CFD operations: Friday liquidation + triple-swap gate| //+------------------------------------------------------------------+ //| THIS MODULE FIXES TWO DEFECTS IN THE EXISTING SCHEDULED CLOSE | //| (CExpertCustom::OnTick, ExpertCustom.mqh ~line 878). | //| | //| DEFECT 1 - THE CLOSE IS A +-1 MINUTE TICK LOTTERY. | //| The current test is | //| MathAbs(nowMinOfDay - targetMinOfDay) <= 1 | //| evaluated inside OnTick. It fires only if a TICK ARRIVES in that | //| two-minute window. On a Friday CFD close - thin book, and on | //| indices the session shutting - that is exactly when ticks stop. | //| Miss the window and the position is carried over the weekend, | //| which is the one outcome the rule exists to prevent. This is the | //| same class of failure already recorded for the session-aware bar | //| clock: work scheduled on a tick that never comes. | //| FIX: a LATCH. Fire on the first tick at-or-after the deadline, | //| once per week, and keep firing until flat rather than assuming | //| one attempt succeeded. | //| | //| DEFECT 2 - BROKER SERVER TIME IS NOT NEW YORK TIME. | //| TimeCurrent() is broker server time (commonly EET/GMT+2/+3). The | //| triple-swap rollover is defined at 17:00 NEW YORK. EU and US DST | //| switch on DIFFERENT dates, so the offset between the two is not | //| constant - there are two ~3-week windows each year where any | //| hardcoded server-hour constant is off by one hour. Deriving from | //| TimeGMT() and applying US DST rules is offset-stable. | //| | //| AND ONE ASSUMPTION NOT TO MAKE: the triple-swap day is NOT always | //| Wednesday. It is per-symbol and the broker publishes it as | //| SYMBOL_SWAP_ROLLOVER3DAYS. Several index and metal CFDs bill on | //| Friday. Read it; never hardcode Wednesday. | //+------------------------------------------------------------------+ #ifndef SWAPWINDOW_MQH #define SWAPWINDOW_MQH //--- 17:00 New York, the standard CFD rollover instant #define SW_ROLLOVER_HOUR_NY 17 #define SW_SECONDS_PER_HOUR 3600 //+------------------------------------------------------------------+ //| US DST: second Sunday of March 02:00 -> first Sunday of November. | //| Returns the UTC offset in seconds (-5h EST / -4h EDT). | //| | //| Computed from the date, not from TimeGMTOffset(): in the strategy | //| tester the terminal's own offset reporting follows the BROKER, so | //| using it here would reintroduce defect 2. | //+------------------------------------------------------------------+ int SWNewYorkUtcOffset(const datetime utc) { MqlDateTime t; TimeToStruct(utc, t); //--- day_of_week of the 1st of the month, from the given date int dowFirst = (int)(t.day_of_week - ((t.day - 1) % 7) + 7) % 7; //--- second Sunday of March int firstSun = 1 + ((7 - dowFirst) % 7); if(t.mon == 3) { int secondSun = firstSun + 7; if(t.day > secondSun || (t.day == secondSun && t.hour >= 7)) // 02:00 EST = 07:00 UTC return -4 * SW_SECONDS_PER_HOUR; return -5 * SW_SECONDS_PER_HOUR; } if(t.mon == 11) { if(t.day > firstSun || (t.day == firstSun && t.hour >= 6)) // 02:00 EDT = 06:00 UTC return -5 * SW_SECONDS_PER_HOUR; return -4 * SW_SECONDS_PER_HOUR; } if(t.mon > 3 && t.mon < 11) return -4 * SW_SECONDS_PER_HOUR; return -5 * SW_SECONDS_PER_HOUR; } //--- New York wall-clock time for a given UTC instant. datetime SWNewYorkTime(const datetime utc) { return utc + SWNewYorkUtcOffset(utc); } //+------------------------------------------------------------------+ //| The symbol's OWN triple-swap day, as the broker publishes it. | //| Falls back to Wednesday only when the symbol reports nothing. | //+------------------------------------------------------------------+ ENUM_DAY_OF_WEEK SWTripleSwapDay(const string symbol) { long day = 0; if(!SymbolInfoInteger(symbol, SYMBOL_SWAP_ROLLOVER3DAYS, day)) return WEDNESDAY; if(day < 0 || day > 6) return WEDNESDAY; return (ENUM_DAY_OF_WEEK)day; } //+------------------------------------------------------------------+ //| Are we inside the `windowMinutes` run-up to this symbol's triple- | //| swap rollover? | //+------------------------------------------------------------------+ bool SWInTripleSwapWindow(const string symbol, const int windowMinutes = 120) { datetime utc = TimeGMT(); datetime ny = SWNewYorkTime(utc); MqlDateTime n; TimeToStruct(ny, n); if((ENUM_DAY_OF_WEEK)n.day_of_week != SWTripleSwapDay(symbol)) return false; int nowMin = n.hour * 60 + n.min; int rolloverMin = SW_ROLLOVER_HOUR_NY * 60; return (nowMin >= rolloverMin - windowMinutes && nowMin < rolloverMin); } //+------------------------------------------------------------------+ //| SWAP DRAG, in ACCOUNT CURRENCY, for holding `lots` across the | //| triple-swap rollover. | //| | //| Returns false when the symbol's swap mode is one this cannot price | //| exactly. A false here must BLOCK the override (fall back to the | //| plain volatility gate) - never be treated as "drag = 0", which | //| would silently wave through the trades this rule exists to stop. | //+------------------------------------------------------------------+ bool SWTripleSwapCost(const string symbol, const bool isLong, const double lots, double &cost) { double rate = isLong ? SymbolInfoDouble(symbol, SYMBOL_SWAP_LONG) : SymbolInfoDouble(symbol, SYMBOL_SWAP_SHORT); if(!MathIsValidNumber(rate) || lots <= 0.0) return false; long mode = SymbolInfoInteger(symbol, SYMBOL_SWAP_MODE); double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double point = SymbolInfoDouble(symbol, SYMBOL_POINT); double price = SymbolInfoDouble(symbol, SYMBOL_BID); double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE); double oneDay = 0.0; switch((int)mode) { case SYMBOL_SWAP_MODE_POINTS: if(tickSize <= 0.0 || tickValue <= 0.0 || point <= 0.0) return false; oneDay = rate * point * (tickValue / tickSize) * lots; break; case SYMBOL_SWAP_MODE_CURRENCY_SYMBOL: case SYMBOL_SWAP_MODE_CURRENCY_MARGIN: case SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT: //--- already money per lot; currency conversion for the first two is //--- the broker's, and assuming parity here would be a fabrication. oneDay = rate * lots; break; case SYMBOL_SWAP_MODE_INTEREST_CURRENT: case SYMBOL_SWAP_MODE_INTEREST_OPEN: if(price <= 0.0 || contract <= 0.0) return false; oneDay = (rate / 100.0) * price * contract * lots / 360.0; break; default: return false; // unpriceable - caller must not override } //--- Triple charge. Sign convention is the broker's: a POSITIVE swap is a //--- credit, so drag is the negative part. Report drag as a positive cost. cost = -3.0 * oneDay; return true; } //+------------------------------------------------------------------+ //| THE GATE. Should this trade be blocked ahead of the triple swap? | //| | //| Blocks unless the expected expansion pays for the carry by a real | //| margin. `expectedMoveAtr` is the meta-label's forecast expansion | //| in ATR units; `atrPrice` is ATR in price units. | //+------------------------------------------------------------------+ bool SWBlockedByTripleSwap(const string symbol, const bool isLong, const double lots, const double expectedMoveAtr, const double atrPrice, const double overrideMultiple, string &why, const int windowMinutes = 120) { if(!SWInTripleSwapWindow(symbol, windowMinutes)) return false; double drag = 0.0; if(!SWTripleSwapCost(symbol, isLong, lots, drag)) { why = "triple-swap window and swap cost is unpriceable on this symbol - blocked"; return true; // refuse, never assume zero } if(drag <= 0.0) { why = "triple-swap window but the carry is a credit - allowed"; return false; } double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if(tickSize <= 0.0 || tickValue <= 0.0 || atrPrice <= 0.0 || expectedMoveAtr <= 0.0) { why = "triple-swap window and the expected move cannot be valued - blocked"; return true; } double expectedValue = expectedMoveAtr * atrPrice * (tickValue / tickSize) * lots; if(expectedValue >= overrideMultiple * drag) { why = StringFormat("triple-swap override: expected %.2f >= %.1f x drag %.2f", expectedValue, overrideMultiple, drag); return false; } why = StringFormat("blocked: expected %.2f < %.1f x triple-swap drag %.2f", expectedValue, overrideMultiple, drag); return true; } //+------------------------------------------------------------------+ //| CWeeklyFlatLatch - the Friday liquidation, as a latch. | //| | //| Deliberately NOT a +-N minute window. Once the deadline passes the | //| latch stays armed until the book is actually flat, so a missing | //| tick delays the close instead of cancelling it. | //+------------------------------------------------------------------+ class CWeeklyFlatLatch { private: int m_armedWeek; // year*100 + ISO week, -1 = idle int m_attempts; int WeekKey(const datetime t) const { MqlDateTime d; TimeToStruct(t, d); return d.year * 100 + (d.day_of_year / 7); } public: CWeeklyFlatLatch(void) : m_armedWeek(-1), m_attempts(0) { } //+---------------------------------------------------------------+ //| True when the weekly flat deadline has passed and we are not | //| yet flat. Call from OnTick AND OnTimer - the timer is what | //| covers the case ticks have stopped entirely. | //| | //| `deadlineNy` is minutes-of-day in NEW YORK time on `flatDay`. | //+---------------------------------------------------------------+ bool ShouldLiquidate(const ENUM_DAY_OF_WEEK flatDay, const int deadlineNyMin) { datetime utc = TimeGMT(); datetime ny = SWNewYorkTime(utc); MqlDateTime n; TimeToStruct(ny, n); int key = WeekKey(ny); if(m_armedWeek == key) return true; // still armed: last attempt left something open if((ENUM_DAY_OF_WEEK)n.day_of_week != flatDay) return false; int nowMin = n.hour * 60 + n.min; //--- AT OR AFTER, not "within a minute of". if(nowMin < deadlineNyMin) return false; m_armedWeek = key; m_attempts = 0; return true; } //--- Called after a liquidation attempt; disarm only once truly flat. void ReportResult(const bool flat) { m_attempts++; if(flat) { m_armedWeek = -1; m_attempts = 0; } else if(m_attempts % 20 == 0) Print(__FUNCTION__ + ": weekly flat latch still armed after " + IntegerToString(m_attempts) + " attempts - positions remain open"); } bool Armed(void) const { return m_armedWeek != -1; } }; #endif // SWAPWINDOW_MQH