Логика кэрри-трейда в MQL5: Создание советника, учитывающего ставки свопа при расчете размера позиции и принятии решений об удержании
Введение
Есть распространенное упущение, которое многие разработчики розничных торговых систем не учитывают. Они либо игнорируют это, либо используют фиксированную цифру, которая не учитывает колебания рынка должным образом, либо откладывают расчеты на неопределенный срок. Для скальперов ставки свопа не имеют особого значения, поскольку они никогда не переносят позиции через ежедневный рыночный ролловер. Для трейдеров, которые держат позиции на ночь, и особенно для стратегий, построенных на высокодоходных валютных парах (таких как AUDJPY или NZDUSD), игнорирование ставок свопа — все равно что ехать на машине, не проверяя уровень топлива.
Ставки свопа — это процентные списания или начисления за перенос позиции через ночь, которые применяются, когда трейдер удерживает позицию после ежедневного закрытия рынка. Если вы держите валюту с более высокой процентной ставкой (например, AUD) против валюты с более низкой процентной ставкой (например, JPY), вы получаете проценты (положительный своп); в противном случае вы платите комиссию, если держите валюту с более низкой процентной ставкой против валюты с более высокой процентной ставкой (отрицательный своп). Совокупные затраты или проценты, начисленные за неделю, могут определить, будет ли сделка прибыльной или нет.
Большинство советников не учитывают ставки свопа. Ставка свопа отображается в истории счета, но она не влияет ни на решение о входе в сделку, ни на логику периода удержания позиции, ни на определение размера позиции. В этой статье мы рассмотрим процесс получения данных о свопах в MQL5, их преобразования в удобный для использования дневной показатель в валюте счета и применения этих данных на практике. Мы реализуем четыре практические функции: DailySwapInAccountCurrency(), ExpectedSwapForPosition(), IsWorthHolding() и CarryAdjustedLotSize(). В совокупности они обеспечивают получение данных о свопах, прогнозирование периода удержания, принятие решений о выходе с учетом кэрри и определение размера позиции с учетом кэрри. Каждая функция представляет собой автономный модуль, который можно встроить в любой существующий советник без изменения его логики входа.
Раздел 1: Ставки свопа — что это такое и почему они важны
Механика процентов за перенос через ночь
При торговле на спотовом рынке Форекс по каждой открытой позиции, переносимой через ежедневное расчетное время (обычно 17:00 по нью-йоркскому времени), возникает списание или начисление процентов за перенос через ночь. Эти ставки рассчитываются на основе разницы между затратами на межбанковское кредитование в двух соответствующих валютах. Каждую ночь трейдеры либо получают начисление, если удерживают валюту с более высокой процентной ставкой, либо несут списание, если удерживают валюту с более низкой процентной ставкой.
Брокеры выражают эти корректировки в виде ставок свопа, которые указываются в спецификации символа в полях Swap Long и Swap Short. Что именно они означают, зависит от брокера и инструмента, и эта вариативность — одна из причин, по которым разработчики обычно предпочитают игнорировать эту тему, а не разбираться в ней. MetaTrader 5 предоставляет девять режимов свопа через SYMBOL_SWAP_MODE (ENUM_SYMBOL_SWAP_MODE). В этой статье подробно рассматриваются шесть распространенных режимов свопа у розничных брокеров, один режим оценивается приближенно, а для двух брокерских режимов повторного открытия позиций, требующих специфичных данных, возвращается значение 0.0 с предупреждением в журнале.
При использовании подходящих инструментов речь идет о значительных суммах. В настоящее время длинная позиция по AUDJPY объемом один стандартный лот приносит от 5 до 9 долларов в день в зависимости от брокера, исходя из дифференциала процентных ставок по AUDJPY. При объеме 0,1 лота длинная позиция по паре AUDJPY приносит примерно от 0,50 до 0,95 доллара в день. Если накопить его за пятидневный период удержания позиции (что дает 7 дней свопа), это покрывает от 14% до 26% риска по стоп-лоссу в 25 долларов исключительно за счет кэрри. На инструментах с более широким дифференциалом процентных ставок, таких как MXN/JPY, этот буфер становится еще более значимым.
Когда своп меняет математику сделки
Рассмотрим простой сценарий. Советник для свинг-трейдинга по AUDJPY открывает длинную позицию со стоп-лоссом в 30 пунктов и тейк-профитом в 80 пунктов. По итогам двух дней позиция находится в минусе на 15 пунктов. При стандартной логике советник может закрыть ее досрочно, если сигнал развернется или сработает условие выхода по времени. Но если по этой позиции ежедневно начисляется примерно 0,75 доллара в виде свопового начисления и она открыта уже два дня, то она уже накопила 1,50 доллара кэрри-дохода. Этот доход сокращает чистый убыток почти на 1,7 пункта (0,1 лота по AUDJPY), компенсируя около 6% общего риска. Это не такая уж большая сумма, но она приобретает значение, когда позиция приближается к уровню стоп-лосса.
Если включить это в стратегию, целенаправленно ориентированную на валютные пары с положительным кэрри и предусматривающую удержание позиций в течение трёх-семи дней, вклад свопа станет полноценной составляющей расчёта доходности. Некоторые стратегии, ориентированные на кэрри, за конкретную неделю получают больше дохода от ночного свопового начисления, чем от движения цены. Для таких стратегий встроенный в советник учет свопов — это не просто дополнительная опция, а неотъемлемая часть, благодаря которой логика становится полноценной.
Даже для стратегий, не ориентированных на кэрри, разработанная в этой статье функция принятия решения об удержании позиции имеет практическую ценность. Любой советник с временным фильтром, удерживающий позиции после ролловера, неявно занимает кэрри-позицию. Понимание того, помогает этот кэрри или вредит и насколько именно, дает советнику больше информации для принятия решения о том, оставаться в сделке или закрыть её.
Среда тройного свопа: функции, описанные в этой статье, учитывают этот фактор, подсчитывая количество ролловеров по средам в течение периода удержания и добавляя по два дополнительных дня свопа за каждый такой ролловер. Это широко применимая эвристика для валютных пар у большинства розничных брокеров. Обратите внимание, что для некоторых инструментов или брокеров день тройного свопа может приходиться на другой день недели, а MetaTrader 5 предоставляет дневные множители (от SYMBOL_SWAP_SUNDAY до SYMBOL_SWAP_SATURDAY), которые при необходимости можно использовать для более точного расчета.
Раздел 2: Чтение данных о свопах в MQL5
Три ключевых свойства символа
MQL5 предоставляет информацию о свопах через три свойства символа: SYMBOL_SWAP_LONG возвращает значение свопа, применяемое к длинным позициям, SYMBOL_SWAP_SHORT — значение для коротких позиций (оба значения имеют тип double и могут быть как положительными, так и отрицательными: положительное значение означает, что трейдер получает начисление; отрицательное — что происходит списание), а SYMBOL_SWAP_MODE возвращает целое число, указывающее, в каких единицах измерения выражены эти значения.
Получить исходные значения несложно. Сложность заключается именно в пересчете этих значений в фактическую сумму в валюте счета за лот в день, поскольку расчет зависит от того, какой режим свопа брокер использует для данного инструмента.
В таблице 1 ниже кратко представлены режимы свопа, доступные в MetaTrader 5, а также формула, необходимая для каждого из них.
| SYMBOL_SWAP_MODE | Значение | Единица свопа | Типовая оценка | Поддержка в коде |
|---|---|---|---|---|
| SYMBOL_SWAP_MODE_DISABLED | 0 | Своп не начисляется и не списывается | — | Полная поддержка — корректно возвращает 0.0 |
| SYMBOL_SWAP_MODE_POINTS | 1 | Пункты на лот в день | swapPts × point × (tickValue / tickSize) | полная поддержка |
| SYMBOL_SWAP_MODE_CURRENCY_SYMBOL | 2 | Базовая валюта на лот в день | swapVal × lots (при необходимости конвертировать в валюту счета) | Частичная поддержка — без конвертации валюты |
| SYMBOL_SWAP_MODE_INTEREST_CURRENT | 3 | % от текущей цены в день | цена Bid × contractSize × swapPct / 100 / 360 | Полная поддержка (использует текущую цену Bid как приближение цены) |
| SYMBOL_SWAP_MODE_CURRENCY_MARGIN | 4 | Валюта маржи на лот | swapVal × lots | Не реализовано — возвращает 0.0 |
| SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT | 5 | Валюта депозита на лот | swapVal × количество лотов (уже в валюте депозита) | полная поддержка |
| SYMBOL_SWAP_MODE_INTEREST_OPEN | 6 | % от цены открытия в день | openPrice × contractSize × swapPct / 100 / 360 | Приближенно — используется цена Bid, а не цена открытия |
| SYMBOL_SWAP_MODE_REOPEN_CURRENT | 7 | Переоткрытие по цене закрытия | Зависит от брокера | Не реализовано — возвращает 0.0 |
| SYMBOL_SWAP_MODE_REOPEN_BID | 8 | Переоткрытие по цене Bid | Зависит от брокера | Не реализовано — возвращает 0.0 |
Таблица 1: значения перечисления SYMBOL_SWAP_MODE и уровень их поддержки в реализации, описанной в данной статье. Приведенные здесь целочисленные значения соответствуют порядку, указанному в документации MQL5. Всегда сверяйте режим, который фактически использует ваш брокер, с окном спецификации символа в MetaTrader 5, поскольку настройки брокеров могут различаться.
На режимы с 0 по 2 приходится подавляющее большинство инструментов у розничных брокеров MetaTrader 5. Для основных валютных пар Forex и большинства кросс-пар стандартным является режим SWAP_MODE_POINTS. Для CFD на индексы и сырьевые товары часто используются режимы SWAP_MODE_INTEREST или SWAP_MODE_CURRENCY_DEPOSIT. Для экзотических валютных пар режим может различаться. Функция преобразования должна корректно обрабатывать все случаи: возвращать ноль и записывать предупреждение в журнал для любого режима, который она не распознает, а не незаметно выдавать неверный результат.
Функция DailySwapInAccountCurrency()
Функция DailySwapInAccountCurrency() принимает имя символа и направление сделки, считывает соответствующее свойство свопа и режим свопа и возвращает расчетный дневной своп на лот в валюте счета. Она точно охватывает наиболее распространенные брокерские конфигурации. Для режимов, основанных на валютах, а также для некоторых брокерских конфигураций перед использованием этого значения в рабочей системе может потребоваться дополнительная конвертация. Затем вызывающий код может масштабировать результат с учетом фактического размера лота. Разделение расчета на один лот и этапа масштабирования по размеру лота позволяет повторно использовать эту функцию для позиций разного размера.
//+------------------------------------------------------------------+ //| Returns estimated daily swap per lot in account currency. | //| Covers the most common retail broker swap modes. | //| For SYMBOL_SWAP_MODE_CURRENCY_MARGIN (4) and | //| SYMBOL_SWAP_MODE_REOPEN_CURRENT (7) the function returns 0.0 | //+------------------------------------------------------------------+ double DailySwapInAccountCurrency(string symbol, int direction) { double swap_rate = 0.0; //--- Read raw swap rate with validity check if(direction > 0) { if(!SymbolInfoDouble(symbol, SYMBOL_SWAP_LONG, swap_rate)) { Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_LONG for ", symbol, " error=", GetLastError()); return(0.0); } } else { if(!SymbolInfoDouble(symbol, SYMBOL_SWAP_SHORT, swap_rate)) { Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_SHORT for ", symbol, " error=", GetLastError()); return(0.0); } } long swap_mode_raw = 0; if(!SymbolInfoInteger(symbol, SYMBOL_SWAP_MODE, swap_mode_raw)) { Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_MODE for ", symbol, " error=", GetLastError()); return(0.0); } ENUM_SYMBOL_SWAP_MODE swap_mode = (ENUM_SYMBOL_SWAP_MODE)swap_mode_raw; double contract_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE); double point_size = SymbolInfoDouble(symbol, SYMBOL_POINT); double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double bid = SymbolInfoDouble(symbol, SYMBOL_BID); double daily_swap = 0.0; //--- Validate bid — may be zero if market is closed or symbol not in Market Watch if(bid <= 0.0 && (swap_mode == SYMBOL_SWAP_MODE_INTEREST_CURRENT || swap_mode == SYMBOL_SWAP_MODE_INTEREST_OPEN)) { Print("DailySwapInAccountCurrency: bid price is 0 for ", symbol, " — result may be inaccurate."); } switch(swap_mode) { case SYMBOL_SWAP_MODE_DISABLED: daily_swap = 0.0; break; case SYMBOL_SWAP_MODE_POINTS: //--- swap_rate is in points per lot per day if(tick_size > 0.0) daily_swap = swap_rate * point_size * (tick_value / tick_size); break; case SYMBOL_SWAP_MODE_CURRENCY_SYMBOL: //--- swap_rate is in base currency per lot per day //--- Note: for non-account-currency pairs, multiply by the //--- base-to-account exchange rate before using in sizing logic. daily_swap = swap_rate; break; case SYMBOL_SWAP_MODE_INTEREST_CURRENT: //--- swap_rate is an annual percentage; bid used as price proxy if(bid > 0.0) daily_swap = (bid * contract_size * swap_rate) / 100.0 / 360.0; // MQL5 standard uses 360 days for interest calculation break; case SYMBOL_SWAP_MODE_CURRENCY_MARGIN: //--- swap_rate is denominated in the margin currency. Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_CURRENCY_MARGIN is not ", "implemented — returning 0.0 for ", symbol); daily_swap = 0.0; break; case SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT: //--- swap_rate is already in the deposit (account) currency per lot daily_swap = swap_rate; break; case SYMBOL_SWAP_MODE_INTEREST_OPEN: //--- swap_rate is an annual percentage of the position open price. if(bid > 0.0) daily_swap = (bid * contract_size * swap_rate) / 100.0 / 360.0; // MQL5 standard uses 360 days for interest calculation else Print("DailySwapInAccountCurrency: bid is 0 for SYMBOL_SWAP_MODE_INTEREST_OPEN on ", symbol, " — returning 0.0"); break; case SYMBOL_SWAP_MODE_REOPEN_CURRENT: //--- Broker-specific implementation (re-opening positions by close price) Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_REOPEN_CURRENT is not ", "implemented — returning 0.0 for ", symbol); daily_swap = 0.0; break; case SYMBOL_SWAP_MODE_REOPEN_BID: //--- Broker-specific implementation (re-opening positions by bid price) Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_REOPEN_BID is not ", "implemented — returning 0.0 for ", symbol); daily_swap = 0.0; break; default: Print("DailySwapInAccountCurrency: unknown swap mode ", swap_mode, " for ", symbol, " — returning 0.0"); daily_swap = 0.0; break; } return(daily_swap); }
Для значений SYMBOL_SWAP_MODE_INTEREST_CURRENT и SYMBOL_SWAP_MODE_INTEREST_OPEN годовая процентная ставка делится на 360, а не на 365. Это соответствует спецификации MetaTrader 5, в которой для моделей свопов на основе процентных ставок используется правило "360 банковских дней". Использование здесь значения 365 привело бы к результатам примерно на 1,4 % ниже фактического расчета брокера, что является небольшой, но предотвратимой погрешностью при длительных периодах удержания.
SYMBOL_SWAP_MODE_INTEREST_OPEN использует текущую цену Bid в качестве приблизительного значения цены на уровне символа. Для расчета по точной формуле требуется фактическая цена открытия позиции, которая становится известна только после открытия позиции. Для обеспечения точности на уровне отдельной позиции при вызове этой логики в контексте управления позициями замените цену Bid на PositionGetDouble(POSITION_PRICE_OPEN).
Если возвращается значение SYMBOL_SWAP_MODE_DISABLED, брокер не взимает проценты за перенос через ночь по данному инструменту. Функция корректно возвращает значение 0,0 без записи предупреждения в журнал, что является правильным поведением для таких активов, как некоторые криптовалютные пары или инструменты с нулевым кэрри.
Функция ExpectedSwapForPosition()//+------------------------------------------------------------------+ //| Returns total estimated swap over a holding window. | //| hold_days is a calendar approximation — it counts the number of | //| nights the position is expected to survive rollover, not exact | //| elapsed seconds. Triple swap is estimated by counting Wednesdays | //| in the window; for non-FX instruments check SYMBOL_SWAP_SUNDAY | //| through SYMBOL_SWAP_SATURDAY for the actual multiplier day. | //+------------------------------------------------------------------+ double ExpectedSwapForPosition(string symbol, int direction, double lots, int hold_days, datetime start_time = 0) { if(start_time == 0) start_time = TimeCurrent(); double daily_rate = DailySwapInAccountCurrency(symbol, direction); //--- Count Wednesdays in the holding window as a triple-swap approximation. //--- Each Wednesday rollover adds 2 extra swap days (3x total). int wed_count = 0; for(int d = 0; d < hold_days; d++) { MqlDateTime dt; TimeToStruct(start_time + (datetime)(d * 86400), dt); if(dt.day_of_week == 3) // Wednesday — adjust if broker uses different day wed_count++; } //--- Total effective swap days: calendar days plus 2 extra for each Wednesday double total_days = (double)hold_days + (wed_count * 2.0); return(daily_rate * total_days * lots); }
Параметр hold_days отражает приблизительное количество переносов через ночь, а не точное измерение прошедшего времени. Расчет по формуле (TimeCurrent() - open_time) / 86400 дает приемлемое календарное приближение для позиций, удерживаемых несколько дней, однако позиция, открытая в 23:58, пересечет первый момент ролловера уже через несколько минут, и шаг в 86 400 секунд не позволит точно это учесть. Для принятия решений об удержании позиций при свинг-трейдинге такой уровень точности, как правило, является приемлемым. Если вам нужно точное количество ролловеров, явно сравнивайте время открытия позиции с каждой границей ролловера у брокера.
Вспомогательная функция для упрощенной оценки свопов
На практике реализацию можно оформить в виде небольшой вспомогательной функции, которая оценивает ожидаемый своп за оставшийся период удержания. Приведенная ниже упрощенная версия иллюстрирует основную идею: получить значение дневного свопа, определить количество эффективных дней ролловера и умножить результат на объем позиции.
double ExpectedSwapForPosition(string symbol, int direction, double lots, int hold_days) { double daily_swap = DailySwapInAccountCurrency(symbol, direction); int triple_days = 0; datetime start = TimeCurrent(); for(int d = 0; d < hold_days; d++) { MqlDateTime dt; TimeToStruct(start + d * 86400, dt); if(dt.day_of_week == 3) triple_days++; } double effective_days = hold_days + triple_days * 2.0; return daily_swap * effective_days * lots; }
Полная версия должна также обрабатывать специфичные для брокера режимы свопа, недоступные котировки, различные графики тройного свопа и лимиты по объему.
Проверка результатов на адекватность
Прежде чем использовать эти функции в реальном советнике, сверьте их результаты со значениями свопа, указанными в окне спецификации символа MetaTrader 5. Щелкните правой кнопкой мыши любой символ в окне "Обзор рынка", выберите "Спецификация" и найдите поля Swap Long и Swap Short. Указанные там значения — это исходные ставки, публикуемые брокером. Запустите DailySwapInAccountCurrency() для того же символа в тестовом скрипте и сравните результаты. Небольшие расхождения нормальны из-за округления и различий во времени конвертации валют; крупные расхождения указывают на то, что режим свопа обрабатывается неверно.Скрипт SwapVerify позволяет выполнить полезную проверку корректности на уровне символа, но не является полноценной проверкой по реальной истории позиций. Для строгой проверки удерживайте позицию у своего брокера как минимум через один перенос через ночь, а затем проверьте в истории счета фактическую сумму свопа, зачисленную или списанную. Сравните это значение с результатом, который DailySwapInAccountCurrency() вернула для этого символа и направления в тот же день. Если значения отличаются больше, чем на небольшую погрешность округления, проверьте, не применяет ли брокер нестандартные множители по дням недели или не использует ли режим свопа, который эта функция пока не поддерживает.
//+------------------------------------------------------------------+ //| SwapVerify.mq5 | //| test script | //+------------------------------------------------------------------+ #include "SwapTools.mqh" //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string sym = _Symbol; double long_swap = DailySwapInAccountCurrency(sym,1); double short_swap = DailySwapInAccountCurrency(sym,-1); int mode = (int)SymbolInfoInteger(sym,SYMBOL_SWAP_MODE); Print("=== SWAP VERIFICATION ==="); Print("Symbol : ",sym); Print("Swap mode : ",mode); Print("Long (raw): ",SymbolInfoDouble(sym,SYMBOL_SWAP_LONG)); Print("Short (raw): ",SymbolInfoDouble(sym,SYMBOL_SWAP_SHORT)); Print("Long /lot/day (account CCY): ",DoubleToString(long_swap,4)); Print("Short /lot/day (account CCY): ",DoubleToString(short_swap,4)); double expected_swap = ExpectedSwapForPosition(sym,1,0.1,5); Print("5-day long carry at 0.1 lot: ",DoubleToString(expected_swap,2)); } //+------------------------------------------------------------------+
На рисунках 1 и 2 ниже показан процесс проверки: спецификации инструментов у брокера сравниваются с расчетами, выполненными скриптом во время выполнения. На рисунке 1 показаны официальные ставки свопа Swap Long и Swap Short, установленные брокером в окне спецификации контракта MetaTrader 5 для пары AUDJPY, а на рисунке 2 — соответствующий программный вывод, созданный скриптом SwapVerify.mq5.

Рис. 1. Окно «Спецификация символа» в MetaTrader 5 для валютной пары AUDJPY, где тип свопа указан в пунктах, ставка свопа по длинной позиции составляет 5,6, а по короткой — −12,8

Рис. 2. Вывод журнала окна "Инструменты" терминала из скрипта SwapVerify, подтверждающий получение во время выполнения конфигурации свопа AUDJPY и соответствующих расчетов на лот в валюте счета
Известные ограничения
Реализация, описанная в этой статье, рассчитана на удобство чтения и практическое применение в наиболее распространенных сценариях розничной торговли. Прежде чем использовать эту реализацию в рабочей системе, учитывайте следующие ограничения:
- Режимы, реализованные не полностью: SYMBOL_SWAP_MODE_CURRENCY_MARGIN, SYMBOL_SWAP_MODE_REOPEN_CURRENT и SYMBOL_SWAP_MODE_REOPEN_BID возвращают 0.0 и записывают предупреждение в журнал. Если ваш брокер использует любой из этих режимов, функции не будут давать точные результаты, пока вы не добавите соответствующую логику конвертации или переоткрытия позиции.
-
Конвертация валюты в валютных режимах: если своп номинирован в валюте, отличной от валюты вашего счета (что часто встречается в режиме SWAP_MODE_MONEY на кросс-парах), исходное значение свопа необходимо умножить на соответствующий обменный курс. Текущая реализация возвращает исходное значение, которое является точным только тогда, когда базовая валюта совпадает с валютой вашего счета.
-
День тройного свопа: эвристика среды работает для большинства валютных пар у розничных брокеров, однако у некоторых инструментов день тройного свопа приходится на другой день недели. MetaTrader 5 предоставляет доступ к дневным множителям через параметры от SYMBOL_SWAP_SUNDAY до SYMBOL_SWAP_SATURDAY. Если вы торгуете CFD, металлами или экзотическими валютными парами, проверьте эти значения у своего брокера, прежде чем полагаться на предположение о среде.
- SYMBOL_SWAP_MODE_INTEREST_OPEN и SYMBOL_SWAP_MODE_INTEREST_CURRENT используют цену Bid как приближенное значение цены: В обоих процентных режимах выполняется деление на 360 в соответствии с принятой в MetaTrader 5 конвенцией банковского дня. В частности, для INTEREST_OPEN точная формула требует цены открытия позиции, которая недоступна на уровне символа. Функция подставляет текущую цену Bid, что вносит пропорциональную погрешность для позиций, цена которых значительно ушла от цены входа.
- hold_days — это календарное количество дней, а не количество ролловеров: деление прошедшего времени в секундах на 86 400 дает разумную оценку для многодневных позиций, но не позволяет точно учесть позиции, открытые вблизи времени отсечки ролловера.
Раздел 3: Пример AUDJPY — числа на практике
Почему AUDJPY — подходящая пара для такой демонстрации
AUDJPY — классический пример валютной пары для кэрри-трейда. Резервный банк Австралии традиционно поддерживал более высокие процентные ставки, чем Банк Японии, который удерживает ставки по JPY около нуля. Этот дифференциал процентных ставок означает, что по длинным позициям AUDJPY обычно начисляется положительное своповое начисление, тогда как по коротким позициям этот дифференциал уплачивается в противоположном направлении. Разница между ставками свопа для длинных и коротких позиций по AUDJPY заметно шире, чем по другим основным кросс-парам, что делает эту пару идеальной для торговых моделей, чувствительных к свопам.
AUDJPY — волатильная валютная пара. Его динамика зависит от отношения рынка к риску: он растет в периоды повышенного глобального аппетита к риску и резко снижается, когда рынки начинают нервничать. Именно это сочетание высокого кэрри и повышенной волатильности делает данный пример интересным объектом для изучения: доход от свопов значителен, но столь же значителен и ценовой риск. Советник должен учитывать и то и другое, чтобы принимать разумные решения об удержании позиции.
Расчеты для объема 0,1 лота
В таблице 2 ниже показан пошаговый расчет кэрри по AUDJPY при текущих приблизительных ставках. Приведенные здесь значения свопа носят иллюстративный характер и будут различаться у разных брокеров; цель — показать арифметику, которую функция DailySwapInAccountCurrency() выполняет внутри.
| Параметр | Значение | Единица измерения | Примечания | Источник |
|---|---|---|---|---|
| Своп по длинной позиции AUDJPY | +12,0 | пункты/лот/день | Положительный кэрри — длинная позиция по AUD против JPY | SYMBOL_SWAP_LONG |
| Своп по короткой позиции AUDJPY | -21,0 | пункты/лот/день | Отрицательный — затраты на удержание короткой позиции | SYMBOL_SWAP_SHORT |
| Размер контракта | 100 000 | единиц | Стандартный размер лота (1,0 лота) | SYMBOL_TRADE_CONTRACT_SIZE |
| Стоимость пункта (JPY) | 0,001 | иен за единицу | 1 пункт = третий знак после запятой по паре AUDJPY | SYMBOL_POINT |
| своп за день для 0,1 лота | 120 | иен в день | (12,0 пункта * 0,001) * 10 000 единиц | Рассчитано |
| Эквивалент в долларах США | примерно 0,77 долл. США | долл. США в день | Сумма пересчитана из иен по текущему курсу USDJPY | Расчет во время выполнения |
Таблица 2: Расчет кэрри по паре AUDJPY при объеме 0,1 лота для счета в долларах США. Ставки свопа у брокеров могут различаться; всегда получайте актуальные значения через SYMBOL_SWAP_LONG, а не используйте жестко заданные значения. При таких ставках 7-дневное удержание позиции (включая среду тройного свопа) приносит примерно 5,40 доллара в виде свопового начисления, что покрывает около 28 % риска при типичном стоп-лоссе в 30 пунктов.
Своповое начисление в размере 5,40 доллара за семь дней при объеме 0,1 лота само по себе может показаться незначительным, но его стоит рассматривать относительно риска позиции. Стоп-лосс в 30 пунктов по паре AUDJPY при объеме 0,1 лота обходится примерно в 19,35 доллара (исходя из стоимости пункта около 0,645 доллара по текущим обменным курсам). После семи дней положительного кэрри, включая среду тройного свопа, по позиции компенсировано примерно 28 % общего риска исключительно за счет процентов. На инструментах с более широкими дифференциалами процентных ставок эта математическая подушка становится ещё более значимой для систем свинг-трейдинга.
Раздел 4: Функция принятия решения об удержании
Принцип работы функции IsWorthHolding()
Функция принятия решения об удержании позиции отвечает на конкретный вопрос: с учетом текущих ценовых потерь по этой позиции, сможет ли ожидаемый доход от свопа за оставшийся период удержания компенсировать достаточную часть этих потерь, чтобы оправдать дальнейшее нахождение в сделке? Она не заменяет логику выхода полностью. Позиция, по которой сработал стоп-лосс, будет закрыта. Однако для позиций в умеренной плавающей просадке, по которым не сработали жесткие условия выхода, эта функция позволяет оценить с поправкой на кэрри, является ли дальнейшее ожидание экономически рациональным.
Функция принимает четыре входных параметра: тикет позиции, максимальное количество дней, в течение которых советник готов удерживать любую сделку, процент плавающего убытка, который должен покрыть своп, чтобы удержание позиции было оправданным, а также, необязательно, текущую временную метку. Она считывает плавающую прибыль/убыток (P&L) по позиции, рассчитывает ожидаемый своп за оставшийся период и возвращает true, если эта величина свопа достигает порога покрытия.
//+------------------------------------------------------------------+ //| Evaluates if position drawdown is justified by carry income | //+------------------------------------------------------------------+ bool IsWorthHolding(ulong ticket, int max_hold_days, double coverage_pct = 40.0) { if(!PositionSelectByTicket(ticket)) return(false); double total_profit = PositionGetDouble(POSITION_PROFIT); double accrued_swap = PositionGetDouble(POSITION_SWAP); double lots = PositionGetDouble(POSITION_VOLUME); string sym = PositionGetString(POSITION_SYMBOL); datetime open_t = (datetime)PositionGetInteger(POSITION_TIME); int pos_type = (int)PositionGetInteger(POSITION_TYPE); int direction = (pos_type == POSITION_TYPE_BUY) ? 1 : -1; //--- Isolate the price-driven P/L by removing already-accrued swap. //--- POSITION_PROFIT includes swap credits, so using it directly would //--- cause double-counting when we add future expected swap below. double price_pnl = total_profit - accrued_swap; //--- If the trade is already profitable on price alone, keep it. if(price_pnl >= 0) return(true); //--- days_held is a calendar approximation. Rollover count may differ //--- by 1 depending on open time relative to the broker rollover cutoff. int days_held = (int)((TimeCurrent() - open_t) / 86400); int days_left = max_hold_days - days_held; if(days_left <= 0) return(false); double future_swap = ExpectedSwapForPosition(sym, direction, lots, days_left); //--- Total carry = what has already been earned + what is still expected. //--- This gives a complete picture of the position's carry contribution. double total_carry = future_swap + accrued_swap; if(total_carry <= 0) return(false); double coverage_req = MathAbs(price_pnl) * (coverage_pct / 100.0); return(total_carry >= coverage_req); }
Параметр coverage_pct является ключевой переменной настройки. Значение 100 означает, что советник будет удерживать позицию только в том случае, если ожидается, что своп за оставшийся период полностью покроет плавающий убыток; это консервативный подход. Значение 40 означает, что советник удерживает позицию до тех пор, пока доход от кэрри покрывает 40 % убытка, оставляя оставшуюся часть на восстановление цены. Правильное значение зависит от доли прибыльных сделок в стратегии и от того, насколько трейдер уверен в исходной торговой гипотезе.
Встраивание логики в цикл советника
Чтобы сохранить чистую и модульную архитектуру, функция IsWorthHolding() интегрируется в выделенный цикл управления, а не размещается непосредственно внутри OnTick(). Типовой шаблон предполагает, что сначала отрабатываются жесткие условия выхода, например разворот технического сигнала или срабатывание жесткого стоп-лосса. Только если эти основные условия не выполняются, а позиция остается в плавающей просадке, советник вызывает IsWorthHolding(), чтобы определить, оправдывает ли прогнозируемый кэрри-доход сохранение рыночной экспозиции.
//+------------------------------------------------------------------+ //| Modular function to manage open positions based on carry logic | //+------------------------------------------------------------------+ void ManageExistingPositions() { //--- Run carry checks once per H1 bar, not every tick. //--- Swap values only change at rollover, so tick-level evaluation //--- adds no useful information and wastes processing time. static datetime last_bar_time = 0; datetime current_bar = iTime(_Symbol, PERIOD_H1, 0); if(current_bar == last_bar_time) return; last_bar_time = current_bar; //--- Loop backwards through open positions to allow safe mid-loop closing for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(!PositionSelectByTicket(ticket)) continue; //--- Filter for the current chart symbol only if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; double profit = PositionGetDouble(POSITION_PROFIT); //--- 1. PRIMARY EXITS (Hard Rules) //--- Check for a technical reversal first. If the trend has flipped, //--- no amount of carry income justifies staying in the trade. if(IsTrendReversed()) { if(!trade.PositionClose(ticket)) PrintFormat("ManageExistingPositions: failed to close ticket #%I64u on reversal — error %d", ticket, GetLastError()); continue; } //--- 2. CARRY-AWARE SECONDARY LOGIC //--- Only evaluate carry when the position is underwater. //--- If profit is flat or positive, there is nothing to offset. if(profit < 0) { if(!IsWorthHolding(ticket, InpHoldDaysLimit, InpSwapCoverage)) { //--- Yield is insufficient to justify the current drawdown — close the position if(!trade.PositionClose(ticket)) PrintFormat("ManageExistingPositions: failed to close ticket #%I64u on carry exit — error %d", ticket, GetLastError()); else PrintFormat("Carry-Aware: Closed ticket #%I64u — swap income does not cover drawdown.", ticket); } else { //--- Yield justifies continuing to hold — log once per bar per ticket PrintFormat("Carry-Aware: Holding ticket #%I64u — carry coverage is sufficient.", ticket); } } } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Evaluate carry logic and manage open positions ManageExistingPositions(); //--- Entry logic placeholder //--- CheckEntrySignals(); } //+------------------------------------------------------------------+
Примечание: функция IsWorthHolding() ни в коем случае не должна иметь приоритет над жестким стоп-лоссом. Если позиция достигла заранее заданного в советнике уровня стоп-лосса, закройте её независимо от того, какое значение возвращает функция расчета свопа. Логика кэрри предназначена для позиций, находящихся в умеренной плавающей просадке и еще не вызвавших срабатывание жесткого условия выхода, а не для оправдания удержания позиции, которая явно ушла против сделки.
Раздел 5: Определение размера позиции с поправкой на кэрри
Определение размера позиции при входе в сделку "кэрри-трейд"
Второе практическое применение — определение размера позиции. Вместо того чтобы определять размер позиции исключительно на основе процентной доли риска, функция CarryAdjustedLotSize() корректирует объем так, чтобы обеспечить целевой вклад ожидаемого свопа.
Прежде чем использовать эту функцию в реальной стратегии, следует учесть следующее: увеличение размера лота с целью получить более крупное своповое начисление также увеличивает риск по стоп-лоссу в той же пропорции. Позиция объемом 0,28 лота сопряжена с ценовым риском, примерно в 2,8 раза превышающим риск позиции объемом 0,1 лота. Кэрри-доход стабилен, но невелик; неблагоприятное движение цены может за считанные минуты свести на нет своповое начисление, накопленное за несколько дней. Используйте расчет размера позиции с поправкой на кэрри как инструмент точной настройки в рамках вашей обычной модели риска, а не как повод превышать ее ограничения. Размер лота с поправкой на кэрри всегда следует проверять на соответствие максимально допустимому риску перед отправкой заявки.
//+------------------------------------------------------------------+ //| Returns a carry-adjusted lot size. | //| | //| IMPORTANT: Increasing lot size to chase swap income also | //| increases stop-loss risk by the same proportion. Carry should | //| never be the primary reason to take on more risk than your | //| normal position sizing allows. Use this function to fine-tune | //| within your existing risk limits, not to override them. | //| The carry-adjusted lot is always capped at base_lots unless swap | //| income meaningfully justifies a larger size under your model. | //+------------------------------------------------------------------+ double CarryAdjustedLotSize(string symbol, int direction, double risk_money, int hold_days, double target_pct = 50.0, double base_lots = 0.1) { double daily_rate = DailySwapInAccountCurrency(symbol, direction); //--- If swap is zero or negative, no carry adjustment is warranted. if(daily_rate <= 0) return(base_lots); double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); //--- Count Wednesdays for accurate total-day estimate datetime now = TimeCurrent(); double total_days = (double)hold_days; for(int d = 0; d < hold_days; d++) { MqlDateTime dt; TimeToStruct(now + (datetime)(d * 86400), dt); if(dt.day_of_week == 3) total_days += 2.0; } //--- Target carry is expressed as a percentage of the ORIGINAL risk_money. //--- We solve for the lot size at which carry == target_pct% of risk_money. //--- Per-lot carry over the window: double carry_per_lot = daily_rate * total_days; if(carry_per_lot <= 0) return(base_lots); double target_carry = risk_money * (target_pct / 100.0); double required_lots = target_carry / carry_per_lot; //--- NOTE: If required_lots > base_lots, verify that your risk model //--- permits the larger position. The stop-loss monetary risk scales //--- linearly with lot size. Always apply your normal risk cap first. if(step > 0) required_lots = MathFloor(required_lots / step) * step; // floor, not round if(required_lots < min_lot) required_lots = min_lot; if(required_lots > max_lot) required_lots = max_lot; //--- Log for transparency PrintFormat("CarryAdjustedLotSize: %s dir=%d carry_per_lot=%.4f " "target=%.2f required=%.2f base=%.2f", symbol, direction, carry_per_lot, target_carry, required_lots, base_lots); return(required_lots); }
При каждом вызове функция выводит результаты вычислений в журнал, что удобно на этапе первоначального тестирования. В рабочей версии эту строку Print() можно удалить или поместить под флаг отладки. Ключевым результатом является возвращаемый размер лота, который можно напрямую передать в вызов открытия позиции в логике входа советника.
Разбор примераПри длинной позиции по AUDJPY объемом 0,1 лота с ежедневным свопом 7,70 доллара на лот за 7 эффективных дней (5 календарных дней, включая одну среду) доход от кэрри составит 5,39 доллара — 21,5 % от риска по стоп-лоссу в размере 25 долларов. Увеличение размера лота повышает кэрри-доход, но в той же степени увеличивает риск по стоп-лоссу, поэтому процент покрытия не улучшается. Функцию CarryAdjustedLotSize() следует использовать для точной настройки размера позиции в рамках существующего бюджета риска, а не за его пределами.
Раздел 6: Практические аспекты и предостережения
Своп-ставки меняются
Своп-ставки изменяются при изменении политики центральных банков. Любой советник, принимающий решения об удержании позиции на основе кэрри, должен считывать ставки во время выполнения, а не полагаться на жестко заданные значения. Здесь функции всегда выполняют прямой вызов SymbolInfoDouble() по текущим данным. Кроме того, стоит периодически сверять результаты работы SwapVerify с фактической историей счета, поскольку некоторые брокеры взимают комиссии сверх базового дифференциала процентных ставок, и эти комиссии не отображаются в спецификации символа.
Пары с отрицательным кэрри
Функция DailySwapInAccountCurrency() возвращает отрицательное значение для направлений, по которым трейдер платит своп. В этом случае функция IsWorthHolding() сразу же возвращает значение false. Функция CarryAdjustedLotSize() возвращает значение baseLots без изменений, если положительный кэрри отсутствует, переходя к стандартному расчету размера позиции на основе риска.
Бэктестинг логики с учетом свопов
В "Тестере стратегий" MetaTrader 5 применяются текущие ставки свопов брокера, а не исторические, что делает результаты бэктестов на основе кэрри ненадежными без дополнительного вмешательства. Предоставленная функция MockSwapForTesting() подставляет фиксированную синтетическую ставку для длинных позиций по AUDJPY, что позволяет контролируемо тестировать поведение функций CarryAdjustedLotSize() и IsWorthHolding(). Это не историческая реконструкция. Для точного бэктестинга практический подход состоит в том, чтобы загружать значения свопов из CSV-файла с ключами по дате и символу и вызывать рабочие функции через оболочку с учетом даты, которая выбирает соответствующую ставку.
Раздел 7: Результаты тестирования: базовый вариант и вариант с учетом кэрри
Тестирование CarryDemo.mq5 на AUDJPY H1 за 24-месячный период с января 2021 года по декабрь 2022 года показывает одну наглядную конфигурацию, в которой логика с учетом кэрри превзошла базовый вариант. В данном конкретном тесте версия с учетом кэрри переждала несколько краткосрочных просадок, на которых базовый вариант закрывал позиции досрочно, а накопленный своповый доход частично компенсировал эти просадки в периоды бокового движения цены.
Чтобы количественно оценить разницу, советник запускали дважды за один и тот же период с одинаковыми настройками, меняя между запусками только входной параметр InpCarryAware. В приведенной ниже таблице 3 сведены четыре ключевых показателя по каждому запуску.
Таблица 3: Базовый вариант и вариант с учетом кэрри на графике AUDJPY H1 (2021–2022)
| Показатель | Базовый вариант (без учета кэрри) | вариант с учетом кэрри |
|---|---|---|
| Чистая прибыль | −144,04 доллара | +659,28 долларов |
| Максимальная просадка | 10,28 % (1 113,55 долл.) | 1,28 % (128,81 долл.) |
| Коэффициент прибыльности | 0,98 | 4,45 |
| Общее количество сделок | 786 | 17 |
Базовый советник совершил 786 сделок, закрывая позицию при каждом развороте скользящей средней и по истечении временного лимита без какого-либо учета свопового дохода. По итогам периода он показал чистый убыток 144,04 доллара США, коэффициент прибыльности ниже 1,0 и максимальную просадку чуть более 10 %. Высокое количество сделок отражает, насколько часто срабатывали механические правила выхода из позиции, каждый раз фиксируя небольшой убыток до того, как кэрри-доход успевал накопиться.
Советник с учетом кэрри за тот же период совершил 17 сделок. Когда появлялся сигнал разворота или позиция переходила в просадку, советник перед принятием решения о закрытии сначала оценивал, достаточно ли накопленного и ожидаемого дохода по свопам для покрытия убытка. В большинстве случаев его было достаточно, и позиция удерживалась. Меньшее количество сделок не обязательно означает упущенную возможность. Кэрри-фильтр по своему принципу избирателен и закрывает позицию только в том случае, если доход по свопам не оправдывает ее дальнейшее удержание. В среде с положительным кэрри, например при длинной позиции по AUDJPY, это условие выполняется редко, что естественным образом приводит к меньшему числу закрытых сделок. В результате чистая прибыль составила 659,28 доллара США, коэффициент прибыльности — 4,45, а максимальная просадка — всего 1,28 %.
Приведенные ниже кривые капитала наглядно показывают, как эти показатели визуально отражаются в двух прогонах.

Рис. 3. Кривая капитала для прогона базового варианта в Тестере стратегий: работа строго по техническим условиям выхода при отключенной логике с учетом кэрри. Без учета доходности для компенсации затрат на удержание позиций стратегия часто сталкивается со снижением капитала в периоды флэта и преждевременными выходами на небольших откатах, из-за чего не может переждать временный рыночный шум и дождаться более значительного восстановления цены

Рис. 4. Кривая капитала прогона в Тестере стратегий при включенной логике с учетом кэрри. При полностью активном учете доходности стратегия использует постоянный ежедневный доход от свопов в качестве финансового буфера для фильтрации краткосрочного рыночного шума, предотвращая преждевременные технические выходы и позволяя безопасно удерживать позиции в периоды флэта или временных просадок
Эти результаты следует рассматривать как демонстрацию работоспособности концепции, а не как эталонную оценку эффективности. В тесте используется синтетическое переопределение свопа (см. раздел 6.3), он охватывает один инструмент и один таймфрейм и не учитывает брокерские спреды, проскальзывание или комиссии. Логика стратегии, построенная на пороговых значениях кэрри, может быть чувствительна к этим порогам, а результаты на других инструментах или периодах времени могут значительно отличаться. Прежде чем применять этот подход в реальной торговле, протестируйте его на фактических исторических данных о свопах вашего брокера для разных инструментов и рыночных условий.
Заключение
Ставки свопа — это не просто примечание. На парах с положительным кэрри, удерживаемых в течение нескольких дней, они существенно влияют на доходность сделок, но большинство автоматизированных стратегий просто оставляет этот вклад неиспользованным. Разрыв между тем, что советник знает о позиции, и тем, что фактически происходит с этой позицией, увеличивается при каждом ролловере, поскольку кэрри-доход накапливается на счете, тогда как логика советника его совершенно не учитывает.
Инструменты, созданные в этой статье, устраняют этот разрыв. Функция DailySwapInAccountCurrency() дает любому советнику практическую оценку дневного кэрри в валюте счета, охватывая наиболее распространенные режимы свопа у розничных брокеров и явно обрабатывая неподдерживаемые режимы. Функция ExpectedSwapForPosition() расширяет этот подход на весь период удержания, используя эвристику тройного свопа по средам, которая работает для большинства валютных пар у стандартных розничных брокеров, с примечаниями о случаях, когда могут применяться множители, зависящие от брокера. Функция IsWorthHolding() использует показатель кэрри в решении "удерживать или закрывать", рассматривая накопленный кэрри как реальный доход, компенсирующий плавающие убытки. Функция CarryAdjustedLotSize() применяет ту же логику к решению о входе, определяя размер позиций так, чтобы ожидаемый вклад кэрри соответствовал заданной доле начального риска.
Ни одна из этих функций не меняет то, как стратегия входит в рынок. Они добавляют уровень осведомленности о том, что происходит после входа, для пар, где после входа возникает ежедневное процентное начисление, которое существенно накапливается при многодневном удержании. Для любого советника, работающего с AUDJPY, NZDUSD или аналогичными инструментами с положительным кэрри, такая осведомленность не является опциональной — это часть того, что делает логику управления позициями полноценной.
Программы, использованные в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | SwapTools.mqh | Include-файл | Include-файл, содержащий функции MockSwapForTesting(), DailySwapInAccountCurrency(), ExpectedSwapForPosition(), IsWorthHolding() и CarryAdjustedLotSize(). Поместите файл в папку MQL5/Include/ и подключите его с помощью одной строки #include. MockSwapForTesting() — это исключительно заглушка для бэктестинга, и её не следует вызывать в торговой логике для реального счета. |
| 2 | SwapVerify.mq5 | Скрипт | Скрипт проверки, который выводит в журнал "Эксперты" текущие ставки свопа, суточный своп в валюте счета и ожидаемый кэрри-доход за 5 дней. Запустите на любом символе перед внедрением логики с учетом свопов. |
| 3 | CarryDemo.mq5 | Демонстрационный советник | Демонстрационный советник, показывающий интеграцию всех четырех функций в простую стратегию открытия длинных позиций по AUDJPY с настраиваемым ограничением по дням удержания, порогом покрытия свопа и расчетом размера лота с поправкой на кэрри. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22175
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Создание индикатора Market Structure Sentinel на языке MQL5
Построение системы обнаружения дивергенций (Часть IV): Создание переиспользуемого модуля обнаружения дивергенций для MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования