This is the single source of truth for how the backtest engine behaves. Every backtest report on uncoded.ch/backtesting pins which revision applied via the
engine_version field in its frontmatter — the per-report MDX no longer inlines this spec to keep exports lean.Engine version
1.1 — see §23 Changelog for history. Behaviour changes bump this number; older revisions are archived as ENGINE-v1.x.md.Scope
Spot-only, single-symbol-per-subprocess, USDT/FDUSD pairs, Binance-style fills. No futures, no margin, no indicators — pure price-distance triggers.
Order types
Limit-Buy, Limit-TP-Sell, Market-Stop-Loss, Market-Trailing-SL. See §3 Order Types.
Reproducibility
Deterministic given identical (candles, config, engine_version). See §16 Determinism.
How to read this document
Sections are numbered. Per-backtest reports cite this spec by section (e.g. “see ENGINE.md §4.5 for TSL-skips-TP”) so any LLM consuming a report can resolve behaviour questions without leaving the documentation set. The document is mirrored fromENGINE.md in the source repo; this Mintlify page is the canonical published version. If the two ever diverge, the in-repo file wins — open a PR to update this page.
Statisches Verhalten der Backtest-Engine. Perengine_version:engine_versionin MDX-Reports referenziert. Verhaltensänderung → Version bumpen, alte Spec archivieren (ENGINE-v1.x.mdetc.). Diese Spec beschreibt was die Engine tut, nicht wie sie implementiert ist. Quelle:BacktestingTest-main/backtester_numba.py(numba-jit’d Python, 1894 Zeilen), Orchestration viabacktest-api/app/services/backtest_runner.py, Mode-Configs untertrading-bot/modes/*.json. Werte ohne Marker sind aus dem Code verifiziert.?markiert verbleibende Unklarheiten.
1.1 last_updated: 2026-05-04 maintainer: aureumvictoria code_revision: backtester_numba.py — ULTRA++ EDITION (array-of-lists orders)
0. Geltungsbereich
Diese Spec gilt für: Spot-Krypto-Backtests (Binance-Style, USDT/FDUSD-Pairs), Single-Symbol pro Subprocess (ein Run = ein Symbol = ein eigener Cash-Pool). Unterstützte Order-Typen: Limit-Buy, Limit-TP-Sell, Market-SL, Market-Trailing-SL. Strategie-Logik: rein preisbasiert über prozentuale Distanz zulastBuyPrice — keine Indikatoren (kein RSI, MACD, MA, etc.). Modes sind reine Parameter-Presets.
Nicht abgedeckt: Futures/Perpetuals, Options, Margin/Leverage, Funding, Borrowing, Stop-Limit, OCO, Iceberg, Multi-Symbol-Portfolios mit gemeinsamem Cash, indikator-basierte Strategien.
1. Zeit- und Datenmodell
1.1 Candle-Definition
- Bar-Stempel: Open-Time (Bar-Beginn). Aus dem Input-DataFrame durchgereicht.
- Timezone: UTC, ISO-8601 mit Offset (
pd.to_datetime(..., utc=True)). - Granularität: vom Provider abhängig (1s nativ unterstützt — Header-Kommentar nennt “1s candles”).
- OHLCV-Reihenfolge innerhalb einer Bar: wird über
intrabar_modemodelliert (siehe §4.4) — Engine nimmt keine Annahme über tatsächlichen intra-bar Pfad, sondern segmentiert deterministisch in 3 monotone Abschnitte.
1.2 Datenquellen
- Preisquelle (API-Pfad): Postgres-Tabelle
backtest_candles, exportiert pro Run viaexport_to_csvintmp/candles.csv. - Auflösungen:
1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d. Default1mwenn nicht erkannt. - Resampling: keines — die DB hält die Auflösung nativ pro Symbol.
- DataFrame-Spalten:
timestamp, open, high, low, close(volume optional, nicht ausgewertet).
1.3 Look-Ahead-Schutz
- Garantie: Signal aus Bar T fillt innerhalb von Bar T, aber niemals zu einem Preis jenseits des aktuellen Segments. Es gibt keine Verzögerung “Signal T → Fill T+1”.
- Mechanismus: Jede Bar wird in 3 monotone Segmente zerlegt; Fills nur innerhalb des durchlaufenen Preisbereichs (
in_range(a, b, price)). - Indikatoren: Engine berechnet keine Indikatoren — Strategie ist rein preisbasiert (Trigger via prozentuale Distanz zu
lastBuyPrice).
1.4 State-Initialisierung
lastBuyPrice: aufdf['open'].iloc[0]— also den Open der ersten Bar. Nicht Close, nicht Mid.base_bal:0.0.quote_bal:config.get('startBal', 10000.0).lastTriggerIndex:-10**9(sentinel “noch nie getriggert”).
1.5 Daten-Lücken / fehlende Bars
- Missing Bar: keine Sonderbehandlung. Bars werden as-is verarbeitet.
- Zero-Volume-Bar: wird normal prozessiert; falls
O=H=L=C, filltin_rangenur, wenn Order exakt auf diesem Preis sitzt. - Stale-Data-Schwelle: keine.
- Empty DataFrame: Engine wirft
ValueError("Backtester received empty DataFrame…")und beendet.
1.6 Tote / unbenutzte Felder
order_latency_seconds(Default0engine,2runner): wird gelesen, aber niemals ausgewertet. Alle Orders sind sofort aktiv abplaced_idx(bo_active_from = placed_idx). Kommentar im Code: “limit orders on Binance. They fill immediately when price hits — no latency.”start(DefaultTrue): wird gelesen (safe_bool), aber im Engine-Loop nicht weiter verwendet. Vermutlich Live-Bot-only.
2. Signal-Pipeline (Bar-Loop)
Pro Bari in dieser Reihenfolge:
2.1 Segmentierung
Defaultintrabar_mode = "OLHC" (im API-Runner; Engine-internes Default ist "OHLC"):
- Segment A:
O → L - Segment B:
L → H - Segment C:
H → C
intrabar_mode = "OHLC":
- Segment A:
O → H - Segment B:
H → L - Segment C:
L → C
2.2 Symbol-Reihenfolge bei Multi-Asset
Nicht relevant — die Engine läuft pro Subprocess auf genau einem Symbol. Multi-Symbol = mehrere parallele Subprocesses.3. Order-Typen
| Typ | Unterstützt | Trigger | Fill-Preis (Effektiv) |
|---|---|---|---|
| Limit Buy | ja | Low ≤ Limit im Segment | Limit × (1 + half_spread) |
| Limit TP-Sell | ja | High ≥ TP im Segment | TP × (1 − half_spread) |
| Market Stop-Loss | ja | Preis durchläuft SL-Level im Segment | SL × (1 − 2·half_spread) (Bid-Side, voller Spread) |
| Market Trailing-SL | ja | Preis ≤ TSL-Level (nach Aktivierung) | TSL × (1 − 2·half_spread) |
| Stop-Limit | nein | — | — |
| OCO | nein | — | — |
| Iceberg | nein | — | — |
| Time-in-Force | implizit GTC | bleiben aktiv bis Fill, Cancel oder Backtest-Ende | — |
half_spread = assumed_spread_bps / 10000 / 2 (siehe §6.3).
Order-Aktivierung: alle Orders sind sofort ab placed_idx aktiv (active_from = placed_idx). Es gibt keine “pending” oder “untriggered” Phase, abgesehen von sellActivation (siehe §12).
4. Candle-interne Ausführungslogik
4.1 Auswertungsreihenfolge pro Segment
- Phase 1 — Catch-Up-Cascade: Trigger-Generierung von
lastBuyPricezum Segment-Start-Preis (handhabt Gaps zwischen Bars). - SellActivation-Refresh: falls aktiviert, basierend auf Segment-Start.
- Phase 2a — TP-Limit-Sells (
maker_feefalls nichtonlyMakerSell-blockiert; TSL-Orders skippen TP-Fills komplett, siehe §4.5). - Phase 2b — Stop-Loss (
taker_fee, fallsstopLoss=True). - Phase 2c — Trailing-SL (
taker_fee, sofern aktiviert; fallsonlyMakerSell→maker_rateals Override). - Phase 2d — Limit-Buys (
maker_feefalls nichtonlyMakerBuy-blockiert). - Phase 3 — Movement-Cascade: Trigger-Generierung von
lastBuyPricezum Segment-End-Preis.
4.2 SL und TP in derselben Bar
- Regel: TP-Phase läuft vor SL-Phase im selben Segment (Phase 2a vor 2b).
- Konsequenz mit OLHC (Default): In Segment A (O→L) liegt typischerweise nur der SL. TP-Sieg-Regel greift hier nicht, weil TP gar nicht im Segment ist. In Segment B (L→H) wird TP getroffen, falls High es erreicht — hier gewinnt TP über SL.
- Faustregel OLHC: Wenn SL und TP beide in derselben Bar liegen, verliert der Trade strukturell — der SL feuert in Segment A, bevor das High kommt.
- Mit
OHLC: umgekehrt — TP gewinnt strukturell, weil High vor Low erreicht wird. - Nach Fill wird sowohl der TP-Eintrag als auch der zugehörige SL-Eintrag aus den Order-Indizes entfernt.
4.3 Entry und Exit in derselben Bar
- Same-Bar-Round-Trip ist erlaubt. Buy in Segment A → Sell in Segment B oder C möglich.
- Min-Hold-Time: keine.
- Within-Segment-Schutz: ein Sell kann nicht im selben Segment fillen, in dem der Buy gefillt wurde (
if so_placed_idx == bar_index AND segment_index <= so_created_seg: continue).
4.4 Intra-Bar-Pfad-Annahme
- Engine-Default:
intrabar_mode = "OHLC"(optimistisch). - API-Runner-Default:
intrabar_mode = "OLHC"(pessimistisch im ersten Segment). - Innerhalb eines Segments wird
in_range(a, b, price)benutzt — Order fillt sobald Segment den Preis berührt, unabhängig von der tatsächlichen Tick-Reihenfolge im Segment.
4.5 TSL-Order skippt TP-Fills
Wichtig: Sobald ein Sell-Order eine TSL-Komponente hat (tsl_drop_pct > 0), wird er von der TP-Fill-Logik komplett übersprungen (if tsl_enabled and so_tsl_drop_pct > 0: continue). Der einzige Exit-Pfad für diesen Split ist:
- TSL feuert (Preis ≤ TSL-Level nach Aktivierung)
- SL feuert (falls
stopLoss=True)
5. Gap-Behandlung
- Limit-Order durch Gap: fillt am Limit-Level, nicht am Open. Kein Price-Improvement.
- Stop-Loss durch Gap: fillt am SL-Level mit Bid-Side-Spread. Kein Gap-Loss-Modell — der reale Gap-Verlust wird nicht abgebildet.
- TSL durch Gap: fillt am TSL-Level mit Bid-Side-Spread. ATH wird auf
tslActivationgesetzt (nicht aufseg_high), um zu vermeiden, dass ein Gap die TSL-Referenz künstlich nach oben treibt. - Catch-Up-Cascade bei großen Lücken: Phase 1 jedes Segments triggert nachträglich alle ausgelassenen Buy-Trigger zwischen
lastBuyPriceund Segment-Start (bis zum Cap von 10000, siehe §10.4). - Trading-Halt / Daten-Lücke: nicht modelliert.
⚠ Realistisch betrachtet eine optimistische Annahme bei großen Gaps. Für Krypto-24/7-Pairs auf 1m–1h selten relevant.
6. Gebühren, Slippage, Funding
6.1 Fees
- Modell:
maker_fee_bps/taker_fee_bps. - Engine-Default:
maker=1.0,taker=7.5bps. - API-Runner-Default:
maker=7.5,taker=7.5bps (überschreibt Engine). - Klassifizierung:
- Maker: Limit-Buys, Limit-TP-Sells.
- Taker: Market-Stop-Loss, Market-Trailing-SL.
- Override:
onlyMakerBuy=True/onlyMakerSell=Truesiehe §6.6.
- Anwendung: beidseitig (Entry und Exit) auf Notional.
- Mindestgebühr: keine.
- Konvertierung:
bps_to_frac(bps) = bps / 10000.
6.2 fees_in_quote — exakte Formeln
True (Default):
- Buy:
fee_quote = price × qty × maker_rate;cost = price × qty + fee_quote; Cash-Abzugquote_bal -= cost;base_received = qty. - Sell (TP):
gross_quote = price × qty;fee_quote = gross_quote × maker_rate;net_quote = gross_quote − fee_quote;quote_bal += net_quote. - Sell (SL/TSL): identisch zu TP, aber
taker_rateund Bid-Cross.
False:
- Buy:
fee_base = qty × maker_rate;base_received_raw = qty − fee_base;base_received = floor_to_step(base_received_raw);cost = price × qty(Fee in Base);quote_bal -= cost. - Sell (TP/SL/TSL):
fee_base = qty × fee_rate;qty_after_fee = max(qty − fee_base, 0);quote_bal += price × qty_after_fee.
6.3 Slippage / Spread
assumed_spread_bps(Default1.5) wirkt als halbierter Spread auf Fill-Preise.half_spread_frac = (assumed_spread_bps / 10000) / 2.- Effektiv-Preise:
_effective_buy_price(p) = p × (1 + half_spread_frac)_effective_sell_price(p) = p × (1 − half_spread_frac)← Limit-Sell (Maker)_market_sell_price(p) = p × (1 − 2 × half_spread_frac)← Market-Sell (voller Bid-Ask-Cross)
- Keine zusätzliche Slippage-Komponente (kein Volumen-Impact, kein Order-Book).
6.4 Funding — nicht modelliert (Spot-Engine).
6.5 Borrowing / Margin — nicht modelliert. 1× Spot only.
6.6 onlyMakerBuy / onlyMakerSell
onlyMakerBuy=True: Wennprice_rounded ≥ current_price_at_trigger, wird der Buy stillschweigend übersprungen (würde Liquidität nehmen).onlyMakerSell=True: Bei Market-Sells (SL/TSL) wirdmaker_ratestatttaker_rateangewendet — die Order wird trotzdem gefillt, aber zum Maker-Tarif. Kein analoger Skip-Mechanismus wie bei Buys.
7. Position-Sizing
7.1 buyPercentage — Trigger, nicht Sizing
- Bedeutung: Prozentuale Preis-Distanz zu
lastBuyPrice. Wennprice ≥ lastBuyPrice × (1 + pct), feuert ein UP-Trigger; wennprice ≤ lastBuyPrice × (1 − pct), ein DOWN-Trigger. - Cascade: in einer einzelnen Cascade-Auswertung können bis zu 10000 Trigger nacheinander feuern (Cap, siehe §10.4).
- Richtung: UP-Trigger nur wenn
canBuyUp=True; DOWN-Trigger nur wenncanBuyDown=True. lastBuyPriceUpdate: nach jedem Trigger auf den aktuellen Preis (nicht die Trigger-Schwelle).
7.2 Splits & Volumen
buyVolumes: Liste von Prozentsätzen. Werden zu Gewichten normalisiert:buyWeights[i] = buyVolumes[i] / 100(NICHT/sum(buyVolumes)— Live-Bot-Match).buySplits: Anzahl Splits (=len(buyVolumes)). Engine-Default7.- Validierung:
len(buyVolumes) == buySplits == len(sellPercentages)— sonstValueError.sum(buyVolumes) > 0— sonstValueError. investmentPerBuy: fixe Quote-Menge pro Trigger; pro Split:inv_split = investmentPerBuy × buyWeights[i].investmentPercentMode=True: ersetzt fixe Menge durchpool = (quote_bal − locked_quote) × investmentPerFreeQuotePercent / 100; danninv_split = max(pool × buyWeights[i], minInvestmentPerQuote × buyWeights[i]).locked_quote: Σbo_price × bo_qtyaller offenen Buy-Orders.minInvestmentPerQuote: untere Schranke im Percent-Mode.
7.3 Hard-Limits (Binance-Filter & Engine-Caps)
tickSize: Preis-Rundung — Buyfloor, TP-Sellceil, SL/TSLfloor. Default1e-8.stepSize: Quantity-Rundung. Buy-Qty wirdceilzur Step-Size gerundet (Live-Bot-Match), Sell-Qty wirdfloor. Default1.minNotional: Orders mitprice × qty < minNotionalwerden stillschweigend übersprungen. API-Runner-Default1.0.dontBuyBelowQuoteAssetBalance: wennquote_bal < dontBuyBelowQuoteAssetBalance, blockiert kompletten Trigger.- PERCENT_PRICE_BY_SIDE-Filter (Binance, hart einkodiert): Buys mit
|price_rounded − current_price| / current_price > 5%werden übersprungen. Kein Config-Feld — Hard-Cap. - Cascade-Cap: maximal 10000 Trigger pro
_cascade_to_price-Call. - Max offene Positionen: kein hartes Limit; begrenzt durch Cash, Distance-Check und PERCENT_PRICE-Filter.
7.4 Cash-Knappheit
cost > quote_bal: Split wird übersprungen, kein Fehler, kein Log.- Parallele Triggers: sequenziell pro Bar — wer zuerst kommt, kriegt zuerst Cash.
7.5 Hebel / Liquidation — Hebel 1× Spot, fix. Liquidation nicht modelliert.
7.6 Duplikat-Prevention
_should_place_new_order_based_on_existing: pro Split-Index prüft die Engine, ob bereits eine offene oder gefüllte Buy-Order inmin_distanceexistiert.min_distance = max(buyPercentage / 100 × current_price, tickSize)— dertickSize-Floor ist wichtig für Low-Price-Tokens (PEPE, SHIB).- Falls duplikat-nahe: Split wird übersprungen.
8. Risk-Management (SL / TP / Trailing)
8.1 Stop-Loss
- Aktivierung:
stopLoss=True(parsed viasafe_bool) +stopLossPercentage. Engine-DefaultstopLoss=False,stopLossPercentage=3.0. Modes typisch5.0aberstopLoss="false"(also inaktiv). - Setzpunkt: Filled-Buy-Preis:
sl_price = filled_price × (1 − stopLossPercentage / 100). - Pro Buy ein eigener SL. Bei Multi-Split: jeder Split hat eigenen, unabhängigen SL.
- Bewegt sich nach Entry? Nein — statisch.
- Fill: Market-Sell,
taker_fee(odermaker_feefallsonlyMakerSell=True), Bid-Cross.
8.2 Take-Profit
sellPercentages: Liste — ein TP pro Split (siehe §9).- Setzpunkt:
sell_price_raw = filled_price × (1 + sellPercentages[split_index] / 100);sell_price = ceil_to_tick(sell_price_raw). - Fill: Limit-Sell,
maker_fee, Effektiv-PreisTP × (1 − half_spread). - TSL-Override: Ist
tsl_drop_pct > 0, wird der TP nicht als reguläre Limit-Order behandelt — nur als Aktivierungs-Schwelle für TSL (§8.3).
8.3 Trailing-Stop
trailingStopLossPercentages: Liste — per Split ein eigener Trail-Drop-Prozentsatz. Engine padded/truncated aufbuySplitsLänge.- TSL-Clamp bei Konflikt: Wenn
tsl_drop_pct >= sellPercentages[i], wirdtsl_drop_pctautomatisch aufsellPercentages[i] / 2reduziert. - Aktivierungs-Preis:
tsl_activation = sell_price— also identisch zum TP-Level dieses Splits. TSL aktiviert sich, sobaldseg_high >= tsl_activation. - Initial-ATH bei Aktivierung:
tslAth = tslActivation(NICHTseg_high). - TSL-Level Update: pro Segment, falls
seg_high > current_ATH:ATH = seg_high,tsl_level = ATH × (1 − tsl_drop / 100), gerundet auffloor_tick. - Safety-Floor:
tsl_level = max(tsl_level, floor_tick(buy_price) + tickSize)— TSL darf niemals unter den Buy-Preis fallen. - Trigger: TSL feuert, wenn
seg_low ≤ current_tsl_level. - Vorwärts-only: TSL-Level wandert nur nach oben (engt sich an), nie zurück.
- TSL ersetzt TP: Splits mit aktivem TSL skippen den nominalen TP-Fill (§4.5).
- Fee:
taker_rate(odermaker_ratefallsonlyMakerSell=True), Market-Sell mit Bid-Cross.
8.4 Time-Stop
- Direkter Time-Stop existiert nicht — aber
sellTimeCurves(§11) erlaubt zeit-basierte TP-Adjustierung.
8.5 Cool-down nach Trigger
triggerCoolDown(Default0): Einheit = Bars (Index-Distanz). Wenni − lastTriggerIndex < triggerCoolDown, wirdcanBuy = Falsefür diese Bar.- Scope: alle Buy-Triggers (UP und DOWN). Sells unbetroffen.
- Restoration:
canBuywird am Bar-Ende auf den ursprünglichen Wert zurückgesetzt.
9. Multi-Level-Sells (sellPercentages)
Wichtig — diese Engine hat keine Multi-Level-Exits pro Position.
Stattdessen erzeugt jeder Trigger N parallele unabhängige Splits (= N getrennte Buy-Orders), und jeder Split hat genau einen TP.
sellPercentages[i]: TP-Distanz für Split i (nicht TP-Level Nr. i einer Position).- Anteils-Bezug: jeder Split ist eine eigene Position;
sellPercentages[i]wird relativ zum Filled-Buy-Preis von Split i angewendet. - Beispiel:
buyVolumes=[40, 30, 30],sellPercentages=[1.5, 3.0, 5.0]→ drei unabhängige Buys mit TPs bei +1.5 %, +3.0 %, +5.0 % über jeweils eigenem Buy-Preis. - Reihenfolge der TP-Erfüllung: richtet sich nach Preis-Erreichen.
- SL nach Partial-Exit: keiner im klassischen Sinn — jeder Split hat eigenen SL, unbeeinflusst von anderen Splits. Kein Break-Even-Move.
10. Multi-Entry & Cascade
10.1 UP-Trigger cancelt alle offenen Buys
Kritisches Verhalten: Wenn ein UP-Trigger feuert, werden ALLE offenen Buy-Orders gecancelt (_cancel_all_open_buys), bevor neue Splits zur neuen Schwelle platziert werden.
- Konsequenz: Nicht-gefüllte Buy-Limits aus früheren DOWN-Triggern verschwinden, sobald der Preis nach oben dreht.
- Implementierung: Canceled Buys werden physisch aus den Arrays entfernt (Pop-and-Swap), nicht nur als “canceled” markiert.
10.2 DOWN-Trigger
canBuyDown=True: ein DOWN-Trigger platziert alle Splits simultan.- Kein Cancel offener Buy-Orders bei DOWN-Triggern — sie bleiben aktiv (kann zu Stapeln führen).
10.3 Trigger-Logik & lastBuyPrice
- Trigger-Schwellen:
up_thr = lastBuyPrice × (1 + pct),dn_thr = lastBuyPrice × (1 − pct). lastBuyPriceUpdate nach Trigger: auf den aktuellen Preis (current_price), nicht die Schwelle.- Falls
canBuyUp=Falseund UP-Schwelle gerissen:lastBuyPricewird trotzdem aufcurrent_priceaktualisiert, aber kein Trigger gefeuert. Identisch fürcanBuyDown=False.
10.4 Cascade-Mechanik
- Pro Segment werden alle ausgelassenen Trigger zwischen
lastBuyPriceund Segment-Start (Phase 1) bzw. Segment-End (Phase 3) nachträglich gefeuert. - Cap: maximal
10000Trigger pro_cascade_to_price-Call. Nach 10000 wird stillschweigend abgebrochen. - Average-Entry: NEIN — keine Wieder-Berechnung von SL/TP. Jeder Buy ist eine getrennte Position.
10.5 Globale Buy/Sell-Schalter
canBuy=False: komplette Cascade-Auswertung wird übersprungen.canSell=False: nach einem Buy-Fill wird kein Sell-Order erzeugt — Hodl-Modus.
11. Zeit-basierte Sell-Repricing (sellTimeCurves)
Komplexe, aber optionale Komponente. Aktiv nur wenn sellTimeCurves non-empty Dict ist.
11.1 Schema
11.2 Key-Lookup
- Engine normalisiert via
_normalize_percent_key(n):f'{n:.10f}'.rstrip('0').rstrip('.')→ z.B.2.5→"2.5",0.0025→"0.0025". - Fallback auf Underscore-Variante:
"2_5"für Filesystem-Kompatibilität. - Falls Key nicht gefunden: Sell behält ursprüngliche
sellPercentages[i].
11.3 after-Format
- Akzeptiert:
"5m","1h","1d","30s","1w"oder rohe Millisekunden. - Multiplikatoren:
s=1000, m=60000, h=3600000, d=86400000, w=604800000. - Auch alternativer Key
"afterMs"wird gelesen.
11.4 Re-Pricing-Loop
- Frequenz: alle
sellTimeCurveCheckIntervalMs / 1000Bars (bei 1m-Bars und Default 60000ms = alle 60 Bars ≈ 1h). - Logik: für jeden offenen Sell:
elapsed = current_time_ms − buy_time_ms- Letzten passenden Tier auswählen (höchstes
after_msmit<= elapsed). new_pct = tier.percent→new_sell_price = buy_fill_price × (1 + new_pct / 100), gerundet auf tickSize (ceil).- Falls
tier.tslPercentgesetzt:tsl_drop_pctebenfalls auf neuen Wert. - Sell-Order wird re-priced (alter Preis aus Index entfernt, neuer eingefügt).
11.5 Tier-Felder
percent/percentage/pct: alle drei Keys werden geprüft (Live-Bot-Kompatibilität).tslPercent/tslPercentage/tsl: alle drei Keys werden geprüft (optional).
12. Sell-Aktivierungs-Logik (sellActivateDistancePercentage / sellCancelDistancePercentage)
Modelliert “Sell-Order nur an Exchange schicken, wenn Preis nahe genug ist”. Aktiv wenn beide Felder > 0.
12.1 Aktivierungs-Range
- Sell-Order ist “active on exchange” wenn:
price_diff_pct ∈ [sellActivateDistancePercentage, sellCancelDistancePercentage]ODER- Preis bereits über Sell-Preis (
current_price >= sell_price).
price_diff_pct = |current_price − sell_price| / sell_price × 100.- Falls außerhalb: Order wird deaktiviert (
is_active_on_exchange = False), TP-Fill geblockt.
12.2 TSL-Bypass
- TSL-Orders (
tsl_drop_pct > 0) umgehen die Aktivierungs-Logik komplett — sind immer aktiv.
12.3 Cumulative-Balance-Cap (Live-Bot-Match)
- Engine summiert
qtyaller bereits aktiven Sells als “reserved base”. - Kandidaten zum Aktivieren werden nach Distanz aufsteigend (closest first) sortiert.
- Nur so viele werden aktiviert, wie in
base_bal − reservedpassen. Bei Überschreitung: Early-Break.
12.4 Re-Evaluation
- Pro Bar: auf Bar-Open.
- Pro Segment: auf Segment-Start-Preis.
13. Portfolio- und Equity-Buchhaltung
13.1 Equity-Spalten (in assetbalanceshistorie.csv)
| Spalte | Formel | Verwendung |
|---|---|---|
timestamp | ISO-8601 | Zeit |
quoteAssetBal | freier Cash inkl. Reservierungen für offene Buys | Cash-Saldo |
baseAssetBal | offene Base-Menge | Position |
baseValueInQuote_mid | base × close (kein Spread, keine Fees) | “Mark”-Wert |
baseValueInQuote_exitNet | base × close × (1 − half_spread) × (1 − taker_rate) | konservativ |
totalValueInQuote_mid | quote + baseValueInQuote_mid | Equity-mid |
totalValueInQuote_exitNet | quote + baseValueInQuote_exitNet | Equity-net (für return_pct) |
totalValueInQuote | identisch zu totalValueInQuote_mid | Legacy-Alias |
baseCostInQuote | Σ Cost-Basis aller offenen Buys | Cost-Basis |
unrealizedPnL_exitNet | baseValueInQuote_exitNet − baseCostInQuote | Unrealized |
reason | konstant "tick" | Snapshot-Typ |
13.2 Equity-Curve-Sampling
- Frequenz: pro Bar zum Close, exakt ein Snapshot mit
reason="tick". - Keine zusätzlichen Snapshots auf Fill-Events. Realized-PnL wird in
quote_balreflektiert, das im nächsten Tick-Snapshot sichtbar ist.
13.3 Drawdown
- Wird in der Engine NICHT berechnet. Equity-Curve wird gespeichert; Drawdown-Berechnung erfolgt downstream.
13.4 baseCostInQuote — exakte Definition
- Wenn
fees_in_quote=True:cost = filled_price × bo_qty + bo_fee_qfür jeden noch lebenden Buy. - Wenn
fees_in_quote=False:cost = filled_price × bo_qty(ohne Fee — die wurde in Base bezahlt).
13.5 Initial-Capital
startBal(Default10000).- Reinvest: ja — realisierter Profit wandert in
quote_balund steht für nächste Buys zur Verfügung.
14. Multi-Symbol-Koordination
- Cash-Pool: nicht gemeinsam. Jedes Symbol läuft in eigenem Subprocess mit eigenem
startBal. - Reihenfolge bei gleichzeitigen Signalen: entfällt (Single-Symbol pro Run).
- Korrelation: nicht modelliert.
- Portfolio-Limits: existieren auf Engine-Level nicht.
15. Metriken & Statistiken
15.1 Trade-Definition & Profit-Formel
- Round-Trip: ein Trade = ein Buy + sein zugehöriger Sell. Multi-Split-Trigger erzeugen N getrennte Trades.
- Trade-PnL (Live-Bot-Match):
15.2 In Engine berechnet
total_profit,avg_profit,min_profit,max_profitwin_rate=(profit > 0).count / total × 100final_value=totalValueInQuote_exitNetder letzten Barreturn_pct=(final_value − start_balance) / start_balance × 100fulfilled_trades,active_orders
15.3 Nicht in Engine berechnet (Downstream)
- Sharpe-Ratio, Sortino-Ratio
- Drawdown / Max-Drawdown
- Annualisierte Returns
- Profit-Factor, Expectancy
- Calmar-Ratio, Recovery-Factor
15.4 fillType-Werte (in fulfilledorders.csv)
"TP"— Take-Profit-Limit-Sell gefüllt"SL"— Stop-Loss-Market-Sell gefüllt"TSL"— Trailing-Stop-Loss-Market-Sell gefüllt
fillType (leer/NULL).
15.5 trades_records-Type-Werte (intern)
"BUY"— Buy-Fill"SELL"— TP-Fill (Limit)"SELL_SL"— SL-Fill"SELL_TSL"— TSL-Fill
16. Determinismus & Reproduzierbarkeit
- RNG-Seed: keiner — Engine nutzt kein
random.*. Vollständig deterministisch. - Tie-Breaking: Order-IDs sind sequentielle Integer-Counters (
buy_id,sell_id). - Floating-Point:
float64durchgehend; keine Decimal-Library. - Reihenfolge-Stabilität: identische Inputs → bit-identische Outputs (gleicher
config_hash). - Numba-JIT: nur für
in_range-Helper;safe no-opfalls Numba unavailable. Keine Verhaltensänderung.
17. Edge-Cases & Fehlerbehandlung
| Szenario | Verhalten |
|---|---|
| Insufficient Cash für Buy | Split stillschweigend übersprungen |
notional < minNotional | Order übersprungen |
quote_bal < dontBuyBelowQuoteAssetBalance | Trigger übersprungen |
qty == 0 nach Step-Rounding | Order übersprungen |
| Buy-Preis > 5 % von current_price | Order übersprungen (PERCENT_PRICE_BY_SIDE) |
onlyMakerBuy und Buy-Preis ≥ Markt | Order übersprungen |
| Cascade > 10000 Trigger | abgebrochen, weitere Trigger ignoriert |
| Symbol delisted mitten im Run | nicht modelliert |
OHLC ungültig (H < L) | keine Validierung |
| Zero-Volume-Bar | normal prozessiert |
| Negativer Preis | nicht unterstützt |
| Position-Größe rundet auf 0 | Order übersprungen via minNotional |
| End-of-Data mit offener Position | Position bleibt offen; kein Auto-Close |
| Subprocess-OOM (RLIMIT_AS) | non-zero Exit, Backtest als failed |
| Empty DataFrame | ValueError → Backtest failed |
len(buyVolumes) != buySplits | ValueError |
sum(buyVolumes) <= 0 | ValueError |
tsl_drop >= sellPercentages[i] | auto-clamped auf sellPercentages[i] / 2 |
len(trailingStopLossPercentages) < buySplits | auto-padded mit 0.0 |
len(trailingStopLossPercentages) > buySplits | auto-truncated |
18. Boolean-Parsing (Mode-JSON-Quirk)
Modes sind historisch JavaScript-kompatibel (Live-Bot ist Node.js) und enthalten Booleans als Strings:safe_bool(val, default):
- Falls
isinstance(val, bool)→ direkt zurück - Falls
isinstance(val, str)→val.strip().lower() == 'true' - Falls
val is None→default - Sonst →
bool(val)(truthy/falsy)
true/false) als auch String-Booleans ("true"/"false") funktionieren. Andere Strings ("yes", "1", "on") werden als False gewertet.
19. Binance-Filter / Hard-Caps (nicht-konfigurierbar)
| Constraint | Wert | Code-Ort | Bedeutung |
|---|---|---|---|
PERCENT_PRICE_BY_SIDE | 5.0 % | _place_split_buy_orders | Buy-Limit darf max. 5 % vom aktuellen Markt entfernt sein |
| Cascade-Trigger-Cap | 10000 | _cascade_to_price | Schutz gegen Endlos-Cascade |
| TSL-Safety-Floor | floor_tick(buy_price) + tickSize | _try_tsl_on_segment, _add_sell | TSL niemals unter Buy-Preis |
| TSL-Drop-Clamp | sell_pct × 0.5 | _place_split_buy_orders, __init__ | Falls tsl_drop ≥ sell_pct |
| Within-Segment-Same-Order-Schutz | implizit | _try_fill_*_on_segment | Sell aus Segment N fillt nicht im selben Segment |
20. Konfigurations-Inputs (vollständig)
Felder, die das Engine-Verhalten ändern. Defaults aus drei Schichten — Engine-Code → MARKET_DEFAULTS (API-Runner) → Mode-JSON. Spätere Schicht überschreibt frühere.20.1 Markt-Konstanten
| Feld | Typ | Engine-Default | Runner-Default | Wirkung |
|---|---|---|---|---|
symbol | str | "PEPEUSDC" | "PEPEUSDT" | Trading-Pair |
tickSize | float | 1e-8 | 1e-8 | Preis-Rundung |
stepSize | float | 1 | 1 | Qty-Rundung |
minNotional | float | 0.0 | 1.0 | Order-Skip-Schwelle |
startBal | float | 10000 | 10000 | Initial-Cash |
20.2 Fees & Spread
| Feld | Typ | Engine-Default | Runner-Default | Wirkung |
|---|---|---|---|---|
maker_fee_bps | float | 1.0 | 7.5 | §6.1 |
taker_fee_bps | float | 7.5 | 7.5 | §6.1 |
fees_in_quote | bool | True | True | §6.2 |
assumed_spread_bps | float | 1.5 | 1.5 | §6.3 |
20.3 Engine-Modi
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
intrabar_mode | str | "OHLC" engine / "OLHC" runner | §4.4 |
triggerCoolDown | int(bars) | 0 | §8.5 |
order_latency_seconds | int(s) | 0 engine / 2 runner | unbenutzt §1.6 |
start | bool-str | True | unbenutzt §1.6 |
20.4 Steuer-Flags
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
canBuy | bool-str | True | §10.5 |
canSell | bool-str | True | §10.5 |
canBuyUp | bool-str | True | §10.3 |
canBuyDown | bool-str | True engine / Mode-spezifisch (typisch False) | §10.2 |
onlyMakerBuy | bool-str | False | §6.6 |
onlyMakerSell | bool-str | False | §6.6 |
20.5 Position-Sizing
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
buyPercentage | float (%) | 0.1 | §7.1 |
buySplits | int | 7 engine / len(buyVolumes) | §7.2 |
buyVolumes | list[float] | [25,15,15,15,10,10,10] | §7.2 |
investmentPerBuy | float | 20 | §7.2 |
investmentPercentMode | bool | False | §7.2 |
investmentPerFreeQuotePercent | float (%) | 0.0 | §7.2 |
minInvestmentPerQuote | float | 0.0 | §7.2 |
dontBuyBelowQuoteAssetBalance | float | 0.0 | §7.3 |
20.6 Risk-Management
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
stopLoss | bool-str | False | §8.1 |
stopLossPercentage | float (%) | 3.0 engine / 5.0 modes | §8.1 |
sellPercentages | list[float] | [0.25,0.35,0.5,0.75,1,2.5,5] | §8.2 / §9 |
trailingStopLossPercentages | list[float] | [0]*buySplits | §8.3 |
20.7 Sell-Aktivierung & Time-Curves
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
sellActivateDistancePercentage | float (%) | 0.0 | §12.1 |
sellCancelDistancePercentage | float (%) | 0.0 | §12.1 |
sellTimeCurves | dict | {} | §11 |
sellTimeCurveCheckIntervalMs | int (ms) | 60000 | §11.4 |
20.8 Daten-Range
| Feld | Typ | Default | Wirkung |
|---|---|---|---|
from | ISO-string | optional | Daten-Range-Beginn |
to | ISO-string | optional | Daten-Range-Ende |
21. CSV-Output-Schemas
21.1 fulfilledorders.csv
| Spalte | Typ | Beschreibung |
|---|---|---|
buyOrderId | int | sequenzielle Buy-ID |
sellOrderId | int | sequenzielle Sell-ID |
buyPrice | float | Effektiver Buy-Preis (post-spread) |
sellPrice | float | Effektiver Sell-Preis |
buyQuantity | float | Original Buy-Qty (vor Fee falls fees_in_base) |
sellQuantity | float | Sell-Qty (= base_received vom Buy) |
buyFeeInQuoteAsset | float | Buy-Fee, immer in Quote |
sellFeeInQuoteAsset | float | Sell-Fee, immer in Quote |
profit | float | siehe §15.1 |
profitPercentage | float | siehe §15.1 |
buyTime | ISO-str | Buy-Fill-Zeit |
sellTime | ISO-str | Sell-Fill-Zeit |
symbol | str | Trading-Pair |
baseAsset | str | Base-Currency |
quoteAsset | str | Quote-Currency |
fillType | str | "TP" / "SL" / "TSL" |
21.2 assetbalanceshistorie.csv
Siehe §13.1 für die 11 Spalten.
21.3 activeorders.csv
| Spalte | Typ | Beschreibung |
|---|---|---|
symbol, baseAsset, quoteAsset | str | Trading-Pair |
type | str | "buyOrder" / "sellOrder" |
id | int | Order-ID |
status | str | "open" / "canceled" |
orderPlacedAtIndex | int | Bar-Index der Platzierung |
activeFromIndex | int | Bar-Index ab Aktivierung |
buyPrice/sellPrice | float-str | je nach type |
quantity | float-str | Order-Qty |
sellPercentage | float-str | nur Buys, der zugehörige TP |
buyOrderId | int | nur Sells, Verweis auf Buy |
tslDropPct | float-str | nur Sells, TSL-Drop |
tslActive | bool | nur Sells |
tslAth | float-str | nur Sells |
tslPrice | float-str | nur Sells |
isActiveOnExchange | bool | nur Sells |
22. Bekannte Limitationen / Nicht modelliert
- Order-Book / Market-Impact: keine Tiefe; Slippage rein über
assumed_spread_bps(konstant). - Liquidations-Mechanik: entfällt (Spot-Only).
- Borrowing-Kosten / Margin-Zinsen: nicht modelliert.
- Funding-Rates: nicht modelliert (Spot).
- Multi-Symbol-Portfolio: kein gemeinsamer Cash-Pool.
- Stress-Events / Halts: nur insofern abgebildet, als sie in den OHLCV-Daten als Lücke auftauchen.
- Stop-Limit / OCO / Iceberg: nicht unterstützt.
order_latency_seconds: Config-Feld vorhanden, aber nicht angewandt.start-Flag: Config-Feld vorhanden, aber nicht angewandt.- Optimistisches Gap-Handling für Limits: Limits fillen am Limit-Level statt am Open jenseits.
- Maker-Reklassifizierung bei
onlyMakerSell: Order fillt trotzdem als Market-Sell, aber zum Maker-Tarif. - PERCENT_PRICE_BY_SIDE 5 %: hart einkodiert, nicht konfigurierbar.
- Cascade-Cap 10000: bei extrem volatilen Phasen (>10000 buyPercentage-Steps) gehen weitere Trigger verloren.
- Indikator-basierte Strategien: nicht möglich; Engine ist preis-/distanz-basiert.
- Drawdown / Sharpe / Sortino: nicht in Engine, nur downstream.
23. Changelog
| Version | Datum | Änderung |
|---|---|---|
1.0 | 2026-05-04 | Initial-Spec — Audit von backtester_numba.py. |
1.1 | 2026-05-04 | Massiv erweitert: 6 Verifikations-Punkte aus v1.0 aufgelöst; neue §11 (sellTimeCurves), §12 (sellActivation), §18 (Boolean-Parsing), §19 (Hard-Caps), §21 (CSV-Schemas); §10 erweitert (UP-Cancel, Cascade-Cap); §8.3 (TSL) komplett neu mit Safety-Floor, Activation-Quelle, ATH-Init; §17 ergänzt um Validation-Errors. §4.2 präzisiert (TP-vs-SL ehrlich, OLHC-strukturell). |