Skip to main content

Documentation Index

Fetch the complete documentation index at: https://uncoded.ch/docs/llms.txt

Use this file to discover all available pages before exploring further.

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 from ENGINE.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. Per engine_version in MDX-Reports referenziert. Verhaltensänderung → Version bumpen, alte Spec archivieren (ENGINE-v1.x.md etc.). 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 via backtest-api/app/services/backtest_runner.py, Mode-Configs unter trading-bot/modes/*.json. Werte ohne Marker sind aus dem Code verifiziert. ? markiert verbleibende Unklarheiten.
engine_version: 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 zu lastBuyPrice — 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_mode modelliert (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 via export_to_csv in tmp/candles.csv.
  • Auflösungen: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d. Default 1m wenn 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: auf df['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, fillt in_range nur, 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 (Default 0 engine, 2 runner): wird gelesen, aber niemals ausgewertet. Alle Orders sind sofort aktiv ab placed_idx (bo_active_from = placed_idx). Kommentar im Code: “limit orders on Binance. They fill immediately when price hits — no latency.”
  • start (Default True): wird gelesen (safe_bool), aber im Engine-Loop nicht weiter verwendet. Vermutlich Live-Bot-only.

2. Signal-Pipeline (Bar-Loop)

Pro Bar i in dieser Reihenfolge:
1. Cooldown-Check         (i - lastTriggerIndex < triggerCoolDown → canBuy=False für diese Bar)
2. SellActivation-Update  (auf Bar-Open — falls aktiviert)
3. SellTimeCurves-Update  (alle N Bars — Re-Pricing nach Zeit)
4. Segmentierung (3 Segmente nach intrabar_mode)
   Pro Segment:
     Phase 1: Catch-Up-Cascade (lastBuyPrice → segment-start)
     Phase 1.5: SellActivation-Update (auf Segment-Start-Preis)
     Phase 2: Fills auf [segment-start, segment-end]:
       a) TP-Sells (Limit, maker fee)
       b) Stop-Loss (Market, taker fee)
       c) Trailing-SL (Market, taker fee)
       d) Buy-Fills (Limit, maker fee)
     Phase 3: Movement-Cascade (lastBuyPrice → segment-end)
5. canBuy-Restore         (falls Cooldown aktiv war)
6. Equity-Snapshot        (zum Bar-Close, reason="tick")

2.1 Segmentierung

Default intrabar_mode = "OLHC" (im API-Runner; Engine-internes Default ist "OHLC"):
  1. Segment A: O → L
  2. Segment B: L → H
  3. Segment C: H → C
Alternative intrabar_mode = "OHLC":
  1. Segment A: O → H
  2. Segment B: H → L
  3. 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

TypUnterstütztTriggerFill-Preis (Effektiv)
Limit BuyjaLow ≤ Limit im SegmentLimit × (1 + half_spread)
Limit TP-SelljaHigh ≥ TP im SegmentTP × (1 − half_spread)
Market Stop-LossjaPreis durchläuft SL-Level im SegmentSL × (1 − 2·half_spread) (Bid-Side, voller Spread)
Market Trailing-SLjaPreis ≤ TSL-Level (nach Aktivierung)TSL × (1 − 2·half_spread)
Stop-Limitnein——
OCOnein——
Icebergnein——
Time-in-Forceimplizit GTCbleiben 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

  1. Phase 1 — Catch-Up-Cascade: Trigger-Generierung von lastBuyPrice zum Segment-Start-Preis (handhabt Gaps zwischen Bars).
  2. SellActivation-Refresh: falls aktiviert, basierend auf Segment-Start.
  3. Phase 2a — TP-Limit-Sells (maker_fee falls nicht onlyMakerSell-blockiert; TSL-Orders skippen TP-Fills komplett, siehe §4.5).
  4. Phase 2b — Stop-Loss (taker_fee, falls stopLoss=True).
  5. Phase 2c — Trailing-SL (taker_fee, sofern aktiviert; falls onlyMakerSell → maker_rate als Override).
  6. Phase 2d — Limit-Buys (maker_fee falls nicht onlyMakerBuy-blockiert).
  7. Phase 3 — Movement-Cascade: Trigger-Generierung von lastBuyPrice zum 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:
  1. TSL feuert (Preis ≤ TSL-Level nach Aktivierung)
  2. SL feuert (falls stopLoss=True)
Konsequenz: Ein Split mit aktivem TSL verkauft niemals zum nominalen TP. Der TP-Preis dient nur als Aktivierungs-Schwelle für das TSL-System (siehe §8.3).

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 tslActivation gesetzt (nicht auf seg_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 lastBuyPrice und 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.5 bps.
  • API-Runner-Default: maker=7.5, taker=7.5 bps (überschreibt Engine).
  • Klassifizierung:
    • Maker: Limit-Buys, Limit-TP-Sells.
    • Taker: Market-Stop-Loss, Market-Trailing-SL.
    • Override: onlyMakerBuy=True/onlyMakerSell=True siehe §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-Abzug quote_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_rate und 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 (Default 1.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: Wenn price_rounded ≥ current_price_at_trigger, wird der Buy stillschweigend übersprungen (würde Liquidität nehmen).
  • onlyMakerSell=True: Bei Market-Sells (SL/TSL) wird maker_rate statt taker_rate angewendet — 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. Wenn price ≥ lastBuyPrice × (1 + pct), feuert ein UP-Trigger; wenn price ≤ 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 wenn canBuyDown=True.
  • lastBuyPrice Update: 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-Default 7.
  • Validierung: len(buyVolumes) == buySplits == len(sellPercentages) — sonst ValueError. sum(buyVolumes) > 0 — sonst ValueError.
  • investmentPerBuy: fixe Quote-Menge pro Trigger; pro Split: inv_split = investmentPerBuy × buyWeights[i].
  • investmentPercentMode=True: ersetzt fixe Menge durch pool = (quote_bal − locked_quote) × investmentPerFreeQuotePercent / 100; dann inv_split = max(pool × buyWeights[i], minInvestmentPerQuote × buyWeights[i]).
  • locked_quote: Σ bo_price × bo_qty aller offenen Buy-Orders.
  • minInvestmentPerQuote: untere Schranke im Percent-Mode.

7.3 Hard-Limits (Binance-Filter & Engine-Caps)

  • tickSize: Preis-Rundung — Buy floor, TP-Sell ceil, SL/TSL floor. Default 1e-8.
  • stepSize: Quantity-Rundung. Buy-Qty wird ceil zur Step-Size gerundet (Live-Bot-Match), Sell-Qty wird floor. Default 1.
  • minNotional: Orders mit price × qty < minNotional werden stillschweigend übersprungen. API-Runner-Default 1.0.
  • dontBuyBelowQuoteAssetBalance: wenn quote_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 in min_distance existiert.
  • min_distance = max(buyPercentage / 100 × current_price, tickSize) — der tickSize-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 via safe_bool) + stopLossPercentage. Engine-Default stopLoss=False, stopLossPercentage=3.0. Modes typisch 5.0 aber stopLoss="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 (oder maker_fee falls onlyMakerSell=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-Preis TP × (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 auf buySplits Länge.
  • TSL-Clamp bei Konflikt: Wenn tsl_drop_pct >= sellPercentages[i], wird tsl_drop_pct automatisch auf sellPercentages[i] / 2 reduziert.
  • Aktivierungs-Preis: tsl_activation = sell_price — also identisch zum TP-Level dieses Splits. TSL aktiviert sich, sobald seg_high >= tsl_activation.
  • Initial-ATH bei Aktivierung: tslAth = tslActivation (NICHT seg_high).
  • TSL-Level Update: pro Segment, falls seg_high > current_ATH: ATH = seg_high, tsl_level = ATH × (1 − tsl_drop / 100), gerundet auf floor_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 (oder maker_rate falls onlyMakerSell=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 (Default 0): Einheit = Bars (Index-Distanz). Wenn i − lastTriggerIndex < triggerCoolDown, wird canBuy = False für diese Bar.
  • Scope: alle Buy-Triggers (UP und DOWN). Sells unbetroffen.
  • Restoration: canBuy wird 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).
  • lastBuyPrice Update nach Trigger: auf den aktuellen Preis (current_price), nicht die Schwelle.
  • Falls canBuyUp=False und UP-Schwelle gerissen: lastBuyPrice wird trotzdem auf current_price aktualisiert, aber kein Trigger gefeuert. Identisch für canBuyDown=False.

10.4 Cascade-Mechanik

  • Pro Segment werden alle ausgelassenen Trigger zwischen lastBuyPrice und Segment-Start (Phase 1) bzw. Segment-End (Phase 3) nachträglich gefeuert.
  • Cap: maximal 10000 Trigger 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

"sellTimeCurves": {
  "0.0025": [
    {"after": "5m",  "percent": 0.005, "tslPercent": 0.002},
    {"after": "10m", "percent": 0.0075, "tslPercent": 0.003},
    {"after": "200m","percent": 0.25,  "tslPercent": 0.1}
  ]
}

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 / 1000 Bars (bei 1m-Bars und Default 60000ms = alle 60 Bars ≈ 1h).
  • Logik: für jeden offenen Sell:
    1. elapsed = current_time_ms − buy_time_ms
    2. Letzten passenden Tier auswählen (höchstes after_ms mit <= elapsed).
    3. new_pct = tier.percent → new_sell_price = buy_fill_price × (1 + new_pct / 100), gerundet auf tickSize (ceil).
    4. Falls tier.tslPercent gesetzt: tsl_drop_pct ebenfalls auf neuen Wert.
    5. 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 qty aller 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 − reserved passen. 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)

SpalteFormelVerwendung
timestampISO-8601Zeit
quoteAssetBalfreier Cash inkl. Reservierungen für offene BuysCash-Saldo
baseAssetBaloffene Base-MengePosition
baseValueInQuote_midbase × close (kein Spread, keine Fees)“Mark”-Wert
baseValueInQuote_exitNetbase × close × (1 − half_spread) × (1 − taker_rate)konservativ
totalValueInQuote_midquote + baseValueInQuote_midEquity-mid
totalValueInQuote_exitNetquote + baseValueInQuote_exitNetEquity-net (für return_pct)
totalValueInQuoteidentisch zu totalValueInQuote_midLegacy-Alias
baseCostInQuoteΣ Cost-Basis aller offenen BuysCost-Basis
unrealizedPnL_exitNetbaseValueInQuote_exitNet − baseCostInQuoteUnrealized
reasonkonstant "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_bal reflektiert, 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_q fü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 (Default 10000).
  • Reinvest: ja — realisierter Profit wandert in quote_bal und 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):
    proportion = sell_qty / actual_received_qty
    buy_fee_allocated = buy_fee_in_quote × proportion
    total_buy_cost = buy_fill_price × sell_qty                     // OHNE Fee
    total_sell_value = effective_sell_price × sell_qty
    profit = total_sell_value − total_buy_cost − buy_fee_allocated − sell_fee_in_quote
    profit_pct = profit / total_buy_cost × 100                     // Nenner OHNE Fee
    

15.2 In Engine berechnet

  • total_profit, avg_profit, min_profit, max_profit
  • win_rate = (profit > 0).count / total × 100
  • final_value = totalValueInQuote_exitNet der letzten Bar
  • return_pct = (final_value − start_balance) / start_balance × 100
  • fulfilled_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
Buy-Trades haben kein 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: float64 durchgehend; keine Decimal-Library.
  • Reihenfolge-Stabilität: identische Inputs → bit-identische Outputs (gleicher config_hash).
  • Numba-JIT: nur für in_range-Helper; safe no-op falls Numba unavailable. Keine Verhaltensänderung.

17. Edge-Cases & Fehlerbehandlung

SzenarioVerhalten
Insufficient Cash für BuySplit stillschweigend übersprungen
notional < minNotionalOrder übersprungen
quote_bal < dontBuyBelowQuoteAssetBalanceTrigger übersprungen
qty == 0 nach Step-RoundingOrder übersprungen
Buy-Preis > 5 % von current_priceOrder übersprungen (PERCENT_PRICE_BY_SIDE)
onlyMakerBuy und Buy-Preis ≥ MarktOrder übersprungen
Cascade > 10000 Triggerabgebrochen, weitere Trigger ignoriert
Symbol delisted mitten im Runnicht modelliert
OHLC ungültig (H < L)keine Validierung
Zero-Volume-Barnormal prozessiert
Negativer Preisnicht unterstützt
Position-Größe rundet auf 0Order übersprungen via minNotional
End-of-Data mit offener PositionPosition bleibt offen; kein Auto-Close
Subprocess-OOM (RLIMIT_AS)non-zero Exit, Backtest als failed
Empty DataFrameValueError → Backtest failed
len(buyVolumes) != buySplitsValueError
sum(buyVolumes) <= 0ValueError
tsl_drop >= sellPercentages[i]auto-clamped auf sellPercentages[i] / 2
len(trailingStopLossPercentages) < buySplitsauto-padded mit 0.0
len(trailingStopLossPercentages) > buySplitsauto-truncated

18. Boolean-Parsing (Mode-JSON-Quirk)

Modes sind historisch JavaScript-kompatibel (Live-Bot ist Node.js) und enthalten Booleans als Strings:
{
  "stopLoss": "false",     // String, nicht boolean false
  "canBuyUp": "true",
  "onlyMakerBuy": "false"
}
Engine parsed via safe_bool(val, default):
  1. Falls isinstance(val, bool) → direkt zurück
  2. Falls isinstance(val, str) → val.strip().lower() == 'true'
  3. Falls val is None → default
  4. Sonst → bool(val) (truthy/falsy)
Konsequenz: Sowohl JSON-Booleans (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)

ConstraintWertCode-OrtBedeutung
PERCENT_PRICE_BY_SIDE5.0 %_place_split_buy_ordersBuy-Limit darf max. 5 % vom aktuellen Markt entfernt sein
Cascade-Trigger-Cap10000_cascade_to_priceSchutz gegen Endlos-Cascade
TSL-Safety-Floorfloor_tick(buy_price) + tickSize_try_tsl_on_segment, _add_sellTSL niemals unter Buy-Preis
TSL-Drop-Clampsell_pct × 0.5_place_split_buy_orders, __init__Falls tsl_drop ≥ sell_pct
Within-Segment-Same-Order-Schutzimplizit_try_fill_*_on_segmentSell 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

FeldTypEngine-DefaultRunner-DefaultWirkung
symbolstr"PEPEUSDC""PEPEUSDT"Trading-Pair
tickSizefloat1e-81e-8Preis-Rundung
stepSizefloat11Qty-Rundung
minNotionalfloat0.01.0Order-Skip-Schwelle
startBalfloat1000010000Initial-Cash

20.2 Fees & Spread

FeldTypEngine-DefaultRunner-DefaultWirkung
maker_fee_bpsfloat1.07.5§6.1
taker_fee_bpsfloat7.57.5§6.1
fees_in_quoteboolTrueTrue§6.2
assumed_spread_bpsfloat1.51.5§6.3

20.3 Engine-Modi

FeldTypDefaultWirkung
intrabar_modestr"OHLC" engine / "OLHC" runner§4.4
triggerCoolDownint(bars)0§8.5
order_latency_secondsint(s)0 engine / 2 runnerunbenutzt §1.6
startbool-strTrueunbenutzt §1.6

20.4 Steuer-Flags

FeldTypDefaultWirkung
canBuybool-strTrue§10.5
canSellbool-strTrue§10.5
canBuyUpbool-strTrue§10.3
canBuyDownbool-strTrue engine / Mode-spezifisch (typisch False)§10.2
onlyMakerBuybool-strFalse§6.6
onlyMakerSellbool-strFalse§6.6

20.5 Position-Sizing

FeldTypDefaultWirkung
buyPercentagefloat (%)0.1§7.1
buySplitsint7 engine / len(buyVolumes)§7.2
buyVolumeslist[float][25,15,15,15,10,10,10]§7.2
investmentPerBuyfloat20§7.2
investmentPercentModeboolFalse§7.2
investmentPerFreeQuotePercentfloat (%)0.0§7.2
minInvestmentPerQuotefloat0.0§7.2
dontBuyBelowQuoteAssetBalancefloat0.0§7.3

20.6 Risk-Management

FeldTypDefaultWirkung
stopLossbool-strFalse§8.1
stopLossPercentagefloat (%)3.0 engine / 5.0 modes§8.1
sellPercentageslist[float][0.25,0.35,0.5,0.75,1,2.5,5]§8.2 / §9
trailingStopLossPercentageslist[float][0]*buySplits§8.3

20.7 Sell-Aktivierung & Time-Curves

FeldTypDefaultWirkung
sellActivateDistancePercentagefloat (%)0.0§12.1
sellCancelDistancePercentagefloat (%)0.0§12.1
sellTimeCurvesdict{}§11
sellTimeCurveCheckIntervalMsint (ms)60000§11.4

20.8 Daten-Range

FeldTypDefaultWirkung
fromISO-stringoptionalDaten-Range-Beginn
toISO-stringoptionalDaten-Range-Ende

21. CSV-Output-Schemas

21.1 fulfilledorders.csv

SpalteTypBeschreibung
buyOrderIdintsequenzielle Buy-ID
sellOrderIdintsequenzielle Sell-ID
buyPricefloatEffektiver Buy-Preis (post-spread)
sellPricefloatEffektiver Sell-Preis
buyQuantityfloatOriginal Buy-Qty (vor Fee falls fees_in_base)
sellQuantityfloatSell-Qty (= base_received vom Buy)
buyFeeInQuoteAssetfloatBuy-Fee, immer in Quote
sellFeeInQuoteAssetfloatSell-Fee, immer in Quote
profitfloatsiehe §15.1
profitPercentagefloatsiehe §15.1
buyTimeISO-strBuy-Fill-Zeit
sellTimeISO-strSell-Fill-Zeit
symbolstrTrading-Pair
baseAssetstrBase-Currency
quoteAssetstrQuote-Currency
fillTypestr"TP" / "SL" / "TSL"

21.2 assetbalanceshistorie.csv

Siehe §13.1 für die 11 Spalten.

21.3 activeorders.csv

SpalteTypBeschreibung
symbol, baseAsset, quoteAssetstrTrading-Pair
typestr"buyOrder" / "sellOrder"
idintOrder-ID
statusstr"open" / "canceled"
orderPlacedAtIndexintBar-Index der Platzierung
activeFromIndexintBar-Index ab Aktivierung
buyPrice/sellPricefloat-strje nach type
quantityfloat-strOrder-Qty
sellPercentagefloat-strnur Buys, der zugehörige TP
buyOrderIdintnur Sells, Verweis auf Buy
tslDropPctfloat-strnur Sells, TSL-Drop
tslActiveboolnur Sells
tslAthfloat-strnur Sells
tslPricefloat-strnur Sells
isActiveOnExchangeboolnur 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

VersionDatumÄnderung
1.02026-05-04Initial-Spec — Audit von backtester_numba.py.
1.12026-05-04Massiv 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).
Last modified on May 5, 2026