---
title: "7.6.6 Capstone C5: crypto market making or arbitrage"
description: "C5 project guide: cross-exchange funding carry with costs, margin and per-counterparty limits, ccxt execution, validation and paper trading."
url: "https://pythonparatrading.com/en/python-trading-course/level-7/capstone-crypto-market-making-arbitrage/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-7/capstone-crypto-market-making-arbitrage.png"
---

# 7.6.6 · Capstone C5: crypto market making or arbitrage with counterparty control

> The brief is a small crypto arbitrage desk that collects perpetual funding without betting on the price, or that quotes both sides, and that survives the bankruptcy of an exchange. This guide sets the scope, runs the skeleton on the level 3 data, including the planted bankruptcy of dedalo, and applies the common rubric to what is defended before the panel.

- Course: [Level 7: Production, MLOps, regulation, career and capstone](https://pythonparatrading.com/en/python-trading-course/level-7/)
- Module: 7.6 · Final capstone
- Duration: 65 min
- Prerequisites: [7.6.1 · Capstone: common requirements, the panel's rubric and preparing the defense](https://pythonparatrading.com/en/python-trading-course/level-7/capstone-requirements-rubric-defense/), [2.3.6 · Introduction to market making: inventory, spread and adverse selection (Avellaneda-Stoikov)](https://pythonparatrading.com/en/python-trading-course/level-2/market-making-avellaneda-stoikov/), [2.5.4 · Deflated Sharpe Ratio and Probability of Backtest Overfitting (PBO)](https://pythonparatrading.com/en/python-trading-course/level-2/deflated-sharpe-ratio-and-pbo/), [3.7.1 · Perpetuals: funding mechanics, mark price and liquidations](https://pythonparatrading.com/en/python-trading-course/level-3/perpetual-futures-funding-mark-price-liquidations/), [3.7.2 · Funding arbitrage, basis with futures and cross-exchange arbitrage](https://pythonparatrading.com/en/python-trading-course/level-3/funding-basis-cross-exchange-arbitrage/), [3.7.4 · ccxt for trading on multiple exchanges](https://pythonparatrading.com/en/python-trading-course/level-3/ccxt-multi-exchange-trading/), [4.2.4 · Stress testing and correlation, liquidity, gap and counterparty risk](https://pythonparatrading.com/en/python-trading-course/level-4/stress-testing-correlation-liquidity-counterparty-risk/), [4.4.4 · Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase](https://pythonparatrading.com/en/python-trading-course/level-4/crypto-exchange-apis-ccxt-and-native/), [4.5.5 · Fault tolerance: idempotency, persistent state and reconciliation at startup](https://pythonparatrading.com/en/python-trading-course/level-4/fault-tolerance-idempotency-and-reconciliation/), [7.4.4 · MiCA after the transitional period: checking a provider in the registers](https://pythonparatrading.com/en/python-trading-course/level-7/mica-authorized-casp-registers/)

## By the end of this lesson

- Define the brief of project C5, in its funding and basis arbitrage variant or its market making variant, and its deliverables.
- Build the executable skeleton: 7-day carry by exchange, portfolio with costs and margin, per-counterparty limits and an alarm that withdraws the capital.
- Execute both legs of a cash-and-carry against simulated exchanges with the ccxt unified API, unwind an orphaned leg and reconcile.
- Apply walk-forward validation and the Deflated Sharpe, and explain why they are not enough for a carry: the risk that matters is not in the sample.
- Plan the four weeks of paper trading on demo accounts, the counterparty monitoring and the evidence for the rubric.

## The brief

A treasury desk trading its own capital wants a return that does not depend on the direction of cryptocurrency
prices. It commissions a system from one of these two families, chosen by the student:

- **Funding and basis arbitrage.** Buy spot and sell the perpetual (or a quarterly future) in the same quantity,
  collect the funding or lock in the basis, and spread the capital across several exchanges (lessons 2.3.7, 3.7.1 and
  3.7.2).
- **Market making.** Quote both sides of a book, earn the bid-ask spread and control inventory and adverse selection,
  with an Avellaneda-Stoikov-type model (lesson 2.3.6).

In both, the brief puts the same thing first: **the capital is deposited on exchanges that can fail**. The system has
to state how much is on each one, why, what takes it out and how much is lost if one goes bankrupt. A strategy that
returns a lot with all the capital on a single exchange does not meet the brief, even if the backtest is good.

Out of scope: DeFi and MEV (lesson 3.7.3), options, directional strategies, leverage above what the skeleton
justifies, trading with real money and any exchange that does not pass the check of lesson 7.4.4 (authorization as a
crypto-asset service provider under MiCA, or an equivalent registration outside the EU).

| Deliverable | What it contains | How it is checked |
|---|---|---|
| Repository | 7.6.1 template, uv and Docker, one command per stage | `make todo` reproduces the report on a clean machine |
| Counterparty register | Exchanges, authorization, limits, segregation, withdrawal tests | Every exchange holding capital has its profile and its limit |
| Trial log | Every configuration tested, with its series | The DSR's number of trials comes from this file |
| Validation report | Walk-forward, DSR, bankruptcy and liquidation scenarios | Stopping rules are dated before the results |
| Executor | Two legs with own identifiers, unwind, reconcile | Tests with injected failures in the simulator |
| Paper trading journal | Four weeks on demo accounts | Every funding period has its row and its reconciliation |
| Technical report and defense | 7.6.1 template, 20-minute presentation | Before the panel |

<aside class="callout" data-kind="riesgo">

The skeleton's data is **synthetic** (level 3): XBQ and XEQ mimic BTC and ETH, and the exchanges `alfa`, `boreal`,
`cenit` and `dedalo` mimic Binance, Deribit, BitMEX and FTX. The perpetual's premium, its narrowing and the bankruptcy
of `dedalo` are planted with known parameters. No result in this guide is evidence that a strategy works in real
markets. This is not advice: the guide does not recommend any exchange or any product.

</aside>

## Architecture

```mermaid
flowchart TB
  D[Exchange data] --> S[Carry signal]
  S --> A[Allocation]
  C[Counterparties] --> A
  A --> R[Risk and margin]
  R --> E[ccxt executor]
  E --> X1[Exchange 1]
  E --> X2[Exchange 2]
  X1 --> K[Reconciliation]
  X2 --> K
  K --> M[Monitor]
```

The flow is that of any live system (lesson 4.5.1), with two pieces specific to the project. The **counterparty
monitor** is an input to the allocation on the same level as the signal: it can take an exchange out of the allocation
even if it is the one that pays the most. And the **allocation** does not simply split by expected return: it splits
with per-exchange limits that are set with the bankruptcy scenario, not with the backtest.

| Piece | Library | Where it was learned |
|---|---|---|
| Funding, mark price and liquidation | pandas | 1.2.6, 3.7.1 |
| Cash-and-carry, basis, differentials and counterparty | pandas | 2.3.7, 3.7.2 |
| Market making and inventory | numpy | 2.3.6 |
| Unified API, errors, rate limits and idempotency | ccxt, ccxt.pro | 3.7.4 |
| Signatures, nonces, clocks and user streams | ccxt, native APIs | 4.4.4 |
| Credentials and least privilege | environment variables, secrets manager | 4.4.5 |
| Counterparty risk and stress | pandas | 4.2.4 |
| Margin and liquidations in derivatives | pandas | 4.2.6 |
| Orders, reconnections and reconciliation | state machine, ccxt | 4.5.2, 4.5.5 |
| Multiple testing and DSR | trial log | 2.2.4, 2.5.4 |
| Monitoring and shutdown | metrics, expiring permission | 7.2.3, 7.3.3 |
| Providers authorized under MiCA | ESMA and CNMV registers | 3.7.5, 7.4.4 |

## Data

The skeleton uses the level 3 data (`public/curso/datos/nivel-3/`); the market making variant, the level 2 data:

| File | What it is | What it is for here |
|---|---|---|
| `cripto3_funding.parquet` | 8-hour funding, index, mark and last price of the perpetual; 4 exchanges, 2 assets, 2020-2025 | Signal, collection and hedge |
| `cripto3_exchanges.csv` | Exchange profiles and the timeline of the `dedalo` bankruptcy | Counterparty scenario and alarm |
| `cripto3_1h.parquet` | 1-hour bars of the `alfa` perpetual with the mark | Margin and liquidations |
| `cripto3_futuros.parquet`, `cripto3_diario.parquet` | Quarterlies and daily closes by exchange | Basis and dislocations (3.7.2) |
| `xsn_funding_8h.csv`, `xsn_ccxt_ohlcv.json` | Funding of another token and the ccxt format | Tests of the data loader |
| `mm2_medio.parquet`, `mm2_flujo.parquet` | One session second by second with informed flow | Market making variant |

The known answer, according to the level 3 README: the funding formula is Binance's (premium plus a clamped interest
term), with a floor of 0.01% per period, 10.95% a year; the bullish premium narrows with a half-life of 2.5 years, and
`dedalo` suspends withdrawals on November 8, 2022 at 12:00 and sets balances to zero on the 11th. Lesson 3.7.2 measured
the carry by exchange and year and the premium of `dedalo` over `alfa`.

With real data, the funding history is downloaded from each exchange with ccxt (`fetch_funding_rate_history`) or with
its native API; for example, the Bybit API v5 documentation describes the funding history endpoint. Fees by tier,
funding caps, each contract's interval and the margin rules are read in each exchange's documentation on the day the
profile is written, with the date: they change, and they are not copied from a tutorial. The funding interval is not
assumed to be 8 hours: it is computed from the timestamps of the history. The authorization checks under MiCA (ESMA's
interim register, the register of the CNMV, Spain's securities market regulator) are those of lesson 7.4.4.

## Executable skeleton

The skeleton is the backbone of the funding arbitrage, end to end, in the course's main environment and without
network access: data, signal, allocation with limits and alarm, margin, two-leg execution with ccxt, validation and
report. The market making variant is built on the simulator from 2.3.6 (exercise 2).

### Fingerprinted data and the bankruptcy timeline

```python
import hashlib
import json

import numpy as np
import pandas as pd
from scipy.stats import norm

def huella(ruta):
    """Abbreviated SHA-256 of a data file (goes into the report)."""
    with open(ruta, "rb") as f:
        return hashlib.sha256(f.read()).hexdigest()[:12]

FICHEROS = ["cripto3_funding.parquet", "cripto3_exchanges.csv",
            "cripto3_1h.parquet"]
HUELLAS = {f: huella(f) for f in FICHEROS}

fu = pd.read_parquet("cripto3_funding.parquet")
def tabla(col):
    return fu.pivot(index="ts", columns=["exchange", "activo"],
                    values=col)
F, I, P = (tabla("tasa_funding"), tabla("precio_indice"),
           tabla("ultimo"))
ev = pd.read_csv("cripto3_exchanges.csv").dropna(subset=["ts_utc"])
EV = dict(zip(ev["evento"], pd.to_datetime(ev["ts_utc"], utc=True)))
print(F.shape, "8-h periods x pairs;", F.index[0].date(), "to",
      F.index[-1].date())
print(ev[["exchange", "ts_utc", "evento"]].to_string(index=False))
```
```output
(6575, 8) 8-h periods x pairs; 2020-01-01 to 2025-12-31
exchange           ts_utc                evento
  dedalo 2022-11-06 00:00               rumores
  dedalo 2022-11-08 12:00 retiradas_suspendidas
  dedalo 2022-11-11 14:00      cese_negociacion
  dedalo 2022-11-11 23:00               quiebra
```

6,575 eight-hour periods per pair, from 2020 to 2025, and the `dedalo` timeline (event keys in Spanish in the data:
`rumores` rumors, `retiradas_suspendidas` withdrawals suspended, `cese_negociacion` trading halted, `quiebra`
bankruptcy): rumors on November 6, 2022, withdrawals suspended on the 8th at 12:00, trading halted on the 11th at 14:00
and bankruptcy that night. In the funding data, `dedalo` has no rows after the last period before the halt.

### Signal, allocation and simulator

The signal is the 7-day carry: the mean of the last 21 published funding rates, annualized. A pair enters when its
carry exceeds one threshold and exits when it falls below another (hysteresis, so as not to pay fees on every
oscillation). The capital is split equally among the active pairs, with a **per-exchange limit**, and each unit of
notional requires $1 + 1/L$ of capital: the spot and the margin of the short at $L = 3$. The simulator decides in each
period with what has been published up to that moment and collects in the next one the funding and the **hedge**: the
difference between what the spot does (the index) and what the perpetual does, which is not zero even though the
position is neutral. Each change in notional pays two sides of 5 bp.

```python
PER = 3 * 365                       # funding periods per year
carry7 = F.rolling(21).mean() * PER  # 7-day annual carry, at t
cobertura = (I.pct_change() - P.pct_change()).to_numpy()
pares = list(F.columns)
exch = np.array([e for e, _ in pares])
ts, Fv, Cv = F.index, F.to_numpy(), carry7.to_numpy()

def simular(entrada=0.11, salida=0.0, limite=0.30, alarma=True,
            modo="igual", coste=5e-4, L=3, vetos=()):
    """Long spot + short perpetual on each active pair. Decides at t
    with what is published up to t; collects funding and hedge at t + 1.
    `vetos`: (exchange, from, to) during which the capital is withdrawn."""
    vetos = list(vetos) + ([("dedalo", EV["rumores"], None)]
                           if alarma else [])
    activo, nom = np.zeros(len(pares), bool), np.zeros(len(pares))
    cap_u = 1 + 1 / L          # capital per unit of notional
    capital, congelado, perdida, giro = 1.0, 0.0, 0.0, 0.0
    atrapado, serie = False, []
    for t in range(len(ts) - 1):
        fuera = {e for e, d, h in vetos
                 if ts[t] >= d and (h is None or ts[t] < h)}
        if ts[t] >= EV["retiradas_suspendidas"] and not atrapado:
            atrapado = True                # whatever is left, trapped
            if "dedalo" not in fuera:
                congelado = nom[exch == "dedalo"].sum() * cap_u
        if atrapado:
            fuera.add("dedalo")
        if ts[t] >= EV["cese_negociacion"] and congelado:
            perdida = congelado / capital
            capital, congelado = capital - congelado, 0.0
        c = Cv[t]
        ok = ~np.isnan(c) & ~np.isin(exch, list(fuera))
        activo = np.where(activo, c > salida, c > entrada) & ok
        w = np.zeros(len(pares))
        if activo.any() and modo == "mejor":
            w[np.where(activo, c, -np.inf).argmax()] = 1.0
        elif activo.any():
            w[activo] = 1 / activo.sum()
        for e in set(exch[w > 0]):        # per-counterparty limit
            m = exch == e
            w[m] *= min(1.0, limite / w[m].sum())
        nuevo = (capital - congelado) * w / cap_u
        cambio = np.abs(nuevo - nom).sum()
        giro += cambio
        nom = nuevo
        cobro = (np.nan_to_num(Fv[t + 1])
                 + np.nan_to_num(cobertura[t + 1]))
        capital += (nom * cobro).sum() - 2 * coste * cambio
        serie.append(capital)
    return pd.Series(serie, index=ts[1:]), perdida, giro

def metricas(s):
    r = s.pct_change().dropna()
    anos = (s.index[-1] - s.index[0]).days / 365.25
    return {"final": s.iloc[-1],
            "annual %": 100 * (max(s.iloc[-1], 1e-9) ** (1 / anos) - 1),
            "Sharpe": r.mean() / r.std() * np.sqrt(PER),
            "max drawdown %": 100 * (s / s.cummax() - 1).min()}

base, perdida, giro = simular()
print({k: round(float(v), 2) for k, v in metricas(base).items()})
anos = 100 * base.resample("YE").last().pct_change().dropna()
print("by year %:", {d.year: round(v, 1) for d, v in anos.items()})
```
```output
{'final': 2.29, 'annual %': 14.84, 'Sharpe': 14.22, 'max drawdown %': -0.75}
by year %: {2021: 27.2, 2022: 7.6, 2023: 7.9, 2024: 7.4, 2025: 6.2}
```

With entry at 11% (just above the formula's floor), exit when the week's carry turns negative, a 30% limit per exchange
and the counterparty alarm, the capital grows from 1 to 2.29 in six years: 14.8% a year, with a maximum drawdown of
0.75%. By year, 27.2% in 2021 and between 6.2% and 7.9% since 2022: the premium narrows and the carry approaches the
floor divided by the capital each unit of notional requires (10.95% / 1.33 ≈ 8.2%), minus costs. The Sharpe of 14.2 is
not a typo: it is what a carry gives without the tail inside the sample.

### Counterparty: concentration, limits and alarm

The alarm in the previous cell takes `dedalo` out of the allocation as soon as the insolvency rumors event appears in
the incident log (in real life, the counterparty monitor: news, withdrawal delays, spot discount against the index,
fund outflows). Without it, and with other ways of allocating:

```python
VARIANTES = {
    "all on the highest payer": dict(limite=1.0, alarma=False,
                                     modo="mejor"),
    "spread, no limit": dict(limite=1.0, alarma=False),
    "spread, 30% limit": dict(limite=0.30, alarma=False),
    "spread, 15% limit": dict(limite=0.15, alarma=False),
    "30% limit and alarm": dict(limite=0.30, alarma=True),
}
filas = {}
for nombre, kw in VARIANTES.items():
    s, perd, _ = simular(**kw)
    filas[nombre] = metricas(s) | {"loss on dedalo %": 100 * perd}
pd.DataFrame(filas).T.round(2)
```
```output
                          final  annual %  Sharpe  max drawdown %  loss on dedalo %
all on the highest payer   0.01    -50.59   -0.20          -99.18             99.12
spread, no limit           1.42      5.98    0.47          -40.15             39.91
spread, 30% limit          1.61      8.25    0.72          -30.04             29.95
spread, 15% limit          1.38      5.50    0.90          -15.04             14.99
30% limit and alarm        2.29     14.84   14.22           -0.75              0.00
```

The figure at the top draws the first column. With all the capital on the exchange that pays the most, which in
November 2022 was `dedalo`, 99% of the capital at that moment is lost. Spread equally with no limit, 39.9%; with a 30%
limit, 30%; with 15%, 15%. The limit turns a total loss into a bounded loss, but it does not prevent it: whatever was
on `dedalo` when withdrawals were suspended stays there. Only the alarm, which acts 60 hours earlier, leaves the loss
at zero.

Two warnings for the technical report. The alarm is written **after** knowing the FTX case: this has to be said before
the panel, and the cost of false alarms has to be measured too (exercise 1). And a 15% limit halves the loss compared
with the 30% one, but it also cuts the return, because with few active exchanges part of the capital sits unused.

```mermaid
stateDiagram-v2
  [*] --> NORMAL
  NORMAL --> WATCH: weak signal
  WATCH --> NORMAL: cleared up
  WATCH --> WITHDRAW: strong signal
  NORMAL --> WITHDRAW: rumor or delay
  WITHDRAW --> VETOED: capital out
  VETOED --> NORMAL: manual review
```

### Margin of the short leg

The perpetual short is liquidated if the mark rises to its liquidation price, even if the spot has gained the same
amount (lessons 3.7.1 and 4.2.6). With the margin topped up every day, the question is on how many days the rise of the
mark from the start of the day would have reached that price:

```python
h1 = pd.read_parquet("cripto3_1h.parquet")
h1["dia"] = h1["ts"].dt.floor("D")
ref = h1.groupby(["activo", "dia"])["mark_cierre"].first()
techo = h1.groupby(["activo", "dia"])["mark_maximo"].max()
subida = techo / ref - 1               # worst mark rise in the day
MMR = 0.005                            # maintenance margin
margen = {}
for L in (2, 3, 5, 10):
    umbral = (1 + 1 / L) / (1 + MMR) - 1
    margen[f"{L}x"] = {"liquidating rise %": 100 * umbral,
                      "days XBQ": int((subida["XBQ"] >= umbral).sum()),
                      "days XEQ": int((subida["XEQ"] >= umbral).sum())}
peor = 100 * subida.groupby(level=0).max()
print(f"worst one-day mark rise: XBQ {peor['XBQ']:.1f}%, "
      f"XEQ {peor['XEQ']:.1f}%")
pd.DataFrame(margen).T.round(1)
```
```output
worst one-day mark rise: XBQ 15.9%, XEQ 18.1%
     liquidating rise %  days XBQ  days XEQ
2x                 49.3       0.0       0.0
3x                 32.7       0.0       0.0
5x                 19.4       0.0       0.0
10x                 9.5      16.0      37.0
```

The worst one-day rise in the mark was 15.9% on XBQ and 18.1% on XEQ. At 3x it takes 32.7%: no day. At 5x, 19.4%:
none, by a narrow margin on XEQ. At 10x a 9.5% rise is enough, and that happened on 16 days on XBQ and 37 on XEQ. The
skeleton uses 3x with daily top-ups; the technical report has to justify the real top-up frequency, which depends on how
long it takes to move funds between accounts.

### Two-leg execution with ccxt

The executor talks to the exchanges through the ccxt unified API. In the skeleton, each exchange is an in-memory class
that inherits from `ccxt.Exchange`: unified markets (`XBQ/USDQ` and the linear perpetual `XBQ/USDQ:USDQ`), precision,
orders, balance and positions with the ccxt structures. On `cenit`, the perpetual is down:

```python
import ccxt
from ccxt.base.errors import ExchangeNotAvailable

class Simulado(ccxt.Exchange):
    """In-memory exchange with the ccxt unified API: markets,
    orders, balance and positions. No network (the HTTP version with
    signing and rate limits is in lesson 3.7.4)."""
    def describe(self):
        return self.deep_extend(super().describe(), {
            "id": "simulado", "has": {"createOrder": True,
                                      "fetchPositions": True}})

    def __init__(self, nombre, precio, caidos=()):
        super().__init__({})
        self.id, self.px, self.caidos = nombre, precio, set(caidos)
        self.reloj, self.n = 1_704_067_200_000, 0   # 2024-01-01 UTC
        self.saldo, self.pos = {"USDQ": 100_000.0, "XBQ": 0.0}, 0.0
        comun = {"base": "XBQ", "quote": "USDQ", "baseId": "XBQ",
                 "quoteId": "USDQ", "active": True, "taker": 5e-4,
                 "maker": 2e-4, "limits": {"cost": {"min": 10}},
                 "precision": {"amount": 0.001, "price": 0.1}}
        mercados = (
            {"id": "XBQUSDQ", "symbol": "XBQ/USDQ", "type": "spot",
             "spot": True},
            {"id": "XBQ-PERP", "symbol": "XBQ/USDQ:USDQ",
             "type": "swap", "swap": True, "linear": True,
             "contract": True, "settle": "USDQ", "contractSize": 1})
        self.set_markets([self.safe_market_structure(comun | m)
                          for m in mercados])

    def create_order(self, symbol, type, side, amount, price=None,
                     params={}):
        if symbol in self.caidos:
            raise ExchangeNotAvailable(f"{self.id} 503 {symbol}")
        m, s = self.market(symbol), 1 if side == "buy" else -1
        q = float(self.amount_to_precision(symbol, amount))
        px = round(self.px * (1 + s * 2e-4), 1)    # half spread
        fee = round(q * px * m["taker"], 4)
        if m["spot"]:
            self.saldo["XBQ"] += s * q
            self.saldo["USDQ"] -= s * q * px + fee
        else:
            self.pos += s * q
            self.saldo["USDQ"] -= fee
        self.reloj, self.n = self.reloj + 250, self.n + 1
        return self.safe_order({
            "id": f"{self.id}-{self.n}", "timestamp": self.reloj,
            "clientOrderId": params.get("clientOrderId"),
            "symbol": symbol, "type": type, "side": side, "amount": q,
            "filled": q, "average": px, "status": "closed",
            "fee": {"cost": fee, "currency": "USDQ"}}, m)

    def fetch_positions(self, symbols=None, params={}):
        return [self.safe_position({
            "symbol": "XBQ/USDQ:USDQ", "contracts": abs(self.pos),
            "side": "short" if self.pos < 0 else "long", "info": {}})]

def abrir_par(ex, q, cid):
    """Buys spot and sells the perpetual; if the second leg fails,
    unwinds the first. Returns the executed orders."""
    hechas = [ex.create_order("XBQ/USDQ", "market", "buy", q,
                              params={"clientOrderId": f"{cid}-c"})]
    try:
        hechas.append(ex.create_order(
            "XBQ/USDQ:USDQ", "market", "sell", q,
            params={"clientOrderId": f"{cid}-p"}))
    except ExchangeNotAvailable as e:
        print(f"{ex.id}: {e}; unwinding the spot leg")
        hechas.append(ex.create_order(
            "XBQ/USDQ", "market", "sell", q,
            params={"clientOrderId": f"{cid}-x"}))
    return hechas

def conciliar(ex, ordenes):
    """Positions according to our own orders versus the exchange."""
    libro = {"XBQ/USDQ": 0.0, "XBQ/USDQ:USDQ": 0.0}
    for o in ordenes:
        libro[o["symbol"]] += o["filled"] * (1 if o["side"] == "buy"
                                             else -1)
    p = ex.fetch_positions()[0]
    real = {"XBQ/USDQ": ex.saldo["XBQ"], "XBQ/USDQ:USDQ":
            p["contracts"] * (-1 if p["side"] == "short" else 1)}
    coste = 100_000 - ex.saldo["USDQ"] - ex.saldo["XBQ"] * ex.px
    return libro == real, real, round(coste, 2)

for ex in (Simulado("alfa", 42_000.0),
           Simulado("cenit", 42_010.0, caidos=["XBQ/USDQ:USDQ"])):
    ordenes = abrir_par(ex, 0.5, f"c5-{ex.id}-0001")
    print(ex.id, [(o["clientOrderId"], o["side"], o["average"])
                  for o in ordenes])
    print("  matches, positions, cost:", conciliar(ex, ordenes))
```
```output
alfa [('c5-alfa-0001-c', 'buy', 42008.4), ('c5-alfa-0001-p', 'sell', 41991.6)]
  matches, positions, cost: (True, {'XBQ/USDQ': 0.5, 'XBQ/USDQ:USDQ': -0.5}, 25.2)
cenit: cenit 503 XBQ/USDQ:USDQ; unwinding the spot leg
cenit [('c5-cenit-0001-c', 'buy', 42018.4), ('c5-cenit-0001-x', 'sell', 42001.6)]
  matches, positions, cost: (True, {'XBQ/USDQ': 0.0, 'XBQ/USDQ:USDQ': 0.0}, 29.4)
```

On `alfa` both legs go through and the reconciliation matches: 0.5 XBQ of spot and −0.5 of perpetual, with a cost of
25.2 USDQ between fees and spread. On `cenit`, the spot goes through, the perpetual returns a 503 and the executor
unwinds the spot with its own identifier (`-x`): the position ends at zero and the attempt costs 29.4 USDQ. Our own
identifiers (`clientOrderId`) are what make it possible to know, after a timeout, whether an order exists without
duplicating it (lesson 3.7.4).

Against a real exchange, the same code uses the exchange's ccxt class and its demo account. It is marked `skip`: it
needs network access and keys, which are read from the environment and never have withdrawal permission (lesson
4.4.5).

```python skip title="ejecutor_demo.py"
import os

import ccxt

ex = ccxt.bybit({"apiKey": os.environ["BYBIT_KEY"],
                 "secret": os.environ["BYBIT_SECRET"]})
ex.enable_demo_trading(True)        # demo account
ex.load_markets()
hist = ex.fetch_funding_rate_history("BTC/USDT:USDT", limit=21)
horas = (hist[-1]["timestamp"] - hist[0]["timestamp"]) / 3.6e6 / 20
carry7 = (sum(h["fundingRate"] for h in hist) / len(hist)
          * 24 / horas * 365)
if carry7 > 0.11:
    q = float(ex.amount_to_precision("BTC/USDT:USDT", 0.01))
    ex.create_order("BTC/USDT", "market", "buy", q,
                    params={"clientOrderId": "c5-0001-c"})
    ex.create_order("BTC/USDT:USDT", "market", "sell", q,
                    params={"clientOrderId": "c5-0001-p"})
print(ex.fetch_positions(["BTC/USDT:USDT"]))
```

`enable_demo_trading` exists in ccxt version 4.5.89 for Bybit and for Binance. Before using it, read in each
exchange's documentation what its demo environment covers (markets, fictitious balances, limits): it is not identical
to the real one.

### Trial log, validation and report

The eight threshold combinations (entry at 8, 11, 15 and 20%; exit at 0 and 5%) are stored in
`experimentos/registro_pruebas.jsonl`, append-only and with the fields of guide 7.6.1, and the Deflated Sharpe's
number of trials is counted from that file. The walk-forward chooses each year with the previous ones and does not
trade if the chosen configuration does not win in training.

```python
REJILLA = [(e, s) for e in (0.08, 0.11, 0.15, 0.20)
           for s in (0.0, 0.05)]
import os

REG = "experimentos/registro_pruebas.jsonl"    # append-only (7.6.1)
os.makedirs("experimentos", exist_ok=True)

def sharpe(x):
    return float(x.mean() / x.std() * np.sqrt(PER))

series = {}
for e, s in REJILLA:
    x = simular(e, s)[0].pct_change().dropna()
    series[(e, s)] = x
    cfg = {"señal": "carry7", "entrada": e, "salida": s,
           "limite": 0.30, "apalancamiento": 3, "coste_pb": 5}
    with open(REG, "a", encoding="utf-8") as f:
        f.write(json.dumps({
            "id": hashlib.sha256(json.dumps(cfg, sort_keys=True)
                                 .encode()).hexdigest()[:10],
            "hipotesis": "the 7-day carry persists",
            "config": cfg, "datos": HUELLAS["cripto3_funding.parquet"],
            "resultado": {"sharpe": round(sharpe(x), 4), "n": len(x)}},
            ensure_ascii=False) + "\n")
with open(REG, encoding="utf-8") as f:
    N = sum(1 for _ in f)

oos = []
for ano in range(2021, 2026):
    ent = {k: sharpe(x[x.index.year < ano]) for k, x in series.items()}
    elegida = max(ent, key=ent.get)
    x = series[elegida][series[elegida].index.year == ano]
    oos.append(x if ent[elegida] > 0 else 0 * x)
    print(ano, "chosen", elegida, f"| out of sample: {100 * x.sum():.1f}%"
          f" (Sharpe {sharpe(x):.1f})")
oos = pd.concat(oos)

def max_esperado(n):
    g = np.euler_gamma
    return ((1 - g) * norm.ppf(1 - 1 / n)
            + g * norm.ppf(1 - 1 / (n * np.e)))

def psr(x, umbral):
    sr = x.mean() / x.std()
    a, k = x.skew(), x.kurt() + 3
    return float(norm.cdf((sr - umbral) * np.sqrt(len(x) - 1) / np.sqrt(
        1 - a * sr + (k - 1) / 4 * sr ** 2)))

sr = pd.Series({k: x.mean() / x.std() for k, x in series.items()})
mejor = max(series, key=lambda k: sharpe(series[k]))
sr0 = sr.std() * max_esperado(N)
print(f"N = {N}; best {mejor}: Sharpe {sharpe(series[mejor]):.1f},"
      f" DSR {psr(series[mejor], sr0):.3f}; walk-forward: Sharpe "
      f"{sharpe(oos):.1f}")
anual = 100 * oos[oos.index.year >= 2023].sum() / 3
print(f"net carry 2023-2025: {anual:.1f}% a year; a bankruptcy with "
      f"30% of the capital inside wipes out {30 / anual:.1f} years")
```
```output
2021 chosen (0.08, 0.0) | out of sample: 23.9% (Sharpe 21.2)
2022 chosen (0.08, 0.0) | out of sample: 7.4% (Sharpe 11.2)
2023 chosen (0.08, 0.0) | out of sample: 7.6% (Sharpe 13.7)
2024 chosen (0.08, 0.0) | out of sample: 7.2% (Sharpe 14.9)
2025 chosen (0.08, 0.0) | out of sample: 6.0% (Sharpe 12.8)
N = 8; best (0.08, 0.0): Sharpe 14.3, DSR 1.000; walk-forward: Sharpe 14.3
net carry 2023-2025: 6.9% a year; a bankruptcy with 30% of the capital inside wipes out 4.3 years
```

```python
informe = {
    "proyecto": "C5", "datos": HUELLAS,
    "estrategia": {"entrada": mejor[0], "salida": mejor[1],
                   "limite_por_exchange": 0.30, "apalancamiento": 3,
                   "coste_pb": 5, "n_pruebas": N},
    "validacion": {"sharpe_walk_forward": round(sharpe(oos), 1),
                   "dsr": round(psr(series[mejor], sr0), 3),
                   "carry_neto_2023_2025_pct": round(anual, 1)},
    "riesgo": {"perdida_dedalo_sin_alarma_pct": round(
                   100 * simular(alarma=False)[1], 1),
               "anos_de_carry_por_quiebra_al_limite": round(
                   30 / anual, 1),
               "dias_liquidacion_3x": margen["3x"]["days XBQ"]
                                      + margen["3x"]["days XEQ"]},
}
informe["decision"] = (
    "paper trading" if informe["validacion"]["dsr"] >= 0.95
    and informe["validacion"]["sharpe_walk_forward"] > 0
    else "stop or redesign")
with open("informe_c5.json", "w", encoding="utf-8") as f:
    json.dump(informe, f, ensure_ascii=False, indent=1)
for parte in ("estrategia", "validacion", "riesgo", "decision"):
    print(parte, json.dumps(informe[parte], ensure_ascii=False))
```
```output
estrategia {"entrada": 0.08, "salida": 0.0, "limite_por_exchange": 0.3, "apalancamiento": 3, "coste_pb": 5, "n_pruebas": 8}
validacion {"sharpe_walk_forward": 14.3, "dsr": 1.0, "carry_neto_2023_2025_pct": 6.9}
riesgo {"perdida_dedalo_sin_alarma_pct": 30.0, "anos_de_carry_por_quiebra_al_limite": 4.3, "dias_liquidacion_3x": 0}
decision "paper trading"
```

The walk-forward chooses the 8% entry with no exit threshold every year, and the out-of-sample Sharpe is 14.3; the
Deflated Sharpe with 8 trials is 1.000. The automatic rule says “paper trading”. The technical report also has to state
the other line of the report, `riesgo` (risk; the report's keys are in Spanish): with 30% of the capital on one
exchange, a bankruptcy wipes out 4.3 years of net carry at the 2023-2025 pace (6.9% a year). That is the number that
decides the size of the limits, and it does not come out of any statistical test.

## Validation

The validation of the carry has two parts that cannot substitute for each other.

**The statistical part**, as in any project of the course: annual walk-forward validation with an expanding window
(lesson 2.5.2), trial log and Deflated Sharpe (2.5.4), or CPCV (2.5.3) if there are more parameters. Here it serves
its purpose: checking that the thresholds are not fitted to the sample and that the carry survives costs in every
year. In the market making variant, the parameters ($\gamma$, $k$, inventory limit) are chosen on some sessions and
measured on others, and the metric includes adverse selection measured separately (2.3.6).

**The scenario part**, which is the one that decides. A carry has a return distribution with many small positive days
and a rare jump: its sample Sharpe is huge as long as the jump is not in the sample. What is validated is the loss in
each scenario and its relation to the carry:

| Scenario | How it is measured | What is recorded |
|---|---|---|
| Bankruptcy of an exchange | Capital on it when withdrawals close | Loss and years of carry it wipes out |
| Liquidation of the short leg | Rise of the mark between margin top-ups | Days that would liquidate at each leverage |
| Prolonged negative funding | Consecutive weeks paying (2022 in the data) | Loss and turnover of the hysteresis |
| Orphaned leg | Failure of the second order | Cost of unwinding and frequency in paper trading |
| Spot dislocation | Discount of one exchange against the index (3.7.2) | Hedge loss if its spot is used |

The stopping rules, dated in the technical report before week 4: out-of-sample net carry not positive in the last
year; DSR below 0.95 with the number of trials recorded; a bankruptcy at the limit that wipes out more than five years
of net carry, unless the limit is lowered; any day of liquidation at the chosen leverage in the available history. The
thresholds are the course's criteria, not a regulation.

## Risks and paper trading

What really brings down a crypto arbitrage desk:

- **Counterparty.** Bankruptcy, frozen withdrawals, commingling of client funds. The FTX case, according to the CFTC
  and SEC complaints, is summarized in lesson 3.7.2. Defenses: limits, withdrawing profits, exchanges authorized under
  MiCA (7.4.4), periodic withdrawal tests and the alarm.
- **Liquidation of the short leg** due to a fast rise with the margin not topped up, or to a wick in the last price on
  an exchange that liquidates on it instead of on the mark (3.7.1).
- **Orphaned and duplicated legs.** An order that goes through on one exchange and not on the other, or a retry that
  duplicates the order after a timeout (3.7.4).
- **Funding that changes sign** and leaves the desk paying while the hysteresis decides.
- **Keys.** A leaked key with withdrawal permission empties the account; the executor's keys only trade, and
  withdrawal addresses are on a whitelist configured by hand (4.4.5).
- **Market making**: adverse selection, inventory that builds up in a sell-off and quotes that stay live when the
  process crashes (cancel-on-disconnect, if the exchange offers it; 4.5.2).

The limits and the kill switch: maximum capital per exchange, maximum leverage, minimum distance to the liquidation
price, maximum number of orders per minute, maximum daily loss (4.2.5), and the state of each counterparty in the
diagram above. An exchange in `WITHDRAW` receives no orders that increase exposure.

The plan for the four weeks of paper trading, on the demo accounts of at least two exchanges:

1. **Every funding period**: download of the published funding, signal, allocation, orders for both legs and
   reconciliation of orders, balances and positions (4.5.5). An unexplained difference blocks the next period.
2. **Every day**: margin and distance to liquidation per position, state of each counterparty, hedge (spot versus
   perpetual) and slippage against the 5 bp assumption (4.5.3).
3. **Every week**: carry collected versus simulated for the same dates, executor incidents (orphaned legs, rejections,
   rate limits) and a deliberate failure test (cutting the connection halfway through an opening).
4. **At the end**: report with the differences between the simulator and what was executed. Four weeks say nothing
   about the tail; they say whether the executor, the reconciliation and the monitor work.

## Milestones and rubric

The schedule is that of guide 7.6.1: 40 to 80 hours over about 10 weeks, with the four weeks of paper trading in
parallel with the technical report.

| Week | Verifiable deliverable |
|---|---|
| 1 | Template repository; funding loader with fingerprints; counterparty register with the 7.4.4 check |
| 2 | Signal, simulator with costs, margin and hedge; known answer of level 3 reproduced |
| 3 | Allocation with limits and alarm; bankruptcy scenario; stopping rules written and dated |
| 4 | Walk-forward, DSR and scenarios; decision to move to paper trading |
| 5 | ccxt executor against the simulator: two legs, unwind, idempotency, reconciliation, with tests |
| 6 | Demo accounts, keys without withdrawal, counterparty monitor and alerts |
| 7-10 | Paper trading, reconciliation per period and monitor; technical report; defense rehearsal |

The rubric is the course's common one (the course's own criteria, not an external standard), with the evidence it
asks for in C5:

| Criterion | Weight | Evidence in C5 |
|---|---|---|
| Reproducibility | 15% | `uv.lock`, Docker, one command; data fingerprints; deterministic simulator with its own clock |
| Data | 15% | Funding with its publication time, defunct exchanges included (no survivorship bias), dated profiles |
| Anti-overfitting validation | 20% | Walk-forward, DSR with the log, bankruptcy and liquidation scenarios with their loss |
| Risk and execution | 15% | Costs, margin, per-counterparty limits, alarm, two-leg executor with unwind and reconciliation |
| Paper trading and monitoring | 15% | Four weeks on demo, reconciliation per period, counterparty state |
| Code quality | 10% | Tests of the executor with injected failures, of the simulator and of the allocation; lint; configuration outside the code |
| Technical report and defense | 10% | 7.6.1 template; limits justified with the bankruptcy scenario |

Mistakes that fail the project on their own, in their C5 form: demonstrable look-ahead (using a period's funding to
decide the position that collects it); results that do not reproduce with the documented command; a backtest without
costs or without margin; API keys in the repository or in its history, all the more so with withdrawal permission;
hiding the number of trials or choosing the period after the fact (for example, starting in 2023 so as not to see the
bankruptcy, or removing `dedalo` from the universe, which is survivorship bias); fewer than four weeks of paper trading.

## Exercises

1. The counterparty alarm fires on a false rumor about `alfa` on March 4, 2024 and the capital is withdrawn for a
   week. How much does that false alarm cost compared with the skeleton?

**Solution.**

```python
falsa = [("alfa", pd.Timestamp("2024-03-04", tz="UTC"),
          pd.Timestamp("2024-03-11", tz="UTC"))]
s_f, _, _ = simular(vetos=falsa)
coste = 1 - s_f.iloc[-1] / base.iloc[-1]
b24 = base[base.index.year == 2024]
diario = (b24.iloc[-1] / b24.iloc[0] - 1) / 365
print(f"final capital: {base.iloc[-1]:.4f} without the false alarm, "
      f"{s_f.iloc[-1]:.4f} with it")
print(f"cost: {100 * coste:.3f}% of the capital, about "
      f"{coste / diario:.1f} days of 2024 carry")
```
```output
final capital: 2.2940 without the false alarm, 2.2920 with it
cost: 0.087% of the capital, about 4.3 days of 2024 carry
```

The false alarm costs 0.087% of the capital, about 4.3 days of 2024 carry: the `alfa` legs are closed and reopened
(four sides with costs) and for a week part of the capital sits unused, because the 30% limit prevents moving what
leaves `alfa` to the other exchanges. Compared with the 30% lost in a bankruptcy at the limit, an alarm that fires by
mistake several times a year is still cheap. What has to be watched is that it does not fire so often that the system
spends more time out than in, and each trigger has to be logged with its reason to calibrate it.

2. If you choose market making: add to the Avellaneda-Stoikov simulator from lesson 2.3.6 a hard inventory limit (do
   not quote the side that would exceed it) and a maker fee. How much of the result is left?

**Solution.**

```python
flujo = pd.read_parquet("mm2_flujo.parquet")
medio = pd.read_parquet("mm2_medio.parquet")
inicio = pd.Timestamp("2025-03-12 13:30:00", tz="UTC")
T = len(medio)
m = np.r_[medio["medio"].iloc[0], medio["medio"].to_numpy()]
seg = np.floor((flujo["ts"] - inicio).dt.total_seconds()).astype(int)
seg = seg.clip(0, T - 1).to_numpy()
signo = np.where(flujo["lado"] == "compra", 1, -1)
alcance = flujo["alcance"].to_numpy()
por_seg = [[] for _ in range(T)]
for i, s in enumerate(seg):
    por_seg[s].append(i)

def creador(q_max, comision, gamma=1e-4, k=35.4, sigma=0.0065):
    """Avellaneda-Stoikov (2.3.6) with a hard inventory limit and a
    maker fee on the cash amount of each fill."""
    q, caja, n = 0, 0.0, 0
    for s in range(T):
        resto = T - s
        r = m[s] - q * gamma * sigma ** 2 * resto
        semi = 0.5 * (gamma * sigma ** 2 * resto
                      + 2 / gamma * np.log(1 + gamma / k))
        bid = np.floor((r - semi) / 0.01 + 1e-9) * 0.01
        ask = np.ceil((r + semi) / 0.01 - 1e-9) * 0.01
        for i in por_seg[s]:
            if (signo[i] == 1 and q - 100 >= -q_max
                    and alcance[i] >= ask - m[s] - 1e-9):
                q, caja = q - 100, caja + 100 * ask * (1 - comision)
                n += 1
            elif (signo[i] == -1 and q + 100 <= q_max
                    and alcance[i] >= m[s] - bid - 1e-9):
                q, caja = q + 100, caja - 100 * bid * (1 + comision)
                n += 1
    return {"result": caja + q * m[T], "fills": n}

pd.DataFrame({
    "no limit, no fee": creador(10 ** 9, 0.0),
    "limit 1000, no fee": creador(1000, 0.0),
    "limit 1000, maker 1 bp": creador(1000, 1e-4),
    "limit 1000, maker 2 bp": creador(1000, 2e-4),
}).T.round(1)
```
```output
                         result    fills
no limit, no fee        38061.0  13169.0
limit 1000, no fee      38135.0  13163.0
limit 1000, maker 1 bp  31748.4  13163.0
limit 1000, maker 2 bp  25361.7  13163.0
```

With Avellaneda-Stoikov the inventory already stays small (in 2.3.6 it never went above 1,700 shares), so the limit of
1,000 barely changes the result: 38,135 USD versus 38,061. The fee does: each basis point of maker fee on the traded
cash amount (about 64 million USD in 13,163 fills of 100 shares) takes about 6,400 USD, 17% of the result. Each
exchange's fee structure (maker, taker, volume discounts) decides whether market making makes sense, and it is read on
its official page with the date. The hard limit is still mandatory: it protects against what the model does not
foresee, such as a price jump or an informed flow that does not stop.

3. Before depositing capital on a new exchange, what goes into its profile in the counterparty register and which
   signals feed its state in the monitor?

**Solution.**

In the profile: legal entity and LEI, country, authorization as a crypto-asset service provider under MiCA and its
check in the ESMA and CNMV registers with the date (lessons 3.7.5 and 7.4.4), segregation of client assets according
to the terms of the contract, services that will be used, capital limit and its justification with the bankruptcy
scenario, permissions of each key (no withdrawal), address whitelist, result of the last withdrawal test and date of the
next review. In the monitor: delays or failures in test withdrawals, spot discount against the index of the other
exchanges (3.7.2), funding very different from the rest without a market cause, API outages beyond the usual, news about
solvency or supervisory actions, and changes in the ESMA register or in the list of unauthorized entities (7.4.4). Weak
signals move to `WATCH`; a strong one, or several weak ones at once, to `WITHDRAW`.

## Self-assessment

1. The best configuration of the skeleton has a Sharpe of 14.3 and a Deflated Sharpe of 1.000 with 8 trials. What do you conclude?

   - A. That the strategy is validated and can be traded with high leverage
   - B. That statistical validation does not see the risk that matters: the sample without the bankruptcy has no jumps, and counterparty risk is validated with scenarios and limits
   - C. That there is look-ahead in the code
   - D. That more configurations must be tested so the DSR goes down

   *Answer B: That statistical validation does not see the risk that matters: the sample without the bankruptcy has no jumps, and counterparty risk is validated with scenarios and limits.* A carry collects little and almost always; its loss arrives all at once and rarely (a bankruptcy, a liquidation). The sample Sharpe measures the calm part. With the dedalo bankruptcy included and no alarm, the same system loses 30% of its capital in one day: that is what has to be defended before the panel.

2. On cenit, the short perpetual leg returns a 503 error after the spot has been bought. What does the skeleton's executor do?

   - A. It retries the perpetual sale until it goes through
   - B. It keeps the spot bought and waits for the next period
   - C. It opens the short on another exchange
   - D. It unwinds the spot leg, logs the incident and reconciles: net position zero at the cost of the round trip

   *Answer D: It unwinds the spot leg, logs the incident and reconciles: net position zero at the cost of the round trip.* A single leg is a directional bet the strategy does not want to hold. Retrying blindly after an error can duplicate orders (lesson 3.7.4); hedging on another exchange changes the counterparty without a prior decision. Unwinding costs 29.4 USDQ in the example and leaves the book square.

3. With the capital spread out and a limit of 30% per exchange, but no alarm, what happens in November 2022?

   - A. The part that was on dedalo when withdrawals were suspended is lost: 30% of the capital, the whole limit
   - B. Nothing: the limit prevents any loss
   - C. All the capital is lost
   - D. Only one week of funding is lost

   *Answer A: The part that was on dedalo when withdrawals were suspended is lost: 30% of the capital, the whole limit.* The limit bounds the loss, it does not prevent it: the capital that was on dedalo on November 8 at 12:00 was trapped. What prevents it in the skeleton is the counterparty alarm, which withdraws the capital on the rumors of the 6th, before the suspension.

4. With daily margin top-ups, how many days in the period would have liquidated the short leg at 3x and at 10x?

   - A. None in either case
   - B. Many at 3x and none at 10x
   - C. None at 3x (it would take a 32.7% rise in the mark in one day) and 53 between XBQ and XEQ at 10x
   - D. One at 3x and one at 10x

   *Answer C: None at 3x (it would take a 32.7% rise in the mark in one day) and 53 between XBQ and XEQ at 10x.* The worst one-day rise in the mark was 15.9% on XBQ and 18.1% on XEQ, far from the 32.7% that liquidates at 3x. At 10x a 9.5% rise is enough, and that happened on 16 days on XBQ and 37 on XEQ. The last points of return from leverage are paid for with liquidations.

5. Which of these facts fails the project on its own, according to the common rubric of 7.6.1?

   - A. That the net carry from 2023 to 2025 is only 6.9% a year
   - B. An API key with withdrawal permission stored in the repository's git history
   - C. Choosing market making instead of arbitrage
   - D. That paper trading shows higher slippage than assumed

   *Answer B: An API key with withdrawal permission stored in the repository's git history.* Credentials in the repository or in its history fail the project on their own, and a key with withdrawal permission is the worst of all (lesson 4.4.5). A modest carry or higher slippage than expected are results that are documented and defended.

## Key points

- C5 is a crypto arbitrage or market making desk whose main risk is not the price but the counterparties: the exchanges hold the capital.
- The skeleton collects funding with long spot and a 3x short perpetual on the exchanges that pay more than the formula's floor, with 5 bp per side and daily margin top-ups: 14.8% a year, falling to 6-8% from 2022.
- Facing the dedalo bankruptcy, concentrating on the highest payer loses everything; spreading with a 30% limit loses 30%; only the alarm that withdraws the capital on the rumors avoids the loss.
- The executor sends both legs with its own identifiers, unwinds an orphaned leg and reconciles against what the exchange reports before the next period.
- A Sharpe of 14 and a DSR of 1.000 do not validate a carry: they measure the calm part. With 30% of the capital on one exchange, a bankruptcy wipes out 4.3 years of net carry.
- Paper trading runs on the exchanges' demo accounts, with keys without withdrawal permission and monitoring of each counterparty.

## Resources

### Research

- [Limit Order Book - Aggregated Market Liquidity](https://pythonparatrading.com/en/limit-order-book-and-market-liquidity/) · Research-0008 · 11 min

### Documentation

- [ccxt: repository (MIT license)](https://github.com/ccxt/ccxt) · github.com
- [ccxt: documentation](https://docs.ccxt.com/) · docs.ccxt.com
- [Bybit API v5: Get Funding Rate History](https://bybit-exchange.github.io/docs/v5/market/history-fund-rate) · bybit-exchange.github.io
- [ESMA: Markets in Crypto-Assets Regulation (MiCA)](https://www.esma.europa.eu/esmas-activities/digital-finance-and-innovation/markets-crypto-assets-regulation-mica) · esma.europa.eu

### Books

- [He, S., Manela, A., Ross, O. and von Wachter, V. (2022): Fundamentals of Perpetual Futures. arXiv:2212.06888](https://arxiv.org/abs/2212.06888) · arxiv.org
- [Avellaneda, M. and Stoikov, S. (2008): High-frequency trading in a limit order book](https://www.math.nyu.edu/~avellane/HighFrequencyTrading.pdf) · math.nyu.edu

Previous lesson: [7.6.5 · Capstone C4: a research platform with LLMs and alternative data](https://pythonparatrading.com/en/python-trading-course/level-7/capstone-llm-research-platform/)
