Deterministic execution semantics of the uncoded.ch backtest engine — bar-loop, intra-bar fill rules, fees, position sizing, risk management, full configuration schema (§20).
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.
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.
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.
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.
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).
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.
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).
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.
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).
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.
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)
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).
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.
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.
buyVolumes: Liste von Prozentsätzen. Werden zu Gewichten normalisiert: buyWeights[i] = buyVolumes[i] / 100 (NICHT /sum(buyVolumes) — Live-Bot-Match).
_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).
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.
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.
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.
Konsequenz: Sowohl JSON-Booleans (true/false) als auch String-Booleans ("true"/"false") funktionieren. Andere Strings ("yes", "1", "on") werden als False gewertet.
Felder, die das Engine-Verhalten ändern. Defaults aus drei Schichten — Engine-Code → MARKET_DEFAULTS (API-Runner) → Mode-JSON. Spätere Schicht überschreibt frühere.