| //+------------------------------------------------------------------+
|
| //| 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
|