---
title: "3.3.3 Currency risk and FX execution: OANDA, IBKR, MT5"
description: "Currency risk in a multi-currency portfolio, hedging with forwards and futures, its cost and cash flows, and FX orders via OANDA, IBKR and MT5."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/currency-risk-and-fx-execution/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/currency-risk-and-fx-execution.png"
---

# 3.3.3 · 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.

- Course: [Level 3: Derivatives: futures, spreads, forex and options](https://pythonparatrading.com/en/python-trading-course/level-3/)
- Module: 3.3 · Advanced forex
- Duration: 75 min
- Prerequisites: [1.2.4 · Forex: pairs, pips, lots, rollover and CFDs](https://pythonparatrading.com/en/python-trading-course/level-1/forex-pairs-pips-lots-and-rollover/), [1.3.6 · Brokers as market data sources](https://pythonparatrading.com/en/python-trading-course/level-1/brokers-as-market-data-sources/), [1.4.1 · Downloading over REST and websockets: pagination, rate limits and retries](https://pythonparatrading.com/en/python-trading-course/level-1/rest-websockets-pagination-and-retries/), [3.3.1 · FX microstructure: OTC, ECN, last look and sessions](https://pythonparatrading.com/en/python-trading-course/level-3/fx-microstructure-otc-ecn-last-look/), [3.3.2 · Carry and crash risk, momentum and value in currencies; baskets](https://pythonparatrading.com/en/python-trading-course/level-3/currency-carry-momentum-value/)

## By the end of this lesson

- Decompose the return of a foreign asset into local return, currency return and cross term, and measure the share of the risk that is currency.
- Hedge currency risk with one-month forwards: hedge ratio, minimum-variance ratio, cost from the interest rate differential and cash flows from the roll.
- Break down the risk of a multi-currency portfolio between assets and currencies and decide what to hedge; compute the hedge with futures and its residual exposure.
- Translate an exposure into each API's order (signed units in OANDA, quantity and action in IBKR, lots in MT5) and read their responses and error codes.
- Write a REST order client with a price bound, retries on the rate limit and a lookup by client ID before resending, and test it against a local server.

## 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 + 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 $r_{\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.

```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)
```
```output
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 $X$ in USD per evro and $F$ the one-month forward:

$$
\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** $h$ between 0 and 1, the hedged return is the unhedged one plus $h$ 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).

```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)
```
```output
           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 $X_t / F_t - 1$, known when the hedge is opened:

```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})")
```
```output
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.* Bars by year from 2010 to 2025. From 2010 to 2013 the hedge earns a little (+0.06% to +0.56%); in 2014 it is nil and from 2015 it costs every year: −0.27% in 2015, −1.37% in 2017, −2.18% in 2018, −2.54% in 2019 (highlighted), −0.63% in 2020, −1.48% in 2022, −1.59% in 2023 and −1.66% in 2025.

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, $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.

```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
```
```output
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.* Horizontal bars: RVA 35.9%, RVE 31.8%, RVB 19.5%, dollar 10.5% (highlighted), pound 1.2%, yen 1.1% and deposits 0.0%. Equities take 87% of the risk; currencies, 13%, almost all of it the dollar.

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?

```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)
```
```output
             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:

```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")
```
```output
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.

```mermaid
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:

| API | How to request a purchase of 1 M EURUSD | Unit |
|---|---|---|
| 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_BUY` | Lots; 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:

```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}")
```
```output
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 documentation[^1]:

- 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.

```python skip title="oanda_orden.py"
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`.

```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:"))
```
```output
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.

```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))
```
```output
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.

```mermaid
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 --> [*]
```

```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)
```
```output
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` documentation[^2], `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.

```python skip title="ibkr_orden_fx.py"
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 documentation[^3]: `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.

```python skip title="mt5_orden_fx.py"
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:

```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}")
```
```output
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.**

```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)
```
```output
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.

2. 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.**

```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
```
```output
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.

3. 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.

[^1]: OANDA, [Order Endpoints](https://developer.oanda.com/rest-live-v20/order-ep/), [Order Definitions](https://developer.oanda.com/rest-live-v20/order-df/) (`MarketOrderRequest`, `LimitOrderRequest`, `OrderState`) and [Transaction Definitions](https://developer.oanda.com/rest-live-v20/transaction-df/) (`OrderFillTransaction`, `OrderCancelTransaction`, `OrderCancelReason`), accessed on October 22, 2026.
[^2]: [ib_async: API](https://ib-api-reloaded.github.io/ib_async/api.html), version 2.1.0 on [PyPI](https://pypi.org/project/ib_async/), accessed on October 22, 2026.
[^3]: MQL5, [order_send](https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py), [symbol_info](https://www.mql5.com/en/docs/python_metatrader5/mt5symbolinfo_py), [order properties](https://www.mql5.com/en/docs/constants/tradingconstants/orderproperties) (`ORDER_FILLING_*`) and [return codes](https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes), accessed on October 22, 2026.

## 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?

   - A. 22.1%: the currency adds no risk
   - B. 32.3%: the volatilities add up
   - C. About 24.9%: the variances add up almost in full because the correlation is low
   - D. 11.9%: the currency offsets

   *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?

   - 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
   - B. Because bonds have more currency risk
   - C. Because bonds pay a coupon
   - D. Because the stocks are already hedged

   *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?

   - A. From the broker's commissions
   - B. From the forward's bid-ask spread
   - C. From the dollar having appreciated
   - 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

   *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?

   - A. Resend it right away: if there is no response, it was not executed
   - 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
   - C. Wait until the next day
   - D. Close all positions

   *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?

   - 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
   - B. Order filled: it does nothing
   - C. Insufficient funds: it reduces the size
   - D. Market closed: it waits for the open

   *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

### Research

- [Finite-State Machines for Quantitative Trading Execution in Python](https://pythonparatrading.com/en/finite-state-machines-trading-python/) · Research-0029 · 47 min

### Documentation

- [OANDA v20 REST: Order Endpoints (POST /v3/accounts/{accountID}/orders)](https://developer.oanda.com/rest-live-v20/order-ep/) · developer.oanda.com
- [OANDA v20 REST: Transaction Definitions (OrderFillTransaction, OrderCancelReason)](https://developer.oanda.com/rest-live-v20/transaction-df/) · developer.oanda.com
- [ib_async: API (IB.placeOrder, Forex, MarketOrder, LimitOrder, Trade)](https://ib-api-reloaded.github.io/ib_async/api.html) · ib-api-reloaded.github.io
- [MetaTrader 5 for Python: order_send](https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py) · mql5.com
- [MQL5: trade server return codes](https://www.mql5.com/en/docs/constants/errorswarnings/enum_trade_return_codes) · mql5.com

### Data

- [Level 3 data: spot indices with dividends and rates (CSV)](https://pythonparatrading.com/curso/datos/nivel-3/futuros3_indices_contado.csv) · Download · futuros3_indices_contado.csv

Previous lesson: [3.3.2 · Carry and crash risk, momentum and value in currencies; baskets](https://pythonparatrading.com/en/python-trading-course/level-3/currency-carry-momentum-value/)

Next lesson: [3.4.1 · Payoffs, put-call parity and arbitrage bounds](https://pythonparatrading.com/en/python-trading-course/level-3/option-payoffs-put-call-parity-arbitrage/)
