Carry Trade Logic in MQL5: Building an EA That Factors Swap Rates Into Position Sizing and Holding Decisions - MQL5 Articles

[
\
Dozens of channels with market analytics in different languages.\
\
MetaTrader 5 / Trading systems
Introduction
There is a common oversight that many retail developers fail to account for. They either ignore it, use a fixed number that doesn't properly account for market fluctuations, or procrastinate the math indefinitely.
For scalpers, swap rates don't really matter as they never hold positions past the daily market rollover. For traders that hold positions overnight, and especially strategies built around high-interest pairs (like AUDJPY or NZDUSD), ignoring swap rates is like driving without checking your fuel gauge.
Swap rates are the overnight interest fees paid or earned when a trader holds a position past the daily market close. If you hold a higher interest currency (like AUD) against a lower interest currency (like JPY), you earn interest (positive swap); otherwise, you pay a fee if you are holding the lower interest currency against the higher interest currency (negative swap). The cumulative cost or interest earned over a week can determine if the trade will be profitable or not.
Most EAs do not account for swap rates. The swap rate shows up in the account history, but it plays no role in the entry decision, the holding period logic, or the position sizing.
In this article, we will walk through the process of retrieving swap data in MQL5, converting it to a usable daily figure in account currency, and putting it to work. We will build four practical functions: DailySwapInAccountCurrency(), ExpectedSwapForPosition(), IsWorthHolding(), and CarryAdjustedLotSize().
Together they handle swap retrieval, hold-window projection, carry-justified exit decisions, and carry-aware position sizing. Each function is a self-contained building block that can be dropped into any existing EA without changing its entry logic.
Section 1: Swap Rates—What They Are and Why They Matter
The Mechanics of Overnight Interest
In spot forex trading, every open position that rolls past the daily settlement time (typically 17:00 New York time) incurs an overnight interest fee or credit. These rates are derived from the difference between the interbank lending costs of the two currencies involved. Every night, traders either receive a credit when they are holding the currency with the higher interest rate or pay a debit when they are holding the currency with the lower interest rate.
Brokers express these adjustments as swap rates, which appear in the symbol specifications under Swap Long and Swap Short. What they actually represent varies by broker and symbol, and that variation is one of the reasons developers tend to ignore the topic rather than untangle it.
MetaTrader 5 exposes nine swap modes through SYMBOL_SWAP_MODE (ENUM_SYMBOL_SWAP_MODE). 0 with a log warning for two broker-specific reopening modes that require proprietary details.
The amounts involved are substantial on the right instruments. A standard lot of AUDJPY long currently earns somewhere between $5 and $9 per day depending on the broker, based on the AUDJPY interest rate differential.
95 per day. Accumulated over a five-day hold (yielding 7 days of swap), it represents 14% to 26% of the risk recovered on a $25 stop-loss purely from the carry.
On instruments with wider differentials, such as MXN/JPY, this cushion becomes even more significant.
When Swap Changes the Trade's Math
Let us consider a simple scenario. A swing EA on AUDJPY opens a long position with a 30-pip stop-loss and an 80-pip take profit.
The trade sits at minus 15 pips after two days. Under standard logic, the EA might close it early if a signal flips or a time-based exit kicks in.
50 in carry income. 1 lot on AUDJPY), recovering about 6% of the total risk.
This is not enormous, but it becomes meaningful when the trade is close to its stop-loss.
Integrate this into a strategy that intentionally targets carry-positive pairs and holds positions for three to seven days, and the swap contribution becomes a genuine part of the return calculation. Some carry-focused strategies earn more from the overnight credit than from price movement in a given week. For those strategies, building swap awareness into the EA is not a nice-to-have feature; it is part of what makes the logic complete.
Even for strategies that are not carry-focused, the hold decision function developed in this article has practical value.Any time-filtered EA that holds positions past rollover implicitly takes a carry position. Knowing whether that carry helps or hurts, and by how much, gives the EA more information to act on when deciding whether to stay in a trade or cut it.
Triple Swap Wednesday: The functions in this article account for this by counting the number of Wednesday rollovers within the holding window and adding two extra swap days for each one. This is a widely applicable heuristic for forex pairs on most retail brokers. Note that for some instruments or brokers the triple-swap day may fall on a different weekday, and MetaTrader 5 provides per-day multipliers (SYMBOL_SWAP_SUNDAY through SYMBOL_SWAP_SATURDAY) that can be used to build a more precise calculation if needed.
Section 2: Reading Swap Data in MQL5
The Three Key Symbol Properties
MQL5 reveals swap information through three symbol properties: SYMBOL_SWAP_LONG returns the swap rate applied to long positions, SYMBOL_SWAP_SHORT returns the rate for short positions (both are doubles and can be positive or negative: a positive value means the trader receives credit; a negative value means a debit is charged), and SYMBOL_SWAP_MODE returns an integer that tells you what unit those values are expressed in.
Getting the raw values is easy. Converting them to actual account currency per lot per day is where the complexity sits, because the calculation depends on which swap mode the broker uses for that instrument.
Table 1 below summarizes the seven modes available in MetaTrader 5 and the formula each one requires.
| SYMBOL_SWAP_MODE | Value | Swap Unit | Typical Estimate | Code Support |
|---|---|---|---|---|
| SYMBOL_SWAP_MODE_DISABLED | 0 | No swap charged | — | Full — returns 0.0 cleanly |
| SYMBOL_SWAP_MODE_POINTS | 1 | Points per lot per day | swapPts × point × (tickValue / tickSize) | Full |
| SYMBOL_SWAP_MODE_CURRENCY_SYMBOL | 2 | Base currency per lot per day | swapVal × lots (convert to account CCY if needed) | Partial — no currency conversion |
| SYMBOL_SWAP_MODE_INTEREST_CURRENT | 3 | % of current price per day | bid × contractSize × swapPct / 100 / 360 | Full (uses current bid as price proxy) |
| SYMBOL_SWAP_MODE_CURRENCY_MARGIN | 4 | Margin currency per lot | swapVal × lots | Not implemented — returns 0.0 |
| SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT | 5 | Deposit currency per lot | swapVal × lots (already in deposit CCY) | Full |
| SYMBOL_SWAP_MODE_INTEREST_OPEN | 6 | % of open price per day | openPrice × contractSize × swapPct / 100 / 360 | Approximate — uses bid, not open price |
| SYMBOL_SWAP_MODE_REOPEN_CURRENT | 7 | Reopened at close price | Broker-specific | Not implemented — returns 0.0 |
| SYMBOL_SWAP_MODE_REOPEN_BID | 8 | Reopened at bid price | Broker-specific | Not implemented — returns 0.0 |
Table 1: SYMBOL_SWAP_MODE enum values and their support level in this article's implementation. Value integers shown here reflect the MQL5 documentation order. Always verify the mode your broker actually uses against the symbol specification window in MetaTrader 5, as broker configurations vary.
Modes 0 through 2 account for the vast majority of instruments on retail MetaTrader 5 brokers. For forex majors and most crosses, SWAP_MODE_POINTS is standard.
CFDs on indices and commodities often use SWAP_MODE_INTEREST or SWAP_MODE_CURRENCY_DEPOSIT. Exotic currency pairs can vary.
The conversion function needs to handle all cases correctly, returning zero and logging a warning for any mode it does not recognize rather than producing a silently incorrect result.
The DailySwapInAccountCurrency() Function
The DailySwapInAccountCurrency() function takes a symbol name and a trade direction, reads the appropriate swap property and mode, and returns an estimated daily swap per lot in account currency. It covers the most common broker setups accurately.
For currency-based modes and some broker-specific configurations, additional conversion work may be required before using this value in a production system. The calling code can then scale by actual lot size.
Separating the per-lot calculation from the lot-scaling step keeps the function reusable across different position sizes.
1//+------------------------------------------------------------------+
2//| Returns estimated daily swap per lot in account currency. |
3//| Covers the most common retail broker swap modes. |
4//| For SYMBOL_SWAP_MODE_CURRENCY_MARGIN (4) and |
5//| SYMBOL_SWAP_MODE_REOPEN_CURRENT (7) the function returns 0.0 |
6//+------------------------------------------------------------------+
7double DailySwapInAccountCurrency(string symbol, int direction)
8 {
9 double swap_rate = 0.0;
10
11//--- Read raw swap rate with validity check
12 if(direction > 0)
13 {
14 if(!SymbolInfoDouble(symbol, SYMBOL_SWAP_LONG, swap_rate))
15 {
16 Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_LONG for ", symbol,
17 " error=", GetLastError());
18 return(0.0);
19 }
20 }
21 else
22 {
23 if(!SymbolInfoDouble(symbol, SYMBOL_SWAP_SHORT, swap_rate))
24 {
25 Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_SHORT for ", symbol,
26 " error=", GetLastError());
27 return(0.0);
28 }
29 }
30
31 long swap_mode_raw = 0;
32 if(!SymbolInfoInteger(symbol, SYMBOL_SWAP_MODE, swap_mode_raw))
33 {
34 Print("DailySwapInAccountCurrency: failed to read SYMBOL_SWAP_MODE for ", symbol,
35 " error=", GetLastError());
36 return(0.0);
37 }
38
39 ENUM_SYMBOL_SWAP_MODE swap_mode = (ENUM_SYMBOL_SWAP_MODE)swap_mode_raw;
40
41 double contract_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
42 double point_size = SymbolInfoDouble(symbol, SYMBOL_POINT);
43 double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
44 double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
45 double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
46 double daily_swap = 0.0;
47
48//--- Validate bid — may be zero if market is closed or symbol not in Market Watch
49 if(bid <= 0.0 &&
50 (swap_mode == SYMBOL_SWAP_MODE_INTEREST_CURRENT ||
51 swap_mode == SYMBOL_SWAP_MODE_INTEREST_OPEN))
52 {
53 Print("DailySwapInAccountCurrency: bid price is 0 for ", symbol,
54 " — result may be inaccurate.");
55 }
56
57 switch(swap_mode)
58 {
59 case SYMBOL_SWAP_MODE_DISABLED:
60 daily_swap = 0.0;
61 break;
62
63 case SYMBOL_SWAP_MODE_POINTS:
64 //--- swap_rate is in points per lot per day
65 if(tick_size > 0.0)
66 daily_swap = swap_rate * point_size * (tick_value / tick_size);
67 break;
68
69 case SYMBOL_SWAP_MODE_CURRENCY_SYMBOL:
70 //--- swap_rate is in base currency per lot per day
71 //--- Note: for non-account-currency pairs, multiply by the
72 //--- base-to-account exchange rate before using in sizing logic.
73 daily_swap = swap_rate;
74 break;
75
76 case SYMBOL_SWAP_MODE_INTEREST_CURRENT:
77 //--- swap_rate is an annual percentage; bid used as price proxy
78 if(bid > 0.0)
79 daily_swap = (bid * contract_size * swap_rate) / 100.0 / 360.0; // MQL5 standard uses 360 days for interest calculation
80 break;
81
82 case SYMBOL_SWAP_MODE_CURRENCY_MARGIN:
83 //--- swap_rate is denominated in the margin currency.
84 Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_CURRENCY_MARGIN is not ",
85 "implemented — returning 0.0 for ", symbol);
86 daily_swap = 0.0;
87 break;
88
89 case SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT:
90 //--- swap_rate is already in the deposit (account) currency per lot
91 daily_swap = swap_rate;
92 break;
93
94 case SYMBOL_SWAP_MODE_INTEREST_OPEN:
95 //--- swap_rate is an annual percentage of the position open price.
96 if(bid > 0.0)
97 daily_swap = (bid * contract_size * swap_rate) / 100.0 / 360.0; // MQL5 standard uses 360 days for interest calculation
98 else
99 Print("DailySwapInAccountCurrency: bid is 0 for SYMBOL_SWAP_MODE_INTEREST_OPEN on ",
100 symbol, " — returning 0.0");
101 break;
102
103 case SYMBOL_SWAP_MODE_REOPEN_CURRENT:
104 //--- Broker-specific implementation (re-opening positions by close price)
105 Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_REOPEN_CURRENT is not ",
106 "implemented — returning 0.0 for ", symbol);
107 daily_swap = 0.0;
108 break;
109
110 case SYMBOL_SWAP_MODE_REOPEN_BID:
111 //--- Broker-specific implementation (re-opening positions by bid price)
112 Print("DailySwapInAccountCurrency: SYMBOL_SWAP_MODE_REOPEN_BID is not ",
113 "implemented — returning 0.0 for ", symbol);
114 daily_swap = 0.0;
115 break;
116
117 default:
118 Print("DailySwapInAccountCurrency: unknown swap mode ", swap_mode,
119 " for ", symbol, " — returning 0.0");
120 daily_swap = 0.0;
121 break;
122 }
123
124 return(daily_swap);
125 }For SYMBOL_SWAP_MODE_INTEREST_CURRENT and SYMBOL_SWAP_MODE_INTEREST_OPEN, the annual percentage rate is divided by 360 rather than 365. This follows the MetaTrader 5 specification, which uses a 360 banking-day convention for its interest-based swap models. Using 365 here would produce results roughly 1.4% lower than the broker's actual calculation, which is a small but avoidable error on long holding periods.
SYMBOL_SWAP_MODE_INTEREST_OPEN uses the current bid as a price proxy at the symbol level. The exact formula requires the position's actual open price, which is only available once a position exists. For per-position accuracy, replace bid with PositionGetDouble(POSITION_PRICE_OPEN) when calling this logic from within a position-management context.
When SYMBOL_SWAP_MODE_DISABLED is returned, the broker charges no overnight interest on that instrument. The function returns 0.0 cleanly without logging a warning, which is the correct behavior for assets like some crypto pairs or instruments with zero carry.
The ExpectedSwapForPosition() Function
1//+------------------------------------------------------------------+
2//| Returns total estimated swap over a holding window. |
3//| hold_days is a calendar approximation — it counts the number of |
4//| nights the position is expected to survive rollover, not exact |
5//| elapsed seconds. Triple swap is estimated by counting Wednesdays |
6//| in the window; for non-FX instruments check SYMBOL_SWAP_SUNDAY |
7//| through SYMBOL_SWAP_SATURDAY for the actual multiplier day. |
8//+------------------------------------------------------------------+
9double ExpectedSwapForPosition(string symbol, int direction,
10 double lots, int hold_days,
11 datetime start_time = 0)
12 {
13 if(start_time == 0)
14 start_time = TimeCurrent();
15
16 double daily_rate = DailySwapInAccountCurrency(symbol, direction);
17
18//--- Count Wednesdays in the holding window as a triple-swap approximation.
19//--- Each Wednesday rollover adds 2 extra swap days (3x total).
20 int wed_count = 0;
21 for(int d = 0; d < hold_days; d++)
22 {
23 MqlDateTime dt;
24 TimeToStruct(start_time + (datetime)(d * 86400), dt);
25 if(dt.day_of_week == 3) // Wednesday — adjust if broker uses different day
26 wed_count++;
27 }
28
29//--- Total effective swap days: calendar days plus 2 extra for each Wednesday
30 double total_days = (double)hold_days + (wed_count * 2.0);
31
32 return(daily_rate * total_days * lots);
33 }The hold_days parameter represents an estimated number of overnight rollovers, not a precise elapsed-time measurement. Calculating it as (TimeCurrent() - open_time) / 86400 gives a reasonable calendar approximation for multi-day positions, but a position that opens at 23:58 will cross its first rollover in minutes, and the 86400-second step will not capture that accurately.
For the purpose of hold decisions on swing trades, this level of precision is generally acceptable. If you need exact rollover counts, compare the position's open time against each broker rollover boundary explicitly.
Simplified Swap Estimation Helper
In practice, the implementation can be wrapped into a small helper that estimates the expected swap over the remaining holding period. The simplified version below shows the core idea: read the daily swap, count effective rollover days, and multiply the result by position volume.
1double ExpectedSwapForPosition(string symbol,
2 int direction,
3 double lots,
4 int hold_days)
5{
6 double daily_swap = DailySwapInAccountCurrency(symbol, direction);
7
8 int triple_days = 0;
9 datetime start = TimeCurrent();
10
11 for(int d = 0; d < hold_days; d++)
12 {
13 MqlDateTime dt;
14 TimeToStruct(start + d * 86400, dt);
15
16 if(dt.day_of_week == 3)
17 triple_days++;
18 }
19
20 double effective_days = hold_days + triple_days * 2.0;
21 return daily_swap * effective_days * lots;
22}The full version should additionally handle broker-specific swap modes, unavailable quotes, different triple-swap schedules, and volume limits.
Sanity-Checking the Output
Before using these functions in a live EA, verify their output against the swap values shown in the MetaTrader 5 symbol specification window. Right-click any symbol in Market Watch, select Specification, and look for Swap Long and Swap Short.
The figures there are what the broker publishes as the raw rates. Run DailySwapInAccountCurrency() on the same symbol in a test script and compare.
Small discrepancies are normal due to rounding and currency conversion timing; large ones suggest the wrong swap mode is being handled.
The SwapVerify script gives you a useful sanity check at the symbol level, but it is not a full validation against real position history. For a rigorous test, hold a position on your broker for at least one overnight rollover, then check the actual swap amount credited or debited in your account history.
Compare that figure to what DailySwapInAccountCurrency() returned for that symbol and direction on the same day. If the numbers differ by more than a small rounding margin, investigate whether the broker applies non-standard weekday multipliers or uses a swap mode the function does not yet support.
1//+------------------------------------------------------------------+
2//| SwapVerify.mq5 |
3//| test script |
4//+------------------------------------------------------------------+
5#include "SwapTools.mqh"
6
7//+------------------------------------------------------------------+
8//| Script program start function |
9//+------------------------------------------------------------------+
10void OnStart()
11 {
12 string sym = _Symbol;
13 double long_swap = DailySwapInAccountCurrency(sym,1);
14 double short_swap = DailySwapInAccountCurrency(sym,-1);
15 int mode = (int)SymbolInfoInteger(sym,SYMBOL_SWAP_MODE);
16
17 Print("=== SWAP VERIFICATION ===");
18 Print("Symbol : ",sym);
19 Print("Swap mode : ",mode);
20 Print("Long (raw): ",SymbolInfoDouble(sym,SYMBOL_SWAP_LONG));
21 Print("Short (raw): ",SymbolInfoDouble(sym,SYMBOL_SWAP_SHORT));
22
23 Print("Long /lot/day (account CCY): ",DoubleToString(long_swap,4));
24 Print("Short /lot/day (account CCY): ",DoubleToString(short_swap,4));
25
26 double expected_swap = ExpectedSwapForPosition(sym,1,0.1,5);
27 Print("5-day long carry at 0.1 lot: ",DoubleToString(expected_swap,2));
28 }
29//+------------------------------------------------------------------+Figures 1 and 2 below illustrate the verification process, contrasting the broker's asset specifications with the runtime calculations from the script. Figure 1 displays the broker's official Swap Long and Swap Short rates within the MetaTrader 5 contract specification window for the AUDJPY pair, while Figure 2 shows the corresponding programmatic output generated by the SwapVerify.mq5 script.
Fig. 1: The MetaTrader 5 Symbol Specification window for the AUDJPY currency pair, displaying a swap type evaluated "In points" with a long swap rate of 5.6 and a short swap rate of -12.8.
Fig. 2: Terminal Toolbox log output from the SwapVerify script verifying the runtime extraction of the AUDJPY swap configuration and its corresponding per-lot calculations in the account currency.
Known Limitations
The implementation in this article is designed to be readable and practical for the most common retail trading scenarios. Before using it in a live system, be aware of the following limitations:
-
Modes not fully implemented: SYMBOL_SWAP_MODE_CURRENCY_MARGIN, SYMBOL_SWAP_MODE_REOPEN_CURRENT, and SYMBOL_SWAP_MODE_REOPEN_BID return 0.0 with a log warning. If your broker uses any of these modes, the functions will not give accurate results until you add the appropriate conversion or reopening logic.
-
Currency conversion in mode 1: When the swap is denominated in a currency that differs from your account currency (common with SWAP_MODE_MONEY on cross pairs), the raw swap value needs to be multiplied by the appropriate exchange rate. The current implementation returns the raw value, which is exact only when the base currency matches your account currency.
-
Triple swap day: The Wednesday heuristic works for most forex pairs on retail brokers, but some instruments have their triple-swap day on a different weekday. MetaTrader 5 exposes per-day multipliers through SYMBOL_SWAP_SUNDAY to SYMBOL_SWAP_SATURDAY. If you trade CFDs, metals, or exotic pairs, check these values for your specific broker before relying on the Wednesday assumption.
-
SYMBOL_SWAP_MODE_INTEREST_OPEN and SYMBOL_SWAP_MODE_INTEREST_CURRENT use bid as a price proxy:Both interest modes divide by 360, following the MetaTrader 5 banking-day convention. For INTEREST_OPEN specifically, the exact formula requires the position's open price, which is unavailable at the symbol level. The function substitutes the current bid, which introduces a proportional error on positions that have moved significantly from their entry price.
-
hold_days is a calendar count, not a rollover count: Dividing elapsed seconds by 86400 gives a reasonable estimate for multi-day positions, but it does not precisely account for positions opened near the rollover cutoff time.
Section 3: The AUDJPY Example—Numbers in Practice
Why AUDJPY Is the Right Pair to Demonstrate This
AUDJPY is a classic example of a carry trade pair. Australia's Reserve Bank has maintained a history of issuing higher interest rates than the Bank of Japan, which keeps JPY interest rates near zero.
That differential between them means long AUDJPY positions typically earn positive swap credit, while short positions pay the differential in the opposite direction. The spread between long and short swap rates on AUDJPY is notably wider than other major crosses, making it ideal for swap-sensitive trading models.
AUDJPY is a volatile pair. It moves with risk sentiment, rallies during periods of global risk appetite, and sells off sharply when markets get nervous.
That combination of high carry and elevated volatility makes it an interesting case study: the swap income is meaningful, but so is the price risk. An EA needs to understand both to make sensible hold decisions.
The Numbers at 0.1 Lot
Table 2 below shows the AUDJPY carry calculation step-by-step at the current approximate rates. The swap values used here are illustrative and will differ by broker; the point is to show the arithmetic that DailySwapInAccountCurrency() performs internally.
| Parameter | Value | Unit | Notes | Source |
|---|---|---|---|---|
| AUDJPY swap long | +12.0 | pts/lot/day | Positive carry — long AUD vs JPY | SYMBOL_SWAP_LONG |
| AUDJPY swap short | -21.0 | pts/lot/day | Negative — cost to hold short | SYMBOL_SWAP_SHORT |
| Contract size | 100,000 | units | Standard lot size (1.0 lot) | SYMBOL_TRADE_CONTRACT_SIZE |
| Point value (JPY) | 0.001 | JPY per unit | 1 point = 3rd decimal on AUDJPY | SYMBOL_POINT |
| 0.1 lot swap/day | 120 | JPY/day | (12.0 pts * 0.001) * 10,000 units | Calculated |
| USD equivalent | approx. $0.77 | USD/day | Converted from JPY using current USDJPY rate | Runtime calculation |
Table 2: AUDJPY carry calculation at 0.1 lot for a USD account. Broker swap rates vary; always retrieve live values via SYMBOL_SWAP_LONG rather than hardcoding. At these rates, a 7-day hold (including triple-swap Wednesday) earns approximately $5.40 in swap credit, recovering roughly 28% of the risk on a typical 30-pip stop-loss.
1 lot may not sound like much in isolation, but consider it relative to the position's risk. 645 at current exchange rates).
After seven days of positive carry, which includes the triple-swap Wednesday, the position has recovered roughly 28% of the total risk purely from interest. On instruments with wider differentials, this mathematical cushion becomes even more significant for swing trading systems.
Section 4: The Hold Decision Function
The Logic Behind IsWorthHolding()
The hold decision function answers a specific question: given what this position is currently losing on price, will the swap income expected over the remaining holding window recover enough of that loss to justify staying in the trade? It does not replace the exit logic entirely.
A position that has hit its stop-loss will be closed. But for positions in moderate drawdown that have not triggered a hard exit, the function provides a carry-adjusted view of whether patience is economically rational.
The function takes four inputs: the position ticket number, the maximum number of days the EA is willing to hold any trade, the percentage of floating loss that swap needs to cover for the hold to be justified, and optionally the current timestamp. It reads the position's floating P&L, computes the expected swap over the remaining window, and returns true if that swap figure meets the coverage threshold.
1//+------------------------------------------------------------------+
2//| Evaluates if position drawdown is justified by carry income |
3//+------------------------------------------------------------------+
4bool IsWorthHolding(ulong ticket, int max_hold_days, double coverage_pct = 40.0)
5 {
6 if(!PositionSelectByTicket(ticket))
7 return(false);
8
9 double total_profit = PositionGetDouble(POSITION_PROFIT);
10 double accrued_swap = PositionGetDouble(POSITION_SWAP);
11 double lots = PositionGetDouble(POSITION_VOLUME);
12 string sym = PositionGetString(POSITION_SYMBOL);
13 datetime open_t = (datetime)PositionGetInteger(POSITION_TIME);
14 int pos_type = (int)PositionGetInteger(POSITION_TYPE);
15 int direction = (pos_type == POSITION_TYPE_BUY) ? 1 : -1;
16
17//--- Isolate the price-driven P/L by removing already-accrued swap.
18//--- POSITION_PROFIT includes swap credits, so using it directly would
19//--- cause double-counting when we add future expected swap below.
20 double price_pnl = total_profit - accrued_swap;
21
22//--- If the trade is already profitable on price alone, keep it.
23 if(price_pnl >= 0)
24 return(true);
25
26//--- days_held is a calendar approximation. Rollover count may differ
27//--- by 1 depending on open time relative to the broker rollover cutoff.
28 int days_held = (int)((TimeCurrent() - open_t) / 86400);
29 int days_left = max_hold_days - days_held;
30
31 if(days_left <= 0)
32 return(false);
33
34 double future_swap = ExpectedSwapForPosition(sym, direction, lots, days_left);
35
36//--- Total carry = what has already been earned + what is still expected.
37//--- This gives a complete picture of the position's carry contribution.
38 double total_carry = future_swap + accrued_swap;
39
40 if(total_carry <= 0)
41 return(false);
42
43 double coverage_req = MathAbs(price_pnl) * (coverage_pct / 100.0);
44 return(total_carry >= coverage_req);
45 }The coverage_pct parameter is the key tuning variable. Setting it at 100 means the EA will only hold if the remaining swap is expected to fully recover the floating loss, a conservative stance.
Setting it at 40 means the EA holds as long as carry covers 40% of the loss, leaving the remainder to price recovery. The right value depends on the strategy's win rate and how confident the trader is in the original trade thesis.
Wiring Logic into the Expert Loop
To maintain a clean and modular architecture, the IsWorthHolding() function is integrated into a dedicated management loop rather than being buried directly inside OnTick(). The typical pattern involves executing "hard exits" first, such as technical signal reversals or hard stop-losses. Only when these primary conditions are not met, and the position remains in a floating drawdown, does the EA call IsWorthHolding() to determine if the projected carry income justifies maintaining the market exposure.
1//+------------------------------------------------------------------+
2//| Modular function to manage open positions based on carry logic |
3//+------------------------------------------------------------------+
4void ManageExistingPositions()
5{
6 //--- Run carry checks once per H1 bar, not every tick.
7 //--- Swap values only change at rollover, so tick-level evaluation
8 //--- adds no useful information and wastes processing time.
9 static datetime last_bar_time = 0;
10 datetime current_bar = iTime(_Symbol, PERIOD_H1, 0);
11 if(current_bar == last_bar_time)
12 return;
13 last_bar_time = current_bar;
14
15 //--- Loop backwards through open positions to allow safe mid-loop closing
16 for(int i = PositionsTotal() - 1; i >= 0; i--)
17 {
18 ulong ticket = PositionGetTicket(i);
19 if(!PositionSelectByTicket(ticket))
20 continue;
21
22 //--- Filter for the current chart symbol only
23 if(PositionGetString(POSITION_SYMBOL) != _Symbol)
24 continue;
25
26 double profit = PositionGetDouble(POSITION_PROFIT);
27
28 //--- 1. PRIMARY EXITS (Hard Rules)
29 //--- Check for a technical reversal first. If the trend has flipped,
30 //--- no amount of carry income justifies staying in the trade.
31 if(IsTrendReversed())
32 {
33 if(!trade.PositionClose(ticket))
34 PrintFormat("ManageExistingPositions: failed to close ticket #%I64u on reversal — error %d",
35 ticket, GetLastError());
36 continue;
37 }
38
39 //--- 2. CARRY-AWARE SECONDARY LOGIC
40 //--- Only evaluate carry when the position is underwater.
41 //--- If profit is flat or positive, there is nothing to offset.
42 if(profit < 0)
43 {
44 if(!IsWorthHolding(ticket, InpHoldDaysLimit, InpSwapCoverage))
45 {
46 //--- Yield is insufficient to justify the current drawdown — close the position
47 if(!trade.PositionClose(ticket))
48 PrintFormat("ManageExistingPositions: failed to close ticket #%I64u on carry exit — error %d",
49 ticket, GetLastError());
50 else
51 PrintFormat("Carry-Aware: Closed ticket #%I64u — swap income does not cover drawdown.",
52 ticket);
53 }
54 else
55 {
56 //--- Yield justifies continuing to hold — log once per bar per ticket
57 PrintFormat("Carry-Aware: Holding ticket #%I64u — carry coverage is sufficient.",
58 ticket);
59 }
60 }
61 }
62}
63//+------------------------------------------------------------------+
64//| Expert tick function |
65//+------------------------------------------------------------------+
66void OnTick()
67{
68 //--- Evaluate carry logic and manage open positions
69 ManageExistingPositions();
70
71 //--- Entry logic placeholder
72 //--- CheckEntrySignals();
73}
74//+------------------------------------------------------------------+Note: IsWorthHolding() should never override a hard stop-loss. If the position has reached the EA's predefined stop-loss, close it regardless of what the swap function returns. The carry logic is for positions sitting in moderate drawdown that have not triggered a hard exit, not for rationalizing holding a position that has moved clearly against the trade.
Section 5: Carry-Adjusted Position Sizing
Sizing a Carry Trade Entry
The second practical application is position sizing. Rather than sizing purely on risk percentage, the CarryAdjustedLotSize() function adjusts the volume to ensure the expected swap meets a target contribution.
A word of caution before using this function in a live strategy: increasing lot size to capture a larger swap credit also increases your stop-loss exposure by the same factor. 1-lot position.
Carry income is steady but small; an adverse price move can erase multiple days of swap credit in minutes. Use carry-adjusted sizing as a fine-tuning tool within your normal risk model, not as a reason to exceed it.
The carry-adjusted lot should always be validated against your maximum allowable risk before submission.
1//+------------------------------------------------------------------+
2//| Returns a carry-adjusted lot size. |
3//| |
4//| IMPORTANT: Increasing lot size to chase swap income also |
5//| increases stop-loss risk by the same proportion. Carry should |
6//| never be the primary reason to take on more risk than your |
7//| normal position sizing allows. Use this function to fine-tune |
8//| within your existing risk limits, not to override them. |
9//| The carry-adjusted lot is always capped at base_lots unless swap |
10//| income meaningfully justifies a larger size under your model. |
11//+------------------------------------------------------------------+
12double CarryAdjustedLotSize(string symbol, int direction,
13 double risk_money, int hold_days,
14 double target_pct = 50.0,
15 double base_lots = 0.1)
16 {
17 double daily_rate = DailySwapInAccountCurrency(symbol, direction);
18
19//--- If swap is zero or negative, no carry adjustment is warranted.
20 if(daily_rate <= 0)
21 return(base_lots);
22
23 double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
24 double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
25 double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
26
27//--- Count Wednesdays for accurate total-day estimate
28 datetime now = TimeCurrent();
29 double total_days = (double)hold_days;
30 for(int d = 0; d < hold_days; d++)
31 {
32 MqlDateTime dt;
33 TimeToStruct(now + (datetime)(d * 86400), dt);
34 if(dt.day_of_week == 3)
35 total_days += 2.0;
36 }
37
38//--- Target carry is expressed as a percentage of the ORIGINAL risk_money.
39//--- We solve for the lot size at which carry == target_pct% of risk_money.
40//--- Per-lot carry over the window:
41 double carry_per_lot = daily_rate * total_days;
42 if(carry_per_lot <= 0)
43 return(base_lots);
44
45 double target_carry = risk_money * (target_pct / 100.0);
46 double required_lots = target_carry / carry_per_lot;
47
48//--- NOTE: If required_lots > base_lots, verify that your risk model
49//--- permits the larger position. The stop-loss monetary risk scales
50//--- linearly with lot size. Always apply your normal risk cap first.
51 if(step > 0)
52 required_lots = MathFloor(required_lots / step) * step; // floor, not round
53
54 if(required_lots < min_lot)
55 required_lots = min_lot;
56 if(required_lots > max_lot)
57 required_lots = max_lot;
58
59//--- Log for transparency
60 PrintFormat("CarryAdjustedLotSize: %s dir=%d carry_per_lot=%.4f "
61 "target=%.2f required=%.2f base=%.2f",
62 symbol, direction, carry_per_lot, target_carry,
63 required_lots, base_lots);
64
65 return(required_lots);
66 }The function prints its calculation to the log on each call, which is useful during initial testing. In production, that Print() line can be removed or wrapped in a debug flag. The key output is the returned lot size, which can be passed directly to the position-opening call in the EA's entry logic.
A Worked Example
At 0.1 lot, AUDJPY long at $7.70/lot daily swap over 7 effective days (5 calendar days, one Wednesday) yields $5.39 carry — 21.5% of a $25 stop-loss risk. Scaling lot size raises carry income but raises stop-loss risk equally, so coverage percentage does not improve. CarryAdjustedLotSize() should be used to fine-tune sizing within an existing risk budget, not beyond it.
Section 6: Practical Considerations and Caveats
Swap Rates Change
Swap rates move when central bank policy changes. Any EA using carry-based hold decisions must read rates at runtime rather than relying on hardcoded values.
The functions here always call SymbolInfoDouble() live. It is also worth periodically cross-checking the output of SwapVerify against actual account history, since some brokers apply fees on top of the raw interest differential that do not appear in the symbol specification.
Negative Carry Pairs
DailySwapInAccountCurrency() returns a negative value on sides where the trader pays swap. IsWorthHolding() returns false immediately in that case. CarryAdjustedLotSize() returns baseLots unchanged when no positive carry is available, falling back to standard risk-based sizing.
Backtesting Swap-Aware Logic
MetaTrader 5 applies current broker swap rates in the Strategy Tester rather than historical ones, which makes carry-based backtest results unreliable without intervention. The included MockSwapForTesting() function injects a fixed synthetic rate for AUDJPY long positions to allow controlled testing of CarryAdjustedLotSize() and IsWorthHolding() behavior.
It is not a historical reconstruction. For accurate backtesting, the practical approach is to load swap values from a CSV file keyed by date and symbol, and route the production functions through a date-aware wrapper that selects the appropriate rate.
Section 7: Performance Results: Baseline vs. Carry-Aware
Testing the CarryDemo.mq5 on AUDJPY H1 over a 24-month window from January 2021 to December 2022 shows one illustrative configuration where carry-aware logic outperformed the baseline. In this particular test, the carry-aware version held through several short-term drawdowns that the baseline version exited early, and the accumulated swap income partially offset those drawdowns during flat price periods.
To quantify the difference, the EA was run twice over the same window with identical settings, changing only the InpCarryAware input between runs. Table 3 below summarises the four key metrics from each run.
Table 3: Baseline vs. Carry-Aware performance on AUDJPY H1 (2021–2022)
| Metric | Baseline (Carry-Unaware) | Carry-Aware |
|---|---|---|
| Net Profit | -$144.04 | +$659.28 |
| Max Drawdown | 10.28% ($1,113.55) | 1.28% ($128.81) |
| Profit Factor | 0.98 | 4.45 |
| Total Trades | 786 | 17 |
The baseline EA traded 786 times, exiting on every MA reversal and time limit without any consideration of swap income. It ended the period with a net loss of $144.04, a profit factor below 1.0, and a maximum drawdown of just over 10%. The high trade count reflects how frequently the mechanical exit rules fired, each time crystallizing a small loss before carry income had a chance to accumulate.
The carry-aware EA made 17 trades over the same period. When a reversal signal fired or the position moved into drawdown, the EA first evaluated whether accumulated and expected swap income was sufficient to cover the loss before deciding to close.
In most cases it was, and the position was held. The lower trade count is not necessarily missed opportunity.
The carry filter is selective by design and closes a position only when swap income cannot justify holding it longer. In a positive-carry environment like long AUDJPY, that condition is rarely met, which naturally produces fewer closed trades.
28%.
The equity curves below illustrate how these numbers translate visually across the two runs.
Fig. 3: Equity curve of the baseline strategy tester run operating strictly on technical exits with carry-aware logic disabled. Without yield awareness to buffer holding costs, the strategy experiences frequent equity erosion during flat market regimes and premature exits on minor retracements, preventing it from riding out temporary noise to capture larger price recoveries.
Fig. 4: Equity curve of the strategy tester run with carry-aware logic enabled. With yield awareness fully active, the strategy uses continuous daily swap income as a financial buffer to filter out short-term market noise, preventing premature technical exits and allowing positions to be safely held through flat regimes or temporary drawdowns.
These results should be treated as a proof-of-concept demonstration, not a performance benchmark. 3), covers a single instrument and timeframe, and does not account for broker spreads, slippage, or commission costs.
Strategy logic that is built around carry thresholds can be sensitive to those thresholds, and results on other instruments or time periods may differ significantly. Before deploying this approach live, run it against your broker's actual historical swap data across multiple instruments and market conditions.
Conclusion
Swap rates are not a footnote. On carry-positive pairs held for multiple days, they are a material contributor to trade returns that most automated strategies simply leave on the table. The gap between what an EA knows about a position and what the position is actually doing grows every time a rollover happens, because the carry income is accumulating in the account while the EA's logic remains completely unaware of it.
The tools built in this article close that gap. DailySwapInAccountCurrency() gives any EA a practical daily carry estimate in account currency, covering the most common retail broker swap modes with explicit handling for unsupported ones.
ExpectedSwapForPosition() extends that to a full holding window, using a Wednesday-based triple swap heuristic that works for most forex pairs on standard retail brokers, with notes on where broker-specific multipliers may apply. IsWorthHolding() puts the carry figure to work in a hold-versus-close decision that treats accumulated carry as real income offsetting floating losses.
CarryAdjustedLotSize() takes the same logic to the entry decision, sizing positions so that the expected carry contribution meets a defined fraction of the initial risk.
None of these functions change how a strategy enters the market. They add a layer of awareness about what happens after entry on pairs where what happens after entry includes a daily interest credit that compounds meaningfully over multi-day holds. For any EA running on AUDJPY, NZDUSD, or similar carry-positive instruments, that awareness is not optional — it is part of what makes the position management logic complete.
Programs used in the article:
| # | Name | Type | Description |
|---|---|---|---|
| 1 | SwapTools.mqh | Include file | Include file containing MockSwapForTesting(), DailySwapInAccountCurrency(), ExpectedSwapForPosition(), IsWorthHolding(), and CarryAdjustedLotSize(). Drop into MQL5/Include/ and reference with a single #include line. MockSwapForTesting() is a backtesting stub only and should not be called in live logic. |
| 2 | SwapVerify.mq5 | Script | Verification script that prints current swap rates, daily swap in account currency, and 5-day expected carry to the Experts log. Run on any symbol before deploying swap-aware logic. |
| 3 | CarryDemo.mq5 | Demo EA | Demonstration EA showing all four functions integrated into a simple AUDJPY long strategy with configurable hold-day limit, swap coverage threshold, and carry-adjusted lot sizing. |
Attached files |
SwapTools.mqh(12.55 KB)
SwapVerify.mq5(1.35 KB)
CarryDemo.mq5(7.16 KB)
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Writer and developer
Other articles by this author
- Keeping Memory Across Restarts: EA State Persistence Using Binary Files in MQL5
- News Filtering with MetaTrader 5 Economic Calendar and CSV Fallback
- Leak-Free Multi-Timeframe Engine with Closed-Bar Reads in MQL5
Beyond GARCH (Part V): Fitting the Multifractal Spectrum in MQL5
This article builds the Spectrum Fitter: from tau(q) we compute f(alpha) with a discrete Legendre transform, then fit Normal, Binomial, Poisson, and Gamma spectra under box constraints using BLEIC. The best model by SSE is selected, and its parameters (eg, alpha min, alpha max or alpha_0, gamma) become the cascade inputs for multifractal simulation.
Exchange Market Algorithm (EMA)
The article presents a detailed analysis of the Exchange Market Algorithm (EMA) inspired by the behavior of stock market traders. The algorithm simulates stock trading, where market participants with varying levels of success employ different strategies to maximize profits.
Neural Networks in Trading: Hierarchical Skill Discovery for Adaptive Agent Behavior (HiSSD)
In this article, we explore the HiSSD framework, which combines hierarchical learning and multi-agent approaches to create adaptive systems. We examine in detail how this innovative methodology helps uncover hidden patterns in financial markets and optimize trading strategies in decentralized environments.
Developing a Multi-Currency Expert Advisor (Part 28): Adding a Position Closing Manager
When running multiple strategies in parallel, you may want to periodically close all open positions and start the strategies over again. The existing code only allows this behavior to be implemented through manual intervention. Let's try to automate this part.
[
\
A unified portal with global market news and analytics to help you trade smarter\
\
This website uses cookies. Learn more about our Cookies Policy.
You are missing trading opportunities:
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
RegistrationLog in
latin characters without spaces
a password will be sent to this email
An error occurred
You agree to website policy and terms of use
If you do not have an account, please register
Allow the use of cookies to log in to the MQL5.com website.
Please enable the necessary setting in your browser, otherwise you will not be able to log in.
