forked from animatedread/Warrior_EA
BLUEPRINT.md reviews the feature/label layers and designs fractional differencing, Garman-Klass / Yang-Zhang targets and a 48-72h expansion label; mql5_patches/ holds the MQL5 side (FFD safe past the 1024-bar series ceiling, vol estimators, the label + veto gate, NY-time swap window). premise_test.py measured the premise on real broker bars: the headline AUC 0.75 was a day-of-week / path-length artifact (a Friday-clipped path is shorter, so it touches K*ATR less). Honest residual 0.56-0.62, mostly within noise once overlap is deflated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
282 lines
12 KiB
MQL5
282 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|