forked from mnbvc188199/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
|