Lesson 1.2.4 · 60 min
Forex: pairs, pips, lots, rollover and CFDs
The currency market has no central exchange and no close: it trades from Sunday to Friday and every open position is rolled over each night. This lesson uses two years of hourly EURUSD to measure what it costs to trade and to hold a position, derives forward points from interest rates and explains the rules ESMA imposes on CFDs for retail clients.
- Module
- 1.2 · Asset classes
- Duration
- 60 min
- Prerequisites
- 1.1.5 · Costs, settlement, leverage and margin, 1.2.3 · Futures: specifications, margin, roll and contango, 0.4.4 · Files (CSV, JSON, YAML, TOML) and dates with time zones
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
A market without an exchange
The currency market (forex, FX) is above all an OTC market: banks, market makers and electronic platforms quote to each other and to their clients without a central exchange (lesson 1.1.1). There is no official price and no close: it trades continuously from Sunday afternoon to Friday afternoon, following the sun through Sydney, Tokyo, London and New York. The trading day is taken to be the one that ends at 17:00 New York time.
A pair is written with two ISO codes: EURUSD is the price of one euro (the base currency) in dollars (the
quote currency). If EURUSD trades at 1.17000, one euro is worth 1.17 dollars. Buying EURUSD means buying euros and
selling dollars. The course data are two years of a synthetic EURUSD with its bid and ask every hour:
import numpy as np
import pandas as pd
fx = pd.read_parquet("eurusd_1h.parquet")
print(len(fx), "hours;", fx["ts"].min(), "to", fx["ts"].max())
fx.head(3)12552 hours; 2024-01-01 00:00:00+00:00 to 2025-12-31 23:00:00+00:00
ts bid ask mid
0 2024-01-01 00:00:00+00:00 1.10379 1.10389 1.103840
1 2024-01-01 01:00:00+00:00 1.10416 1.10421 1.104185
2 2024-01-01 02:00:00+00:00 1.10405 1.10412 1.104085The timestamps are in UTC. The quote has five decimals: the fifth is a fraction of a pip.
Pips, lots and what a move is worth
A pip is the conventional unit of movement: in EURUSD, the fourth decimal (0.0001). The fifth, which almost every provider quotes today, is a tenth of a pip. The position is measured in lots of the base currency; the convention among retail brokers is:
| Lot | Units of the base | Value of one pip in EURUSD |
|---|---|---|
| Standard | 100,000 EUR | 10 USD |
| Mini | 10,000 EUR | 1 USD |
| Micro | 1,000 EUR | 0.10 USD |
The pip value comes out in the quote currency (USD in EURUSD). A euro account converts it at the current rate:
PIP = 0.0001
def valor_pip(unidades, precio):
"""Value of one EURUSD pip in USD and in EUR."""
usd = unidades * PIP
return usd, usd / precio
precio = fx.set_index("ts").loc["2025-06-02 15:00", "mid"]
print(f"EURUSD mid: {precio:.5f}")
for nombre, unidades in [("standard", 100_000), ("mini", 10_000),
("micro", 1_000)]:
usd, eur = valor_pip(unidades, precio)
print(f"{nombre:9s} {usd:7.2f} USD {eur:7.2f} EUR")EURUSD mid: 1.17162
standard 10.00 USD 8.54 EUR
mini 1.00 USD 0.85 EUR
micro 0.10 USD 0.09 EURWith EURUSD at 1.17162, one pip on a standard lot is 10 USD, 8.54 EUR. A move of 100 pips (from 1.1700 to 1.1800, 0.85%) is 1,000 USD per lot. In pairs where the account is in neither the base nor the quote currency (EURGBP for a dollar account, for example), the pip value is converted with a third exchange rate.
The spread is also measured in pips:
fx["horquilla_pips"] = (fx["ask"] - fx["bid"]) / PIP
print(fx["horquilla_pips"].describe().round(2))
media = fx["horquilla_pips"].mean()
print(f"Cost of crossing the average spread, 1 lot: "
f"{media * 10:.2f} USD")count 12552.00
mean 0.46
std 0.51
min 0.10
25% 0.20
50% 0.30
75% 0.50
max 5.60
Name: horquilla_pips, dtype: float64
Cost of crossing the average spread, 1 lot: 4.56 USDThe median is 0.3 pips and the mean rises to 0.46 because of a few very expensive hours (the maximum, 5.6 pips). Crossing the average spread with a standard lot costs 4.56 USD. A system that trades many times a day pays that cost on every entry and every exit (lesson 1.1.5), and does not pay it equally at every hour.
The spread through the day
The market never sleeps, but its liquidity does change. Grouping by New York time, which is the reference for the currency day:
ny = fx["ts"].dt.tz_convert("America/New_York")
fx["hora_ny"] = ny.dt.hour
por_hora = fx.groupby("hora_ny")["horquilla_pips"].mean()
print(por_hora.round(2).to_string())hora_ny
0 0.61
1 0.52
2 0.28
3 0.21
4 0.21
5 0.21
6 0.21
7 0.19
8 0.15
9 0.15
10 0.15
11 0.21
12 0.30
13 0.31
14 0.31
15 0.31
16 0.31
17 2.57
18 0.61
19 0.63
20 0.62
21 0.62
22 0.63
23 0.62The figure at the top plots that table. Three segments:
- From 18:00 to 01:00 New York time (Asian session): about 0.6 pips. Asia is trading, but the major euro and dollar centers are closed.
- From 02:00 to 16:00: London opens and the spread falls to 0.2; in the London–New York overlap (08:00 to 10:00) it reaches its minimum, 0.15 pips. After the London close it rises to 0.3.
- 17:00: 2.57 pips on average, seventeen times the minimum. It is the rollover hour: the currency day ends, open positions are rolled to the next value date and liquidity providers pull or widen their prices for a few minutes. On Sunday, that same hour is also the first of the week.
For a system that runs in UTC (as almost all do), there is a trap: the rollover hour is fixed in New York and moves in UTC with US daylight saving time.
fx["hora_utc"] = fx["ts"].dt.hour
fx["mes"] = fx["ts"].dt.month
cara = (fx[fx["ts"].dt.year == 2025]
.groupby(["mes", "hora_utc"])["horquilla_pips"].mean()
.groupby("mes").idxmax().str[1])
cara.rename("hora_utc_mas_cara")mes
1 22
2 22
3 21
4 21
5 21
6 21
7 21
8 21
9 21
10 21
11 22
12 22
Name: hora_utc_mas_cara, dtype: int64From November to February, the most expensive hour is 22:00 UTC; from March to October, 21:00 UTC. March and November are mixed months: for part of the month the rollover hour falls on one UTC hour and for part on the other, and the table only shows the dominant one. A “do not trade at 21:00 UTC” filter fails four months a year (lesson 0.4.4).
Spot, forward, swap and future
A spot currency trade does not settle instantly. According to the BIS definition in its triennial survey, spot is an exchange of two currencies at a price agreed today with a value (delivery) date within two business days; the market convention for EURUSD is a value date two business days out (T+2). Anything that settles later is a forward (outright forward).
Mermaid source
flowchart TB L[Monday: trade] --> X[Wednesday: value] R[Rollover 17:00 NY] --> S[Tom-next swap] S --> J[Value one day later]
- Forward: an exchange on a date after spot, at a price fixed today. OTC, customized.
- FX swap: a spot trade and a forward trade in the opposite direction, at the same time. It is the tool used to roll positions: the tom-next pushes the value date back one day.
- Currency future: a standardized forward on an exchange (the CME, for example), with quarterly expiries, margin and daily settlement (lesson 1.2.3).
The forward price is not a forecast: it is set by arbitrage. Someone holding euros can invest them at the euro rate, or convert them to dollars, invest them at the dollar rate and reverse the exchange forward. Both have to give the same result (covered interest rate parity). With rates on an Act/360 basis:
tipos = pd.read_csv("tipos_corto_eur_usd.csv", parse_dates=["fecha"],
index_col="fecha")
i_eur, i_usd = tipos.loc["2025-06-02", ["tipo_eur", "tipo_usd"]] / 100
print(f"i_EUR = {i_eur:.3%} i_USD = {i_usd:.3%} S = {precio:.5f}")
def forward(S, i_cot, i_base, dias):
return S * (1 + i_cot * dias / 360) / (1 + i_base * dias / 360)
plazos = {"1 week": 7, "1 month": 30, "3 months": 91, "1 year": 365}
pd.DataFrame({
"forward": {k: forward(precio, i_usd, i_eur, d)
for k, d in plazos.items()},
"puntos": {k: (forward(precio, i_usd, i_eur, d) - precio) / PIP
for k, d in plazos.items()},
}).round({"forward": 5, "puntos": 1})i_EUR = 2.165% i_USD = 4.293% S = 1.17162
forward puntos
1 week 1.17210 4.8
1 month 1.17369 20.7
3 months 1.17788 62.7
1 year 1.19635 247.4With the dollar rate 2.1 points above the euro rate, the euro trades forward at a premium: 4.8 pips at one week, 62.7 at three months and 247.4 at one year. Whoever buys euros forward pays more than spot, and that is offset by what they give up by not earning dollar interest. (To keep it simple, the example uses the overnight rate for every tenor; in practice each tenor has its own rate.)
Swaps and rollover: the cost of holding the position
A spot position at a retail broker is never delivered: every day, at 17:00 New York time, the broker rolls it to the next value date with a tom-next swap. The economic effect is that of parity: whoever is long EURUSD holds euros (earns the euro rate) and owes dollars (pays the dollar rate). That differential is the rollover or swap, and it is charged or credited every night.
A calendar detail makes it triple once a week. Wednesday's roll moves the value date from Friday to the following Monday (spot is two business days), so it covers three days of interest:
cierre = (fx.set_index("ts")["mid"].tz_convert("America/New_York")
.between_time("16:00", "16:00"))
cierre.index = cierre.index.normalize().tz_localize(None)
dias = cierre.loc["2025"].to_frame("S").join(tipos / 100)
dias = dias[dias.index.dayofweek < 5]
# days of interest for each roll: 3 for Wednesday's
dias["n"] = np.where(dias.index.dayofweek == 2, 3, 1)
nominal = 100_000 # EUR
dif = dias["tipo_eur"] - dias["tipo_usd"]
dias["swap_usd"] = nominal * dias["S"] * dif * dias["n"] / 360
print(f"Rolls: {len(dias)},"
f" days of interest: {dias['n'].sum()}")
print(f"Average EUR − USD differential: {100 * dif.mean():.2f} points")
print(f"Rollover for the year, long: {dias['swap_usd'].sum():,.0f} USD")
dias["swap_usd"].loc["2025-06-02":"2025-06-06"].round(2)Rolls: 261, days of interest: 367
Average EUR − USD differential: -2.01 points
Rollover for the year, long: -2,360 USD
ts
2025-06-02 -6.89
2025-06-03 -6.90
2025-06-04 -20.78
2025-06-05 -6.89
2025-06-06 -6.94
Name: swap_usd, dtype: float64A one-lot long in EURUSD during 2025 pays 2,360 USD in theoretical rollover, before the broker's markup: the average differential was 2.01 points against it. 261 rolls add up to 367 days of interest, and each Wednesday costs triple: in the week of June 2, 6.89 USD per night and 20.78 on Wednesday. A short would receive the same, minus the broker's markup, which in practice reduces what is received and increases what is paid. A trend strategy that holds positions for weeks has to include this cost; a carry strategy (buying the high-rate currency) lives off it.
CFDs and ESMA's limits
Many retail traders do not trade spot currencies but CFDs (contracts for difference): a contract with the broker that pays the price difference of the underlying, with margin and rollover, without delivery. Since 2018, ESMA has restricted their sale to retail clients in the EU, and the CNMV (the Spanish securities regulator) has kept the same measures in Spain since August 1, 2019 (Resolution of June 27, 2019). The rules:
| Underlying | Minimum initial margin | Maximum leverage |
|---|---|---|
| Pairs made of two of USD, EUR, JPY, GBP, CAD, CHF | 3.33% | 30:1 |
| Other pairs, gold and major indices | 5% | 20:1 |
| Other commodities and indices | 10% | 10:1 |
| Stocks and other underlyings | 20% | 5:1 |
| Crypto-assets | 50% | 2:1 |
Besides the margin, the provider has to:
- Close positions when the account's funds plus unrealized gains fall below half of the initial margin posted for all open CFDs.
- Provide negative balance protection: the client cannot lose more than the funds in their CFD account.
- Include a standardized risk warning with the percentage of its retail accounts that lose money. The standard text of the CNMV resolution cites the supervisors' studies: between 74% and 89% of retail investors lose money when trading CFDs.
Mermaid source
stateDiagram-v2 [*] --> Open Open --> Warning: low margin Warning --> Open: adds funds Warning --> Closed: funds < 50% Open --> Closed: client closes Closed --> [*]
With the minimum margin, how much of a move closes a position? If all the capital is used as margin, the account reaches 50% of the margin when it loses half, that is, half of 3.33% of the notional:
capital = 1_000 # EUR
margen = 1 / 30 # 3.33% of the notional
nominal = capital / margen
cierre_pct = 0.5 * margen
pips = cierre_pct * precio / PIP
print(f"Maximum notional: {nominal:,.0f} EUR ({nominal / 1e5:.2f} lots)")
print(f"Closed out at a loss of {0.5 * capital:,.0f} EUR:"
f" a {cierre_pct:.2%} adverse move, about {pips:.0f} pips")Maximum notional: 30,000 EUR (0.30 lots)
Closed out at a loss of 500 EUR: a 1.67% adverse move, about 195 pipsWith 1,000 EUR you can open 30,000 EUR of notional (0.30 lots). The position is closed when it loses 500 EUR, with an adverse move of 1.67%, about 195 pips. Is that a lot? The data show how often EURUSD moves that much against a long within a few days:
mid = fx.set_index("ts")["mid"]
for horas, nombre in [(24, "one day"), (120, "five days")]:
peor = mid[::-1].rolling(horas, min_periods=horas).min()[::-1]
caida = (peor / mid - 1).dropna()
print(f"Drop of more than {cierre_pct:.2%} within {nombre}: "
f"{(caida < -cierre_pct).mean():.1%} of hours")Drop of more than 1.67% within one day: 0.5% of hours
Drop of more than 1.67% within five days: 9.0% of hoursAt maximum leverage, a long opened at any given hour hits the margin close-out within the next 24 hours in 0.5% of cases, and within the next five days in 9.0%: almost one in ten, and that with a low-volatility currency and without counting the spread or the rollover. ESMA's limit does not make a position safe: it only makes it less fragile than with no limit. Sensible leverage is decided by position sizing (lesson 0.1.2), not by the legal maximum.
What this changes in your data
- The price has two sides. A currency backtest buys at the ask and sells at the bid; with the mid price, it gives away the spread on every trade.
- The spread depends on the hour. A daily average hides that the rollover hour costs ten times more than the London–New York overlap.
- UTC hours move. The rollover is at 17:00 New York time; in UTC, at 21:00 or 22:00 depending on the time of year.
- Rollover is a cash flow. Holding a position costs or earns the rate differential, triple on Wednesdays. A backtest that omits it favors strategies that buy the low-rate currency.
- There is no official daily “close”. Daily currency bars depend on the cutoff time the provider uses (usually 17:00 New York time); two providers with different cutoffs produce different candles.
Exercises
- Compute the theoretical rollover of a one-lot short in EURUSD during 2024. Does it receive or pay, and how much?
Solution
d24 = cierre.loc["2024"].to_frame("S").join(tipos / 100)
d24 = d24[d24.index.dayofweek < 5]
d24["n"] = np.where(d24.index.dayofweek == 2, 3, 1)
dif24 = d24["tipo_usd"] - d24["tipo_eur"] # short: holds USD, owes EUR
swap24 = 100_000 * d24["S"] * dif24 * d24["n"] / 360
print(f"Average USD − EUR differential: {100 * dif24.mean():.2f} points")
print(f"Rollover for the year, short: {swap24.sum():,.0f} USD")Average USD − EUR differential: 1.48 points
Rollover for the year, short: 1,618 USDA short in EURUSD holds dollars and owes euros: it earns the dollar rate and pays the euro rate. In 2024 the differential was about 1.5 points in its favor and the short would have received more than 1,600 USD in theory per lot. A real broker would pay less, because it deducts its markup, but the sign is the same.
- Compute the average spread by day of the week (in New York time). Which day is the most expensive, and why?
Solution
fx["dia_ny"] = ny.dt.day_name()
fx.groupby("dia_ny")["horquilla_pips"].mean().round(2).sort_values()dia_ny
Friday 0.27
Monday 0.45
Wednesday 0.45
Thursday 0.46
Tuesday 0.46
Sunday 0.89
Name: horquilla_pips, dtype: float64Sunday is by far the most expensive: it has only seven market hours (from 17:00 to 24:00 New York time), all of them in the Asian session, and the first, the weekly open, at more than 2.5 pips. Weekdays look alike; Friday closes at 17:00 and has no rollover hour, so its average is lower.
- A friend shows you a backtest of a currency strategy with a Sharpe of 2 that holds long EURUSD positions for ten days on average, using mid-price data and no rollover. Which three costs are missing, and in which direction would they change the result?
Solution
The missing costs are the spread (buying at the ask and selling at the bid: about 0.5 pips on average per round trip, more at certain hours), the broker's commission if it charges one, and the rollover: with the dollar rate above the euro rate in 2024 and 2025, a long pays every night and triple on Wednesdays, about 2 points a year on the notional. All three worsen the result; the rollover weighs heavily on long positions held for days, and the spread, on strategies that enter and exit often. If it also uses CFDs, ESMA's maximum leverage and the 50% margin close-out can close positions that the backtest keeps open.
Each answer is folded under its question
Self-assessment
EURUSD trades at 1.17000. What is one pip worth on a standard lot of 100,000 EUR, in USD and in EUR?
- A1 USD and 0.85 EUR
- B10 USD and 8.55 EUR
- C100 USD and 85.47 EUR
- D10 EUR and 11.70 USD
Show answer
Answer B. 10 USD and 8.55 EUR
One EURUSD pip is 0.0001 USD per euro: 100,000 × 0.0001 = 10 USD. For a euro account, 10 / 1.17 = 8.55 EUR. The pip value always comes out in the quote currency (the second one in the pair).
Why does the EURUSD spread widen so much at 17:00 New York time?
- ABecause the New York Stock Exchange closes
- BBecause it is when US macro data is released
- CBecause it is the rollover hour: the value date changes, banks reprice and liquidity drops for a few minutes
- DBecause Tokyo opens
Show answer
Answer C. Because it is the rollover hour: the value date changes, banks reprice and liquidity drops for a few minutes
At 17:00 New York time the currency market's “day” ends: open positions are rolled to the next value date. Liquidity providers pull or widen their prices. In the course data the average spread at that hour is 2.57 pips, against 0.15 in the best hours.
The euro overnight rate is 2.165% and the dollar rate 4.293%. How does one-year EURUSD trade compared with spot?
- ACheaper, because the euro yields less
- BThe same: the forward is the market's forecast
- CIt cannot be known without an ECB forecast
- DHigher: the low-rate currency (the euro) trades at a forward premium, and the difference offsets the rate differential
Show answer
Answer D. Higher: the low-rate currency (the euro) trades at a forward premium, and the difference offsets the rate differential
By covered interest rate parity, F = S·(1 + i_USD·d/360)/(1 + i_EUR·d/360). With the higher dollar rate, F > S: positive forward points. Otherwise you could borrow in euros, convert, invest in dollars and hedge forward for a riskless gain.
A retail account in the EU opens a CFD on EURUSD with ESMA's minimum margin. When must the provider close the position?
- AWhen the funds plus unrealized gains fall below half of the initial margin posted
- BWhen the balance reaches zero
- CWhen the price moves 50%
- DNever: the client decides
Show answer
Answer A. When the funds plus unrealized gains fall below half of the initial margin posted
It is the margin close-out rule at 50%. With a margin of 3.33% of the notional, half is 1.67 points of the notional: an adverse move of a little over 1.6% closes the position. In addition, the client cannot lose more than what is in the account (negative balance protection).
A long EURUSD position is held open from Tuesday to Thursday. How many days of rollover does it pay or receive?
- ATwo
- BOne
- CFour: two nights plus the Wednesday triple, which covers the weekend
- DNone if it is closed before Friday
Show answer
Answer C. Four: two nights plus the Wednesday triple, which covers the weekend
Tuesday to Wednesday: one day. Wednesday to Thursday: three days, because the value date jumps from Friday to Monday (spot is two business days). In total, four days of interest differential.
Key points
- In EURUSD, the euro is the base currency and the dollar the quote currency: one pip (0.0001) on a lot of 100,000 EUR is worth 10 USD, and it is converted to the account currency at the current rate.
- The spread depends on the time of day: 0.15 pips in the London–New York overlap, 0.6 in the Asian session and 2.57 at 17:00 New York time, the rollover hour. In UTC that hour shifts with daylight saving time.
- Spot settles in two business days; forward, later, at the price set by interest rate parity. Forward points are not a forecast: they are the interest differential.
- Holding a position from one day to the next costs or earns the rate differential (the rollover), triple on Wednesday night. Long EURUSD in 2025, the differential worked against you.
- In the EU, a retail CFD on major currencies has a maximum leverage of 30 to 1 (3.33% margin), mandatory close-out at 50% of margin and negative balance protection.