wickbot / docs /ARCHITECTURE.md
Joedroid's picture
deploy: update wickbot codebase (part 7)
4550bff verified
|
Raw
History Blame Contribute Delete
10.9 kB

WickBot β€” Architecture Overview

A map of how the pieces fit together, for anyone (human or AI agent) picking this project back up.


Process model: one process, several background threads

Everything runs in a single Python process (main.py), with several daemon threads sharing the same in-memory objects β€” this is deliberate: it means there's exactly one MT5Connector, one TradeExecutor, one WickBotTelegram, and every subsystem (dashboard, goal monitor, position manager) reads/writes the same state rather than a second connection that could drift out of sync.

main.py (main thread)
β”œβ”€β”€ candle-close loop: for each symbol, fetch candles β†’ indicators β†’ strategies β†’ rating β†’ execute
β”‚     (sleeps until the next candle close for Config.TIMEFRAME β€” can be hours on H4/D1)
β”‚
β”œβ”€β”€ telegram_bot.py (background thread, via tg.run_in_background())
β”‚     long-polls Telegram, handles /status /stats /goal /positions /arm_real etc.
β”‚
β”œβ”€β”€ webapp/server.py (background thread, via uvicorn.Server.run())
β”‚     FastAPI dashboard β€” Telegram Mini App AND direct-browser access
β”‚
β”œβ”€β”€ goal_tracker monitor (background thread, main.py's start_goal_monitor())
β”‚     checks drawdown kill-switch + goal pace every 60s, independent of candle-close
β”‚
β”œβ”€β”€ position_manager (background thread, position_manager.start_position_manager())
β”‚     checks breakeven/trailing/time-decay + reconciles closed trades every 30s
β”‚
└── auto_tuner monitor (background thread, main.py's start_auto_tuner())
      checks for new tuning suggestions every AUTO_TUNER_CHECK_INTERVAL_HOURS (default 6h),
      notifies via Telegram β€” never applies anything itself

Why separate threads instead of folding everything into the candle-close loop: that loop can sleep for hours between cycles on H4/D1 timeframes. A drawdown kill-switch or a trailing stop can't wait that long to react β€” so anything time-sensitive gets its own short-interval loop instead.


Data flow: signal generation β†’ execution

mt5_connector.get_candles()          β€” 300 bars, native or mt5linux backend, transparently
        β”‚
        β–Ό
indicators.add_all_indicators()      β€” EMA/RSI/ATR/MACD/Bollinger, attached as columns
        β”‚
        β–Ό
strategies.evaluate_all()            β€” 5 independent strategies, each wraps wick_rules detectors
        β”‚  (trend_ema_pullback, liquidity_sweep_reversal, pin_bar_reversal,
        β”‚   engulfing_wick_reversal, judas_swing_session_open)
        β–Ό
rating.rate_signal()                 β€” 0-100 confluence score (confirmation candle, close-back
        β”‚                               ratio, structure alignment, news blackout, stop sanity)
        β–Ό
main.py filters by MIN_SIGNAL_SCORE
        β”‚
        β–Ό
trade_executor.TradeExecutor.execute()
        β”‚  - re-checks account is_demo/is_real fresh (never cached)
        β”‚  - if real: requires ALLOW_REAL_TRADING + graduation + session-armed, else logs only
        β”‚  - risk_manager.build_order_plan() sizes the position
        β–Ό
mt5_connector.send_market_order()    β€” tagged with Config.MAGIC_NUMBER
        β”‚
        β–Ό
performance_tracker.log_trade_open() β€” status='open', records ticket + risk_amount + initial_sl

Data flow: managing & closing a trade

position_manager (every 30s, independent thread)
        β”‚
        β”œβ”€ reconcile_closed_trades()
        β”‚     compares DB 'open' trades against connector.get_bot_positions()
        β”‚     anything missing from MT5's open list β†’ pull history_deals, compute
        β”‚     realized profit/commission/swap β†’ result_r = profit / risk_amount β†’
        β”‚     performance_tracker.log_trade_close() with status closed_win/closed_loss/closed_be
        β”‚
        └─ for each still-open position:
              profit_r = (current_price - entry) / initial_risk_distance
              β”‚
              β”œβ”€ Rule 1: breakeven move at BREAKEVEN_TRIGGER_R
              β”‚     buffer = max(R-fraction, ATR-scaled, cost-based) β€” never smaller
              β”‚
              β”œβ”€ Rule 2: trailing from TRAILING_ACTIVATION_R
              β”‚     distance tightens if higher-timeframe bias (wick_rules.market_structure_bias
              β”‚     on the next TF up) opposes the trade direction
              β”‚
              └─ Rule 3: time decay β€” tighten a stagnant pre-breakeven trade past TIME_DECAY_HOURS

              β†’ mt5_connector.modify_position_sl() β€” only ever moves toward LESS risk
              β†’ performance_tracker.update_trade_sl() β€” record-keeping only, never touches
                the fixed initial_sl risk basis

Critical invariant: every rule above only ever moves a stop in the trade's favor. If you're extending this file, preserve that β€” it's what makes position management apply identically to demo and real without needing its own arming gate (see CLAUDE.md's safety invariants).


Two-tier config: static vs. dynamic

config.py's Config class uses a metaclass (_ConfigMeta) so that most settings (Config.MIN_SIGNAL_SCORE, Config.BREAKEVEN_TRIGGER_R, etc.) are actually properties, not plain class attributes β€” every read transparently checks runtime_config.py's SQLite+JSON store (through a short cache) first, falling back to the .env-derived default if no override exists. This is what lets the dashboard's Settings card change trading behavior live, with zero changes needed at any of the dozens of call sites across the codebase that just do Config.SOMETHING like always.

TUNABLE_REGISTRY (also in config.py) is the single list that both generates these properties AND is served directly to the dashboard to render the Settings page β€” one source of truth, not two lists that could drift apart. It's also the allowlist: webapp/server.py's /api/settings/update and auto_tuner.py's apply_suggestion() both check a key against this registry before writing anywhere, so a key that was never added to the registry (like ALLOW_REAL_TRADING) is structurally impossible to change through either path β€” not just discouraged by convention.

Secrets, anything the dashboard itself needs before it can start, and the entire real-trading safety gate stay as plain static attributes, deliberately outside this system β€” see CLAUDE.md's safety invariants for why.


Database schema (logs/trades.db, SQLite)

Single trades table, migration-safe (new columns added via ALTER TABLE ... ADD COLUMN wrapped in try/except, so old databases keep working without a manual migration step β€” see performance_tracker._MIGRATION_COLUMNS).

Column Purpose
id, opened_at, closed_at Identity + timestamps
account_type demo / real / real_blocked (blocked = would've been real but safety gate stopped it)
symbol, strategy, pattern, side, score, grade What fired and why
entry, sl, tp, volume Trade parameters (sl is current/last-known, updated as it moves)
initial_sl Fixed SL at open β€” the permanent R=1 risk basis, never overwritten
ticket MT5 position ticket β€” the join key between MT5's live position list and this DB
risk_amount Dollars risked at open (from risk_manager.build_order_plan()) β€” denominator for realized result_r on close
result_r, status Filled in by reconcile_closed_trades() once MT5 shows the position gone
breakeven_moved, trailing_active Flags read by position_manager to avoid redundant SL modifications
exit_reason 'tp' / 'sl' / 'other', classified in reconcile_closed_trades() by comparing the actual exit price to the trade's stored TP/SL β€” feeds auto_tuner.py's TP-percentage calibration rule

A second, separate SQLite file (logs/openrouter_models.db) caches the OpenRouter model/pricing catalog β€” see openrouter_client.py. Three more small, separate SQLite files exist for the same reason (keeping unrelated concerns in their own storage rather than one increasingly-overloaded file): logs/runtime_config.db (live settings overrides, mirrored to logs/runtime_config.json), logs/accounts.db (encrypted broker account credentials), and logs/tuning.db (pending/applied/dismissed AI tuning suggestions).


Module responsibility map

Module Owns
config.py All settings, loaded once at import time from .env
mt5_connector.py The only place that talks to MT5 directly β€” both backends (native/mt5linux) exposed identically
indicators.py Pure pandas technical indicators, no state
wick_rules.py Pattern detection (pin bar, engulfing, liquidity sweep, Judas swing) + structure bias β€” pure functions on a DataFrame, no I/O
scalp_strategy.py EMA20/Stochastic/Fibonacci pullback scalp β€” adapted from a trading video's text description, with an explicit tp_ref (fixed target) rather than the default R-multiple TP
rating.py Turns a raw pattern Signal into a scored RatedSignal
strategies.py Combines indicators + wick_rules into named, independently-tracked strategies
risk_manager.py Position sizing math only β€” no I/O
performance_tracker.py The only module that touches trades.db
trade_executor.py The single choke point for every order β€” owns the real-trading safety gate
telegram_bot.py Owner-only command surface + notifications
webapp/ Dashboard (FastAPI backend + one HTML frontend), dual-auth (Telegram initData / browser token)
news_calendar.py Live economic calendar with static-hours fallback
openrouter_client.py Model catalog caching + free-then-cheap-paid routing β€” advisory only, never trades
goal_tracker.py Natural-language goal parsing, pace reporting, drawdown kill-switch
position_manager.py Breakeven/trailing/time-decay + closed-trade reconciliation + manual position controls
runtime_config.py SQLite+JSON key-value store backing every dashboard-editable setting
account_manager.py Encrypted multi-account storage; MT5Connector.switch_account() logs into a different stored account within the same terminal
auto_tuner.py Analyzes demo stats, proposes (never auto-applies) MIN_SIGNAL_SCORE raises or strategy/symbol disabling
main.py Wires everything together, owns the candle-close loop

Testing approach used throughout development

No live MT5 terminal or OpenRouter key is available in a typical dev/CI sandbox, so everything here was tested with fake connector objects implementing just the methods a given module needs, plus FastAPI's TestClient for the dashboard. See CLAUDE.md's "How to test changes here" section for the pattern to reuse.