282 行
12 KiB
MQL5
282 行
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
|