Proč většina paper-trading systémů lže

2026-05-02 · realistic-fill model · ~7 min

Když si někdo postaví backtest engine a paper-trading bota, většinou v nějaké podobě dělá tuhle chybu:

def execute_close(position):
    fill_price = market.last_price
    pnl = (fill_price - position.entry) * position.size

To je broken. last_price je cena posledního trade-u, ne cena za kterou bys ty teď prodal.

Co je last_price doopravdy

Na orderbooku existují dvě hodnoty:

Když chceš prodat okamžitě, dostaneš bid. Když chceš koupit okamžitě, platíš ask. last_price je jen cena posledního trade-u — může být kdekoliv mezi nimi, nebo dokonce úplně mimo current orderbook když market je tenký.

Rozdíl mezi bid a ask se jmenuje spread. Na velmi likvidním S&P 500 ETF je 0.01%. Na tenké Polymarket "longshot" market je extrémně velký.

Konkrétní případ: Polymarket longshot

Vezmi market "Will Toronto Raptors win the 2026 NBA Finals?" Pre-playoff je outcome NO na ceně ~99¢. Outcome YES je longshot — třeba 0.5¢.

Backtest engine vidí last_price = 0.005 a počítá s tím že prodáš za $0.005. Realita:

SidePrice
Bid$0.003
Last$0.005
Ask$0.008

To je 60% spread. Když máš 1000 shares co backtest hodnotí na $5.00 (1000 × $0.005), reálně dostaneš za market sell $3.00. Backtest přefoukl PnL o 67%.

Ještě horší — partial fill na tenkém marketu

Bid může být $0.003 ale jen na 200 shares. Zbylých 800 shares prodáš v hlubším orderbook layeru — třeba $0.001. Average fill = $1.40 za 1000 shares. Backtest pořád říká $5.00.

Tohle je důvod proč backtest křivky vypadají krásně, ale live PnL je proti.

Jak to řeším v BotLab engine

V /opt/copybot/copybot/risk.py mám funkci realistic_fill_price():

def realistic_fill_price(price: float, side: str, size_usd: float) -> float:
    """Bid-ask spread model na základě polymarket data analysis."""
    if price < 0.05:
        spread_pct = 0.20      # 20% spread na pennies
    elif price < 0.10:
        spread_pct = 0.10
    elif price < 0.20:
        spread_pct = 0.05
    elif price <= 0.80:
        spread_pct = 0.013     # tightest band 1.3%
    elif price < 0.95:
        spread_pct = 0.03
    else:
        spread_pct = 0.05      # near-cert tail

    half = price * spread_pct / 2
    if side == "BUY":
        fill = price + half     # paying ask
    else:
        fill = price - half     # receiving bid

    # Size impact (linear penalty over $20)
    if size_usd > 20:
        impact = (size_usd - 20) / 1000 * 0.02
        if side == "BUY":
            fill += price * impact
        else:
            fill -= price * impact

    return max(0.001, min(0.999, fill))

Empiricky kalibrováno na 30 dní polymarket orderbook snapshots. Mid-band ($0.20-$0.80) je nejlikvidnější — 1.3% spread reálně. Penny ranges mají blow-up spread protože market makers se tam nemají rádi.

Konkrétní dopad: Bot F counter-factual

Bot F je longshot-lottery strategie — kupuje predikční trhy s prices $0.01-$0.05 kde scanner detekoval mispriced upside. Před implementací realistic-fill, paper PnL ukazoval $2,359.

Po refactoru engine na realistic-fill: $1,376.91. 42% reduction.

Ten zbytek mezi nimi byl simulated alpha — number co bych live nikdy neviděl. Backtest mě klamal že strategie umí 5x to co umí.

"Paper PnL co počítá s last_price je placebo. Paper PnL s realistic-fill je nejlepší proxy live results bez nasazení reálných peněz."

Co tohle znamená pro CZ trading content

Když ti někdo na YouTube ukazuje MT4 backtest s 90% win rate a Sharpe 3.0, zeptej se:

Pokud na žádnou z těch otázek neumí jasně odpovědět, ten backtest je fan fiction. Ne mock-up live tradingu.

Open question

Realistic-fill model na Polymarketu je relativně jednoduchý — orderbook je veřejný, lze kalibrovat. Pro Forex je to násobně složitější: každý broker má jiný spread, weekend gaps, news-event spread blow-ups, Asian session vs London session differences.

Můj forex-bot to teď dělá hrubě — fixed spread 0.5 pip pro EURUSD/GBPUSD, 1.0 pip pro USDJPY/CHF, 1.5 pip pro AUD/NZD/CAD. Adekvátní pro daily trades, hrubá pro M1 scalping. To bude další iterace.

Engine kód je veřejný na GitHubu. Když najdeš v fill modelu chybu, otevři PR.