Lesson 3.3.3 · 75 min

Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5

A portfolio in evros holding stocks in dollars has two risks: the stocks and the dollar. This lesson separates them, measures how much a one-month forward hedge removes, how much it costs and how much cash it demands each month, and breaks down the risk of a portfolio with five assets in four currencies. Then it sends the orders: the real code for OANDA v20, Interactive Brokers (ib_async) and MetaTrader 5, and a local server that mimics OANDA to actually run price bounds, rate limits and retries without duplicate orders.

Fig. — Volatility in evros of a US equity index and of a 10-year bond by the share of the dollar hedged with one-month forwards (2010-2025)
Level 3 lessons
  1. 3.1 · Futures in depth

    1. 3.1.1 Main futures contracts
    2. 3.1.2 Futures curve: basis, cost of carry, contango and roll yield
    3. 3.1.3 Roll management, margins, SPAN and implied leverage
    4. 3.1.4 Diversified futures systems: Carver's approach
  2. 3.2 · Futures spreads

    1. 3.2.1 Calendar spreads, butterflies and condors
    2. 3.2.2 Intercommodity spreads: crack, spark and crush
    3. 3.2.3 Rate and index spreads: 2s10s, NOB, NQ/ES and Europe versus the U.S.
    4. 3.2.4 Hedge ratios: by notional, by DV01 and by volatility
    5. 3.2.5 Spread data and backtesting: listed, synthetic and legged
    6. 3.2.6 Seasonality of spreads: multi-year analysis, patterns and traps
  3. 3.3 · Advanced forex

    1. 3.3.1 FX microstructure: OTC, ECN, last look and sessions
    2. 3.3.2 Carry and crash risk, momentum and value in currencies; baskets
    3. 3.3.3 Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5
  4. 3.4 · Option theory and pricing

    1. 3.4.1 Payoffs, put-call parity and arbitrage bounds
    2. 3.4.2 Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo
    3. 3.4.3 Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm
    4. 3.4.4 Implied volatility: Newton-Raphson, Brent, py_vollib and QuantLib
    5. 3.4.5 Smile, skew and term structure; volatility surface with SVI
    6. 3.4.6 Realized vs implied volatility; volatility risk premium; VIX and VSTOXX
    7. 3.4.7 Advanced models: Heston, SABR and local volatility
  5. 3.5 · Option structures in Python

    1. 3.5.1 Directional and vertical structures: call, put, covered call, collar and spreads
    2. 3.5.2 Volatility and range structures: straddles, strangles, condors and butterflies
    3. 3.5.3 Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard
    4. 3.5.4 A custom tool: payoff, aggregate Greeks and scenarios
    5. 3.5.5 Management: adjustments, rolls, early closes, early assignment and pin risk
  6. 3.6 · Systematic volatility and options strategies

    1. 3.6.1 Systematic premium selling: put-write, strangles and their tail risk
    2. 3.6.2 Delta hedging, gamma scalping and dispersion
    3. 3.6.3 VIX and VSTOXX strategies: volatility futures and ETPs
    4. 3.6.4 Options backtesting: data, fills and event adjustments
  7. 3.7 · Crypto derivatives and specific strategies

    1. 3.7.1 Perpetuals: funding mechanics, mark price and liquidations
    2. 3.7.2 Funding arbitrage, basis with futures and cross-exchange arbitrage
    3. 3.7.3 Crypto options, on-chain data and DeFi
    4. 3.7.4 ccxt for trading on multiple exchanges
    5. 3.7.5 MiCA: the EU framework for crypto-assets and their service providers (CASPs)

Two risks in one position

An investor whose currency is the evro (QEV, the course's fictitious euro) buys a US stock index. What they earn in evros depends on two things: what the index does in dollars and what the dollar does against the evro. If the index rises 10% and the dollar falls 10%, in evros they earn almost nothing:

1+rQEV=(1+rlocal) (1+rfx)⟹rQEV=rlocal+rfx+rlocal rfx1 + r_{\text{QEV}} = (1 + r_{\text{local}})\,(1 + r_{\text{fx}}) \quad\Longrightarrow\quad r_{\text{QEV}} = r_{\text{local}} + r_{\text{fx}} + r_{\text{local}}\,r_{\text{fx}}

with rfxr_{\text{fx}} the return of the dollar measured in evros. The third term, the cross term, is small over a month and grows with the horizon. The variance is, approximately, that of the two parts plus twice their covariance: if the two sources are weakly correlated, the risks add up almost in full.

The data: futuros3_indices_contado.csv has the fictitious indices of module 3.1 in their currency (RVA mimics the S&P 500, RVB the Nasdaq-100, RVE the Euro Stoxx 50) with their dividend yield, from 2010 to 2025, and fx3_diario.parquet the twelve currencies of lesson 3.3.2. Everything is synthetic. Monthly total return: the change in the index plus one twelfth of the dividend.

IN [01]python
import numpy as np
import pandas as pd

ix = pd.read_csv("futuros3_indices_contado.csv", parse_dates=["fecha"])
P = ix.pivot(index="fecha", columns="indice", values="contado")
q = ix.pivot(index="fecha", columns="indice",
             values="rentabilidad_dividendo")
fin = P.index.to_series().groupby(P.index.to_period("M")).max()
R = (P.loc[fin].pct_change() + q.loc[fin].shift(1) / 1200).iloc[1:]

fx = pd.read_parquet("fx3_diario.parquet")
def mensual(col):
    t = fx.pivot(index="fecha", columns="divisa", values=col)
    return t.reindex(P.index).ffill().loc[fin]

X = mensual("usd_por_unidad")             # USD per unit of currency
r_usd = (X["QEV"].shift(1) / X["QEV"] - 1).iloc[1:]  # USD in QEV
r_qev = (1 + R["RVA"]) * (1 + r_usd) - 1

def vol(x):
    return 100 * x.std() * np.sqrt(12)

pd.Series({"vol_local": vol(R["RVA"]), "vol_dolar": vol(r_usd),
           "correlacion": R["RVA"].corr(r_usd),
           "vol_en_qev": vol(r_qev)}).round(2)
OUT [01]The RVA index seen from the evro
vol_local      22.14
vol_dolar      10.15
correlacion     0.07
vol_en_qev     24.90
dtype: float64

The index has a local volatility of 22.1% and the dollar, measured in evros, of 10.2%. With a correlation of only 0.07, in evros the position has a volatility of 24.9%: almost three points more. The sign of the correlation matters. If the dollar rose when stocks fall (as a safe haven does), the currency risk would cushion the falls; here they barely move together.

Hedging with a one-month forward

The classic hedge is to sell forward the dollars one holds. At each month end, the dollar value of the position is sold one month forward, at the forward rate; the next month the contract is settled and another one is opened. For each evro invested, with XX in USD per evro and FF the one-month forward:

hedge result=Xt−1Ft−1−Xt−1Xt\text{hedge result} = \frac{X_{t-1}}{F_{t-1}} - \frac{X_{t-1}}{X_t}

the first term is what is received for the dollars sold forward and the second what they are worth at spot. With a hedge ratio hh between 0 and 1, the hedged return is the unhedged one plus hh times that result. The hedge is not perfect: it hedges the value at the start of the month, not what the position gains or loses during the month.

For comparison, a second asset: a 10-year Treasury bond approximated with the curve in tipos3_curva.csv (a par bond with a semiannual coupon equal to the previous month's yield, repriced with this month's yield; an approximation, not a bond index).

IN [02]python
F = mensual("forward_1m")["QEV"]                  # USD per QEV
cobertura = (X["QEV"].shift(1) / F.shift(1)
             - X["QEV"].shift(1) / X["QEV"]).iloc[1:]

curva = pd.read_csv("tipos3_curva.csv", parse_dates=["fecha"])
y = curva.set_index("fecha")["usd_10a"].loc[fin] / 100

def precio_par(y, cupon, anos):
    """Price of a bond with a semiannual coupon, face value 1."""
    d = (1 + y / 2) ** (-2 * anos)
    return cupon / y * (1 - d) + d

bono = (precio_par(y, y.shift(1), 10 - 1 / 12) - 1
        + y.shift(1) / 12).iloc[1:]
activos = {"acciones": R["RVA"], "bono": bono}
filas = {}
for nombre, r_loc in activos.items():
    sin = (1 + r_loc) * (1 + r_usd) - 1
    for h in (0, 0.25, 0.5, 0.75, 1.0):
        filas[(nombre, h)] = vol(sin + h * cobertura)
    c = np.cov(sin, cobertura)
    filas[(nombre, "h_minvar")] = -c[0, 1] / c[1, 1]
    filas[(nombre, "vol_local")] = vol(r_loc)
pd.Series(filas).unstack(0).round(2)
OUT [02]Volatility by hedge ratio: stocks and bond
           acciones   bono
0             24.90  14.63
0.25          23.80  12.92
0.5           22.93  11.52
0.75          22.32  10.56
1.0           21.98  10.16
h_minvar       1.16   1.04
vol_local     22.14  10.15

This is the header figure. For the stocks, fully hedging the dollar lowers the volatility from 24.9% to 22.0%: slightly below the local one (22.1%), because the correlation is slightly positive. For the bond the hedge matters much more: from 14.6% to 10.2%, the local volatility. The reason is one of proportions: the dollar has a volatility of 10%, the same as the bond and less than half that of the stocks. In foreign fixed income the currency is almost half the risk; in stocks it is diluted.

The minimum-variance ratio (the slope of the unhedged return on the hedge result) comes out at 1.16 for the stocks and 1.04 for the bond: somewhat more than a full hedge, because of the positive correlation between the asset and the dollar. With another sample it could come out at 0.8. Hedging more than 100% is betting against the dollar; almost all investment policies set a ratio (0, 50% or 100%) for governance reasons, not estimation ones.

What it costs: the interest rate differential

Through covered parity (lesson 3.3.2), the forward carries the interest rate differential inside it. Whoever sells dollars forward when the dollar pays more interest than the evro receives fewer evros than at spot: they pay the differential. The cost of each month is Xt/Ft−1X_t / F_t - 1, known when the hedge is opened:

IN [03]python
coste_mes = (X["QEV"] / F - 1).iloc[:-1]       # known when hedging
por_ano = 100 * coste_mes.groupby(coste_mes.index.year).sum()
# negative = the hedge costs; positive = it earns
print(por_ano.round(2).to_frame("carry_cob_pct").T.to_string())
print()
peor = cobertura.idxmin()
print(f"worst monthly settlement: {100 * cobertura.min():.2f} % "
      f"({peor:%Y-%m})")
doce = cobertura.rolling(12).sum()
print(f"worst 12-month sum: {100 * doce.min():.2f} % "
      f"(through {doce.idxmin():%Y-%m})")
OUT [03]Annual cost of the hedge and cash flows
fecha          2010  2011  2012  2013  2014  2015  2016  2017  2018  2019  2020  2021  2022  2023  2024  2025
carry_cob_pct  0.32  0.56  0.18  0.06 -0.01 -0.27 -0.76 -1.37 -2.18 -2.54 -0.63 -0.48 -1.48 -1.59 -1.19 -1.66

worst monthly settlement: -7.06 % (2021-01)
worst 12-month sum: -24.64 % (through 2021-11)
Fig. — Annual result of hedging the dollar with one-month forwards for an evro investor: the rate differential implied by the forward

From 2010 to 2014 evro and dollar rates were similar and the hedge cost almost nothing (or earned a little). Since 2015, with the dollar paying more, it costs every year: 2.54% in 2019, 1.59% in 2023, 1.66% in 2025 (in the table, with a negative sign). Against the 22% volatility of the stocks it is little; for a bond, whose yield is a few points, it can be a large part of what it earns. The hedge is also changed by the covered parity basis: with the −15 bp basis that the evro has in the data, getting dollars forward is somewhat more expensive than the deposits say (lesson 3.3.2).

The second figure is the one backtests forget: the cash flows. Every month the forward is settled. If the dollar appreciates, the hedge loses, and that loss has to be paid in cash that month, even though the equivalent gain on the stocks in dollars has not been realized. In the worst month of the data the settlement is −7.1% of the portfolio, and in the worst twelve consecutive months (through November 2021), −24.6%. A portfolio that does not have that cash has to sell assets to pay for the hedge, often at the worst moment.

A portfolio in four currencies

A more realistic case: a portfolio in evros with US stocks (RVA 35%, RVB 15%), evro-area stocks (RVE 30%) and cash in pounds (QLB, 10%) and yen (QYN, 10%). The cash earns the one-month deposit rate of its currency. The currency exposure is: dollar 50%, pound 10%, yen 10%; the remaining 30% is already in evros.

The risk is broken down with the usual decomposition (lesson 2.6.1): the contribution of each source to the total variance is its weight times its covariance with the portfolio, ck=wk cov(rk,rp)c_k = w_k\,\text{cov}(r_k, r_p), and the contributions add up to the variance. Here the sources are the local returns of the assets and those of each currency against the evro, weighted by their exposure.

IN [04]python
dep = mensual("tipo_deposito_1m")
en_qev = X.div(X["QEV"], axis=0)            # QEV per unit
fx_qev = (en_qev / en_qev.shift(1) - 1).iloc[1:]
fx_qev["USD"] = r_usd
fuentes = pd.DataFrame({
    "RVA": R["RVA"], "RVB": R["RVB"], "RVE": R["RVE"],
    "dep_QLB": dep["QLB"].shift(1).iloc[1:] / 1200,
    "dep_QYN": dep["QYN"].shift(1).iloc[1:] / 1200,
    "fx_USD": fx_qev["USD"], "fx_QLB": fx_qev["QLB"],
    "fx_QYN": fx_qev["QYN"]}).dropna()
w = pd.Series({"RVA": .35, "RVB": .15, "RVE": .30, "dep_QLB": .10,
               "dep_QYN": .10, "fx_USD": .50, "fx_QLB": .10,
               "fx_QYN": .10})
cartera = fuentes @ w
contrib = w * fuentes.apply(lambda c: c.cov(cartera))
pct = 100 * contrib / cartera.var()
print(f"portfolio volatility: {vol(cartera):.2f} %")
pct.round(1).to_frame("riesgo_pct").T
OUT [04]Risk contribution of assets and currencies
portfolio volatility: 19.71 %
             RVA   RVB   RVE  dep_QLB  dep_QYN  fx_USD  fx_QLB  fx_QYN
riesgo_pct  35.9  19.5  31.8      0.0     -0.0    10.5     1.2     1.1
Fig. — Contribution of each asset and each currency to the variance of an evro portfolio with US and evro-area equities and cash in pounds and yen

The portfolio has a volatility of 19.7% in evros. The stocks take 87% of the variance: RVA 35.9%, RVE 31.8%, RVB 19.5%. The dollar contributes 10.5% and the pound and the yen, 1.2% and 1.1%; the deposits, nothing (their return barely varies). The decomposition is linear: it adds up the local returns and the weighted currency returns, without the cross term, which over a month is small. In the data model the yen is a safe-haven currency that appreciates in crises, but with a 10% weight its effect does not offset its own noise: it adds risk, if only a little. What to hedge?

IN [05]python
Fu = mensual("forward_1m")
par_usd = fx.groupby("divisa")["par"].first().str.startswith("USD")
Fu.loc[:, par_usd] = 1 / Fu.loc[:, par_usd]     # USD per unit
f_qev = Fu.div(Fu["QEV"], axis=0)               # QEV per unit
f_qev["USD"] = 1 / Fu["QEV"]
s_qev = en_qev.copy()
s_qev["USD"] = 1 / X["QEV"]
# sell forward 1 QEV of exposure to each currency, per QEV
cob = ((f_qev.shift(1) - s_qev) / s_qev.shift(1)).iloc[1:]
cob = cob[["USD", "QLB", "QYN"]].loc[fuentes.index]
expo = {"USD": .50, "QLB": .10, "QYN": .10}
casos = {"unhedged": [], "USD only": ["USD"],
         "USD and QLB": ["USD", "QLB"], "all": ["USD", "QLB", "QYN"]}
res = {}
for nom, cubre in casos.items():
    r_c = cartera + sum(expo[c] * cob[c] for c in cubre)
    res[nom] = {"vol_pct": vol(r_c), "media_pct": 1200 * r_c.mean(),
                "peor_mes_pct": 100 * r_c.min()}
pd.DataFrame(res).T.round(2)
OUT [05]What happens when hedging each currency
             vol_pct  media_pct  peor_mes_pct
unhedged       19.71      11.59        -25.28
USD only       18.23      10.94        -24.60
USD and QLB    18.16      10.90        -24.81
all            18.16      10.94        -24.77

Hedging the dollar lowers the volatility from 19.71% to 18.23% and costs 0.65 points of average return a year (from 11.59% to 10.94%): it is the interest rate differential of the previous section. Also hedging the pound lowers it to 18.16%, and adding the yen does not change it. On return, hedging the pound costs 0.04 points and hedging the yen, a currency with a lower rate than the evro, gives them back: the investor collects the differential. The worst month barely changes (from −25.3% to −24.6%) because the stocks dominate it. For this portfolio, the decision that matters is the dollar one; the pound and yen one is almost indifferent in risk. It is a decision per currency and with data, not a general rule: with more weight in foreign fixed income, the hedge would matter much more (previous section).

Hedging with futures

Instead of forwards, currency futures on an exchange can be used. DQE, in the data, mimics the CME Euro FX future: it is quoted in USD per evro, with a multiplier of 125,000 evros per contract (the specifications of the real contract, in lesson 3.1.1). An evro investor with assets in dollars is long the dollar and short the evro: to hedge, they buy evro futures. The number of contracts is the exposure in evros divided by the notional in evros of one contract:

IN [06]python
esp = pd.read_csv("futuros3_especificaciones.csv").set_index("mercado")
curvas = pd.read_parquet("futuros3_curvas.parquet",
                         filters=[("mercado", "==", "DQE")])
venc = pd.read_csv("futuros3_contratos.csv",
                   parse_dates=["vencimiento"])
dia = curvas[curvas["fecha"] == curvas["fecha"].max()].merge(
    venc[["contrato", "vencimiento"]], on="contrato")
front = dia.sort_values("vencimiento").iloc[0]  # the nearest one
mult = esp.loc["DQE", "multiplicador"]

capital_qev = 10_000_000
expo_usd = 0.50 * capital_qev * X["QEV"].iloc[-1]    # in USD
n_exacto = expo_usd / front["liquidacion"] / mult
n = round(n_exacto)
residuo_usd = expo_usd - n * mult * front["liquidacion"]
print(f"{front['contrato']}: {front['liquidacion']:.5f} USD per QEV")
print(f"exposure: {expo_usd:,.0f} USD; contracts: {n_exacto:.2f}"
      f" -> {n}")
print(f"unhedged residual: {residuo_usd:,.0f} USD")
print(f"initial margin: {n * esp.loc['DQE', 'margen_inicial']:,.0f}"
      " USD")
OUT [06]DQE contracts to hedge the dollar of a portfolio
DQEH26: 1.38165 USD per QEV
exposure: 6,884,270 USD; contracts: 39.86 -> 40
unhedged residual: -23,980 USD
initial margin: 136,000 USD

The dollar exposure, 6,884,270 USD, calls for 39.86 contracts of DQEH26 at 1.38165; with 40, 23,980 USD too many are hedged: only whole contracts can be bought, and the exposure changes every day with the value of the stocks. The initial margin, 136,000 USD, is posted in dollars. Three more differences from the forward:

  • The margin and the daily settlements are in dollars (lesson 3.1.3). Hedging the dollar needs a dollar account with cash for the settlements, which in turn has a small currency risk.
  • The future has to be rolled every quarter, with the cost of the roll (lesson 3.1.2); its basis reflects the same interest rate differential as the forward.
  • The counterparty is the clearing house, not a bank: no bilateral credit agreements, in exchange for daily margins.

A future hedges the notional of an exposure; its own gains and losses, it does not. That is why a future on a foreign index (an E-mini bought from Europe) only has currency risk on the margin and on the accumulated result, not on the whole notional: the notional has not been paid.

From exposure to order: three APIs

The hedge and currency positions are executed through a broker. The three most used from Python by individuals and small professionals have different architectures (lesson 1.3.6): OANDA is a direct REST API; IBKR goes through TWS or IB Gateway, a local program that keeps the session; MetaTrader 5 goes through a terminal installed on Windows, which is what talks to the broker's server.

SystemREST OANDA v20ib_asyncTWS or GatewayMetaTrader5MT5 terminal
Mermaid source
flowchart LR
  S[System] --> O[REST OANDA v20]
  S --> I[ib_async]
  I --> G[TWS or Gateway]
  S --> M[MetaTrader5]
  M --> T[MT5 terminal]

Each one measures the order differently, and mixing them up is the most expensive and silliest mistake one can make:

APIHow to request a purchase of 1 M EURUSDUnit
OANDA v20"instrument": "EUR_USD", "units": "1000000" (a sale, "-1000000")Units of the base currency, signed
IBKR (ib_async)Forex("EURUSD"), MarketOrder("BUY", 1_000_000)Quantity of the base currency and BUY/SELL action
MetaTrader 5"symbol": "EURUSD", "volume": 10.0, "type": ORDER_TYPE_BUYLots; the lot size is given by symbol_info().trade_contract_size

The three things to know before sending: which currency is the base of the pair (a dollar exposure against evros is a sale of EUR_USD, not a purchase of USD_EUR, which does not exist), the minimum size and the step (volume_min and volume_step in MT5) and the precision of the quantity (OANDA rejects units with more decimals than allowed, with UNITS_PRECISION_EXCEEDED). A function that translates it:

IN [07]python
def orden_cobertura(expo_usd, precio, tam_lote=100_000, paso=0.01):
    """Hedge expo_usd (long USD) by buying EURUSD: the base is
    the evro, so the quantity is expressed in evros."""
    evros = expo_usd / precio
    lotes = round(round(evros / tam_lote / paso) * paso, 2)
    return {
        "oanda": {"instrument": "EUR_USD", "units": str(round(evros))},
        "ibkr": ("BUY", round(evros)),
        "mt5": {"symbol": "EURUSD", "volume": lotes,
                "lotes_en_evros": round(lotes * tam_lote)},
    }

precio = 1.37692                       # ask when sending the order
for k, v in orden_cobertura(expo_usd, precio).items():
    print(f"{k:6s} {v}")
OUT [07]One exposure translated to each API
oanda  {'instrument': 'EUR_USD', 'units': '4999760'}
ibkr   ('BUY', 4999760)
mt5    {'symbol': 'EURUSD', 'volume': 50.0, 'lotes_en_evros': 5000000}

With the same exposure and a price of 1.37692 when sending (EURUSD plays here the role of the course's QEVUSD), OANDA asks for 4,999,760 units, IBKR the same quantity and MT5 50.00 lots, which are 5,000,000 evros: rounding to the 0.01-lot step buys 240 evros too many. An MT5 lot is 100,000 units at almost all currency brokers (the example in the symbol_info documentation shows trade_contract_size=100000.0, volume_min=0.01 and volume_step=0.01), but the broker sets it: it has to be read, not assumed.

OANDA v20: orders over REST

A market order in OANDA is a POST /v3/accounts/{accountID}/orders with a JSON body with an order field. According to the v20 documentation1Note 1 · OANDA, Order Endpoints, Order Definitions (MarketOrderRequest, LimitOrderRequest, OrderState) and Transaction Definitions (OrderFillTransaction, OrderCancelTransaction, OrderCancelReason), accessed on October 22, 2026.:

  • Positive units buy and negative ones sell; the timeInForce of a market order can only be FOK (fill or kill, the default) or IOC.
  • priceBound is the worst price at which the client accepts the fill. If the fill price exceeds it, the order is cancelled with the reason BOUNDS_VIOLATION.
  • clientExtensions carries an id, a tag and a comment from the client. The id makes it possible to look up the order later with GET /v3/accounts/{accountID}/orders/@{id}.
  • The 201 response brings orderCreateTransaction and, if the order was filled, orderFillTransaction (with fullVWAP, tradeOpened, halfSpreadCost…) or, if it was cancelled immediately, orderCancelTransaction with its reason. Prices and quantities arrive as text.
IN [08]oanda_orden.pypython
import json
import os
from urllib.request import Request, urlopen

BASE = "https://api-fxpractice.oanda.com"      # practice account
cuenta = os.environ["OANDA_ACCOUNT"]
orden = {"order": {
    "type": "MARKET", "instrument": "EUR_USD", "units": "4999760",
    "timeInForce": "FOK", "positionFill": "DEFAULT",
    "priceBound": "1.37740",                   # worst accepted price
    "clientExtensions": {"id": "cob-2025-12-31-usd",
                         "tag": "cobertura"}}}
req = Request(f"{BASE}/v3/accounts/{cuenta}/orders",
              data=json.dumps(orden).encode(), method="POST",
              headers={"Authorization":
                       f"Bearer {os.environ['OANDA_TOKEN']}",
                       "Content-Type": "application/json"})
with urlopen(req, timeout=5) as r:
    respuesta = json.load(r)
fill = respuesta.get("orderFillTransaction")
cancel = respuesta.get("orderCancelTransaction")

A local server that mimics OANDA

To actually run the client, with its retries and its errors, without an account or network, the next cell starts a local HTTP server in a thread (free port chosen by the system) that mimics the shape of those responses. It is not OANDA: it is a test bench with three programmed behaviors. The second request it receives returns a 429 (rate limit), an order tagged as lenta is recorded and filled, but its response takes longer than the client's timeout, and the price of each order comes from a fixed list, so that one of them exceeds its priceBound.

IN [09]python
import json
import socket
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import Request, urlopen

class Simulador:
    def __init__(self, precios):
        self.precios = list(precios)       # price of each fill
        self.ordenes = {}                  # client id -> order
        self.peticiones = 0
        self.lock = threading.Lock()

class Manejador(BaseHTTPRequestHandler):
    def log_message(self, *args):          # no logging to stderr
        pass

    def _json(self, codigo, cuerpo):
        datos = json.dumps(cuerpo).encode()
        self.send_response(codigo)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(datos)))
        self.end_headers()
        self.wfile.write(datos)

    def do_POST(self):
        sim = self.server.sim
        if self.headers.get("Authorization") != "Bearer prueba":
            return self._json(401, {"errorMessage": "Insufficient "
                                    "authorization"})
        largo = int(self.headers["Content-Length"])
        orden = json.loads(self.rfile.read(largo))["order"]
        with sim.lock:
            sim.peticiones += 1
            if sim.peticiones == 2:
                return self._json(429, {"errorMessage":
                                        "Too many requests"})
            oid = str(100 + len(sim.ordenes))
            precio = sim.precios.pop(0)
            unidades = float(orden["units"])
            limite = float(orden.get("priceBound", "nan"))
            fuera = (precio > limite if unidades > 0
                     else precio < limite)
            ext = orden.get("clientExtensions", {})
            sim.ordenes[ext["id"]] = {
                "id": oid, "state": "CANCELLED" if fuera else "FILLED",
                "clientExtensions": ext}
        cuerpo = {"orderCreateTransaction": {
            "id": oid, "type": "MARKET_ORDER",
            "instrument": orden["instrument"], "units": orden["units"]}}
        if fuera:
            cuerpo["orderCancelTransaction"] = {
                "type": "ORDER_CANCEL", "orderID": oid,
                "reason": "BOUNDS_VIOLATION"}
        else:
            cuerpo["orderFillTransaction"] = {
                "type": "ORDER_FILL", "orderID": oid,
                "units": orden["units"], "fullVWAP": f"{precio:.5f}",
                "tradeOpened": {"tradeID": str(int(oid) + 1),
                                "units": orden["units"],
                                "price": f"{precio:.5f}"}}
        if ext.get("tag") == "lenta":
            time.sleep(0.6)                # response that gets lost
        self._json(201, cuerpo)

    def do_GET(self):
        sim = self.server.sim
        cid = self.path.rsplit("/@", 1)[-1]
        with sim.lock:
            orden = sim.ordenes.get(cid)
        if orden is None:
            return self._json(404, {"errorMessage": "Order not found"})
        self._json(200, {"order": orden})

class Servidor(ThreadingHTTPServer):
    def handle_error(self, request, client_address):
        pass                    # the client that no longer waits: no trace

servidor = Servidor(("127.0.0.1", 0), Manejador)
servidor.sim = Simulador([1.37705, 1.37781, 1.37698, 1.37701])
threading.Thread(target=servidor.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{servidor.server_address[1]}"
print("simulator running:", BASE.startswith("http://127.0.0.1:"))
OUT [09]Local server with the shape of OANDA v20 responses
simulator running: True

The client does three things an order client should always do: on a 429, it waits an increasing amount of time and retries; if the connection times out with no response, it does not resend blindly, but looks up the order by its client ID, because it may have arrived and been filled; and it returns the response as is, so that the system decides what to do with a cancellation.

IN [10]python
CABECERAS = {"Authorization": "Bearer prueba",
             "Content-Type": "application/json"}

def consultar(cuenta, cid):
    url = f"{BASE}/v3/accounts/{cuenta}/orders/@{cid}"
    try:
        with urlopen(Request(url, headers=CABECERAS), timeout=1) as r:
            return json.load(r)["order"]
    except HTTPError as e:
        if e.code == 404:
            return None
        raise

def enviar(cuenta, orden, intentos=4, espera=0.05, timeout=0.3):
    url = f"{BASE}/v3/accounts/{cuenta}/orders"
    datos = json.dumps({"order": orden}).encode()
    cid = orden["clientExtensions"]["id"]
    for k in range(intentos):
        req = Request(url, data=datos, method="POST",
                      headers=CABECERAS)
        try:
            with urlopen(req, timeout=timeout) as r:
                return json.load(r)
        except HTTPError as e:
            if e.code != 429:
                raise
            print(f"  {cid}: 429, retry after {espera * 2**k:.2f} s")
            time.sleep(espera * 2**k)
        except (TimeoutError, socket.timeout):
            previa = consultar(cuenta, cid)
            print(f"  {cid}: no response; lookup: "
                  f"{previa['state'] if previa else 'does not exist'}")
            if previa is not None:
                return {"consultada": previa}
    raise RuntimeError(f"{cid}: no response after {intentos} attempts")

def orden_mercado(cid, unidades, limite, tag="cobertura"):
    return {"type": "MARKET", "instrument": "QEV_USD",
            "units": str(unidades), "timeInForce": "FOK",
            "priceBound": f"{limite:.5f}",
            "clientExtensions": {"id": cid, "tag": tag}}

def resumen_respuesta(resp):
    if "consultada" in resp:
        return f"recovered by lookup: {resp['consultada']['state']}"
    if "orderFillTransaction" in resp:
        t = resp["orderFillTransaction"]
        return f"filled {t['units']} at {t['fullVWAP']}"
    return "cancelled: " + resp["orderCancelTransaction"]["reason"]

pruebas = [("cob-1", 2_000_000, 1.37740, "cobertura"),
           ("cob-2", 1_000_000, 1.37740, "cobertura"),
           ("cob-3", 1_000_000, 1.37740, "lenta")]
for cid, u, lim, tag in pruebas:
    resp = enviar("001-001-1", orden_mercado(cid, u, lim, tag))
    print(cid, "->", resumen_respuesta(resp))
OUT [10]Client with retries and lookup by client ID
cob-1 -> filled 2000000 at 1.37705
  cob-2: 429, retry after 0.05 s
cob-2 -> cancelled: BOUNDS_VIOLATION
  cob-3: no response; lookup: FILLED
cob-3 -> recovered by lookup: FILLED

The first order is filled at 1.37705, within its bound of 1.37740. The second runs into the 429, waits 0.05 seconds and retries; on that retry the price has jumped to 1.37781, above the bound, and the simulator cancels it with BOUNDS_VIOLATION, as OANDA would. The client does not resend it on its own: a cancellation on price is a decision for the system (ask for a price again, widen the bound or wait), just like a last-look rejection in lesson 3.3.1. The third is filled on the server, but its response takes longer than the client's timeout; the client looks up @cob-3, sees that it is FILLED and does not duplicate it. Without that lookup, the retry would have opened a second position of one million.

201 with fill201 with cancel429retryno responseexistsdoes not existSentFilledCancelledWaitingLookup
Mermaid source
stateDiagram-v2
  [*] --> Sent
  Sent --> Filled: 201 with fill
  Sent --> Cancelled: 201 with cancel
  Sent --> Waiting: 429
  Waiting --> Sent: retry
  Sent --> Lookup: no response
  Lookup --> Filled: exists
  Lookup --> Sent: does not exist
  Filled --> [*]
  Cancelled --> [*]
IN [11]python
servidor.shutdown()
servidor.server_close()
for cid, o in servidor.sim.ordenes.items():
    print(cid, o["id"], o["state"])
print("POST requests received:", servidor.sim.peticiones)
OUT [11]What is left recorded on the server
cob-1 100 FILLED
cob-2 101 CANCELLED
cob-3 102 FILLED
POST requests received: 4

Three orders recorded and four requests received: the 429 one did not create any order. This is the bookkeeping a system has to be able to reconcile with the broker's at the end of the day: orders sent, accepted, cancelled and filled, each with its identifier.

Interactive Brokers with ib_async

In IBKR the order is sent by TWS or IB Gateway (lesson 1.3.6 for the connection and the ports). According to the ib_async documentation2Note 2 · ib_async: API, version 2.1.0 on PyPI, accessed on October 22, 2026., Forex(pair, exchange='IDEALPRO') defines the pair (base and quote from "EURUSD"), MarketOrder(action, totalQuantity) and LimitOrder(action, totalQuantity, lmtPrice) the orders, whatIfOrder returns the effect on commissions and margin without sending it, and placeOrder returns a Trade that updates itself with the status (trade.orderStatus.status), the fills (trade.fills) and a log; trade.isDone() says whether it has reached a final state (Filled, Cancelled, ApiCancelled or Inactive). The order's orderRef field is the client ID, which comes back in the fills.

IN [12]ibkr_orden_fx.pypython
from ib_async import IB, Forex, LimitOrder     # uv add ib_async

ib = IB()
ib.connect("127.0.0.1", 7497, clientId=7)      # TWS, paper account
par = Forex("EURUSD")
ib.qualifyContracts(par)

orden = LimitOrder("BUY", 4_999_760, 1.37740)
orden.orderRef = "cob-2025-12-31-usd"          # client ID
orden.tif = "IOC"
impacto = ib.whatIfOrder(par, orden)           # margin and commission
print(impacto.initMarginChange, impacto.commission)

trade = ib.placeOrder(par, orden)
while not trade.isDone():
    ib.sleep(0.2)                              # let the loop run
print(trade.orderStatus.status, trade.orderStatus.filled,
      trade.orderStatus.avgFillPrice)
for f in trade.fills:
    print(f.execution.execId, f.execution.shares, f.execution.price)
ib.disconnect()

In IBKR an IOC limit order plays the role of OANDA's priceBound: it fills what it can up to the limit price and cancels the rest. If the program loses the connection to TWS after sending, on reconnecting ib.openTrades() and ib.reqExecutions() say what happened, and the orderRef makes it possible to recognize one's own orders before resending anything.

MetaTrader 5: order_send and its return codes

In MT5 the order is a dictionary with the MqlTradeRequest structure, and the response an OrderSendResult with a retcode. According to the MQL5 documentation3Note 3 · MQL5, order_send, symbol_info, order properties (ORDER_FILLING_*) and return codes, accessed on October 22, 2026.: action is the type of operation (TRADE_ACTION_DEAL for a market order), volume is in lots, price is the requested price, deviation the maximum accepted deviation from it in points, type_filling the fill policy (ORDER_FILLING_FOK, ORDER_FILLING_IOC or ORDER_FILLING_RETURN, depending on what the symbol allows) and magic a number that identifies the sending system.

IN [13]mt5_orden_fx.pypython
import MetaTrader5 as mt5                      # Windows only

if not mt5.initialize():
    raise RuntimeError(mt5.last_error())
info = mt5.symbol_info("EURUSD")
lotes = round(4_999_760 / info.trade_contract_size
              / info.volume_step) * info.volume_step
tick = mt5.symbol_info_tick("EURUSD")
peticion = {
    "action": mt5.TRADE_ACTION_DEAL, "symbol": "EURUSD",
    "volume": lotes, "type": mt5.ORDER_TYPE_BUY, "price": tick.ask,
    "deviation": 5,                            # points
    "magic": 20251231, "comment": "cob-usd",
    "type_time": mt5.ORDER_TIME_GTC,
    "type_filling": mt5.ORDER_FILLING_IOC,
}
resultado = mt5.order_send(peticion)
print(resultado.retcode, resultado.deal, resultado.price)
mt5.shutdown()

The return codes are what the system decides on. A function that classifies them, with the values from the MQL5 table, can be tested without a terminal on simulated results:

IN [14]python
from collections import namedtuple

RETCODE = {10009: ("DONE", "done"),
           10010: ("DONE_PARTIAL", "partial: send the rest"),
           10004: ("REQUOTE", "new price and resend if it fits"),
           10020: ("PRICE_CHANGED", "new price and resend if it fits"),
           10021: ("PRICE_OFF", "no prices: wait"),
           10024: ("TOO_MANY_REQUESTS", "wait and retry"),
           10018: ("MARKET_CLOSED", "wait for the session"),
           10019: ("NO_MONEY", "do not retry: check margin"),
           10014: ("INVALID_VOLUME", "do not retry: check lots"),
           10006: ("REJECT", "do not retry without checking")}

Resultado = namedtuple("Resultado", "retcode volume price")
simulados = [Resultado(10004, 0.0, 0.0),
             Resultado(10009, 50.0, 1.37694),
             Resultado(10014, 0.0, 0.0)]
for res in simulados:
    nombre, accion = RETCODE.get(res.retcode, ("?", "check"))
    print(f"{res.retcode} TRADE_RETCODE_{nombre:<13s} -> {accion}")
OUT [14]What to do with each MT5 return code
10004 TRADE_RETCODE_REQUOTE       -> new price and resend if it fits
10009 TRADE_RETCODE_DONE          -> done
10014 TRADE_RETCODE_INVALID_VOLUME -> do not retry: check lots

The requote (10004) and the price change (10020) are MT5's way of saying the same as the last look of lesson 3.3.1 or OANDA's BOUNDS_VIOLATION: the requested price is no longer good. The difference between retrying or not is in the table: a price or rate error can be retried with care; a volume or margin error or a generic rejection cannot, because the retry will give the same error or, worse, a different one with the position half done.

What goes wrong when executing currencies

  • The base of the pair. Hedging dollars from the evro is buying EUR_USD. A flipped sign doubles the exposure instead of hedging it. It is worth checking the position after each order, not just the response.
  • Lots and units. 50 MT5 lots versus 4,999,760 OANDA units: a factor of 100,000 between one and the other.
  • Duplicates. Every order carries a client ID and is looked up before resending.
  • Rejections on price. Last look, BOUNDS_VIOLATION, requotes: some of the orders are not filled, and they are the ones that arrive when the price moves.
  • The time. The daily rollover at 17:00 New York time (lesson 1.2.4), with wide spreads, and the weekend close, during which a position cannot be adjusted. A hedge rolled at month end should do it away from those hours.
  • Cash flows from the hedge. The forward or the future is settled in cash: one has to have it, or sell assets to pay for it.
  • Rate limits. OANDA, IBKR and MT5 limit requests (lesson 1.3.6 for the OANDA and IBKR figures); a retry loop without increasing backoff ends up blocked. The state machine design from the site's research on execution helps keep every order always in a known state.

Exercises

  1. Repeat the hedge ratio table with RVE as the foreign asset for a dollar investor (that is, measure RVE in USD and hedge the evro). Is the hedge as useful as for the evro investor with RVA?
Solution
IN [15]python
r_qev_en_usd = (X["QEV"] / X["QEV"].shift(1) - 1).iloc[1:]
sin_usd = (1 + R["RVE"]) * (1 + r_qev_en_usd) - 1
# sell evros forward: receives F USD per QEV, worth X at spot
cob_usd = ((F.shift(1) - X["QEV"]) / X["QEV"].shift(1)).iloc[1:]
t = {h: vol(sin_usd + h * cob_usd) for h in (0, 0.5, 1.0)}
c = np.cov(sin_usd, cob_usd)
t["h_minvar"] = -c[0, 1] / c[1, 1]
t["vol_local"] = vol(R["RVE"])
pd.Series(t).round(2)
OUT [15]A dollar investor with evro-area stocks
0            24.38
0.5          23.25
1.0          23.34
h_minvar      0.71
vol_local    23.19
dtype: float64

For the dollar investor, RVE has a local volatility of 23.19% and an unhedged one of 24.38%. Hedging half leaves it at 23.25% and hedging fully, at 23.34%: slightly more than the half hedge. The minimum-variance ratio is 0.71, against the 1.16 of the evro investor with RVA. The case is not symmetric: what decides is the correlation of each index with the foreign currency seen from each side. A minimum-variance ratio below 1 indicates that, in the data, the evro seen from the dollar tends to move somewhat in the opposite direction to RVE and dampens its swings: removing it fully is not what reduces the variance the most. With RVA the opposite happens (the dollar rises somewhat with RVA, correlation of 0.07), and the ratio comes out above 1. In both cases the benefit of hedging is one to three points of volatility: moderate in stocks.

  1. In the four-currency portfolio, compute the risk contribution when the dollar and the pound are hedged (the recommended policy). What share of the risk is still currency?
Solution
IN [16]python
cubierta = cartera + expo["USD"] * cob["USD"] + expo["QLB"] * cob["QLB"]
f2 = fuentes.assign(cob_USD=cob["USD"], cob_QLB=cob["QLB"])
w2 = pd.concat([w, pd.Series({"cob_USD": .50, "cob_QLB": .10})])
contrib2 = w2 * f2.apply(lambda c: c.cov(cubierta))
pct2 = 100 * contrib2 / cubierta.var()
divisa = pct2[["fx_USD", "fx_QLB", "fx_QYN",
               "cob_USD", "cob_QLB"]].sum()
print(f"volatility: {vol(cubierta):.2f} %; currency: {divisa:.1f} %")
pct2.round(1).to_frame("riesgo_pct").T
OUT [16]Contributions with the dollar and the pound hedged
volatility: 18.16 %; currency: 0.1 %
             RVA   RVB   RVE  dep_QLB  dep_QYN  fx_USD  fx_QLB  fx_QYN  cob_USD  cob_QLB
riesgo_pct  41.5  22.2  36.1      0.0     -0.0     3.7     0.3     0.2     -3.7     -0.3

With the dollar and the pound hedged, the volatility drops to 18.16% and each currency and its hedge offset each other (3.7% against −3.7% for the dollar, 0.3% against −0.3% for the pound): what is left of currency risk is 0.1% of the variance, the unhedged yen plus the residuals. The portfolio's risk is now almost only that of the stocks: the hedging decision is made, and what is managed from there on is the equity.

  1. In the simulator, the client retries the 429s with a wait of 0.05 · 2^k seconds. Explain why an increasing wait is better than retrying right away and what you would add so that ten processes receiving the 429 at the same time do not collide again at the same instant.
Solution

A 429 says that the server is receiving more requests than it accepts. Retrying right away adds more requests exactly when there are too many, and prolongs the block; a wait that doubles on each attempt (0.05, 0.1, 0.2 s…) cuts the load quickly and reaches a rate the server accepts. If ten processes receive the 429 at the same time and all wait exactly the same, they collide again at the same time: a random part is added to the wait (jitter, e.g. multiplying it by a uniform number between 0.5 and 1.5) to spread out the retries. And if the server returns a header that indicates when to retry, it is respected over one's own formula. Lesson 1.4.1 has the same pattern for data downloads.

Footnotes

  1. OANDA, Order Endpoints, Order Definitions (MarketOrderRequest, LimitOrderRequest, OrderState) and Transaction Definitions (OrderFillTransaction, OrderCancelTransaction, OrderCancelReason), accessed on October 22, 2026. ↩

  2. ib_async: API, version 2.1.0 on PyPI, accessed on October 22, 2026. ↩

  3. MQL5, order_send, symbol_info, order properties (ORDER_FILLING_*) and return codes, accessed on October 22, 2026. ↩

Each answer is folded under its question

Self-assessment

  1. An investor in evros holds a US stock index with a local volatility of 22.1%, and the dollar, measured in evros, has one of 10.2%, with a correlation of 0.07 between the two. What is the volatility of the unhedged position?

    • A22.1%: the currency adds no risk
    • B32.3%: the volatilities add up
    • CAbout 24.9%: the variances add up almost in full because the correlation is low
    • D11.9%: the currency offsets
    Show answer

    Answer C. About 24.9%: the variances add up almost in full because the correlation is low

    With correlation ρ, the variance is σ²_local + σ²_fx + 2ρσ_localσ_fx (plus a small cross term): √(0.221² + 0.102² + 2·0.07·0.221·0.102) ≈ 0.249. The data measures 24.9%. Hedging fully leaves it at 22.0%.

  2. In the same case, hedging the dollar of a 10-year bond lowers its volatility from 14.6% to 10.2%; hedging the stock index, from 24.9% to 22.0%. Why does the hedge matter more for the bond?

    • ABecause the volatility of the dollar (10%) is of the same order as that of the bond (10%) and much lower than that of the stocks (22%): in the bond the currency is almost half the risk
    • BBecause bonds have more currency risk
    • CBecause bonds pay a coupon
    • DBecause the stocks are already hedged
    Show answer

    Answer A. Because the volatility of the dollar (10%) is of the same order as that of the bond (10%) and much lower than that of the stocks (22%): in the bond the currency is almost half the risk

    The share of the risk that is currency depends on how much risk the asset has. In fixed income, the currency dominates; in stocks, it is diluted. That is why many portfolios almost always hedge foreign fixed income and decide case by case for stocks.

  3. Since 2015, hedging the dollar with forwards costs the evro investor between 0.3% and 2.5% a year in the data. Where does that cost come from?

    • AFrom the broker's commissions
    • BFrom the forward's bid-ask spread
    • CFrom the dollar having appreciated
    • DFrom the interest rate differential: selling dollars forward is done at a price that reflects the dollar rate being higher than the evro rate (covered parity), plus the basis
    Show answer

    Answer D. From the interest rate differential: selling dollars forward is done at a price that reflects the dollar rate being higher than the evro rate (covered parity), plus the basis

    Through covered parity, the forward reflects the interest rate differential. Whoever sells the high-rate currency forward receives less than spot: they pay the differential. In 2010-2014, with similar rates, the hedge cost almost nothing; with the dollar paying two or three points more, it costs that every year.

  4. A REST client sends a market order and the connection times out with no response. What should it do before resending it?

    • AResend it right away: if there is no response, it was not executed
    • BLook up the order by its client ID (in OANDA, GET /v3/accounts/{id}/orders/@{clientID}): the order may have arrived and been filled even though the response was lost
    • CWait until the next day
    • DClose all positions
    Show answer

    Answer B. Look up the order by its client ID (in OANDA, GET /v3/accounts/{id}/orders/@{clientID}): the order may have arrived and been filled even though the response was lost

    A timeout does not say whether the order arrived. Resending blindly can double the position. That is why every order carries a client ID (clientExtensions.id in OANDA, orderRef in IBKR, magic and comment in MT5) and the client looks it up before resending.

  5. MetaTrader 5 returns retcode = 10004 to a market order. What does it mean and what does a system do?

    • ARequote (TRADE_RETCODE_REQUOTE): the price is no longer valid; it asks for a new price and decides whether to resend within its maximum deviation
    • BOrder filled: it does nothing
    • CInsufficient funds: it reduces the size
    • DMarket closed: it waits for the open
    Show answer

    Answer A. Requote (TRADE_RETCODE_REQUOTE): the price is no longer valid; it asks for a new price and decides whether to resend within its maximum deviation

    According to the MQL5 return code table, 10004 is TRADE_RETCODE_REQUOTE, 10009 is TRADE_RETCODE_DONE, 10019 is not enough money and 10018 market closed. A requote is a rejection on price, like the last look of lesson 3.3.1: it is resent with the new price only if it is still within the bound.

Key points

  • The return in one's own currency is (1 + r_local)(1 + r_fx) − 1. For an evro investor with a US index, the dollar adds 2.8 points of volatility (from 22.1% local to 24.9%); to a 10-year bond it adds 4.5 (from 10.2% to 14.6%).
  • Hedging with a one-month forward trades currency risk for the interest rate differential: almost free in 2010-2014 and between 0.3% and 2.5% a year since 2015 in the data, with monthly cash flows that can reach 7% of the portfolio in one month.
  • In a portfolio of five assets and four currencies, the currency is a small part of the risk (13% of the variance, 10.5 points from the dollar): hedging the dollar lowers the volatility from 19.7% to 18.2% and costs 0.65 points of return a year; also hedging the pound and the yen changes almost nothing.
  • A future hedges the currency with a residual exposure: only whole contracts can be bought, the margin and daily settlements are in the contract's currency and the contract has to be rolled.
  • Each API measures the order its own way: OANDA in signed units of the base currency, IBKR in quantity of the base with a BUY or SELL action, MT5 in lots with the broker's contract size. Converting wrong is an error by a factor of 100,000.
  • A serious order client sets a price bound (priceBound, deviation), retries with increasing backoff on the rate limit and, if there is no response, looks up its client ID before resending.

Resources

Data