← Trading Systems
Python · Backtesting · Live data

Backtesting & Algo Charting App

A futures research-and-execution stack (~46k lines across ~110 files): TradingView-style charts via lightweight-charts-python, live bar data from the Rithmic API, a market-structure backtest engine, XGBoost trade filtering with leakage-aware methodology, live bracket-order routing through two brokers, and native C++ ACSIL orderflow studies for Sierra Chart.

Python lightweight-charts-python Rithmic API (protobuf) XGBoost / scikit-learn Pandas SQLite C++ / ACSIL
Lightweight Charts · desktop UI
Built with agentic AI. Development on this project runs through multi-agent Claude Code workflows — developer → independent-reviewer cycles with evidence gates, batched into unattended runs that execute overnight and resume from committed ledgers. How the agent system works →

What these tools are

The app is built on two main building blocks:

Project overview

Historical OHLCV is stored in SQLite; the same bar data feeds a backtest engine that computes pivots, session high/low (NY, London, Asia), and strategy logic. The chart is built with lightweight_charts.Chart(toolbox=True): timeframes, indicators (e.g. RSI subchart), deltas, and a “go live” mode that pulls new bars from Rithmic and calls chart.update(). Strategy results are shown in tables (by direction, run-up, P&L); trades can be exported to Excel. Optional integration posts bracket orders to Rithmic via a separate subprocess when the backtest or live logic triggers an entry.

Charts & live data

  • Single main chart with toolbox, logarithmic price scale, and configurable precision.
  • Topbar: mode (Live vs Hist), timezone, indicators, timeframe switcher, deltas/pivots toggles.
  • Live mode: bars requested from Rithmic (SampleTimeBar subprocess); new bar pushed with chart.update(series).
  • Hotkeys for adding bars and exiting; click subscription for placing levels or markers.

Backtest & execution infra

  • Backtest runs over SQLite OHLCV; structure (unbroken levels, pivots) and sessions computed per timeframe.
  • Strategy class tracks opened/closed trades, recalc on new extrema; export to Excel.
  • Optional: when a trade is taken, bracket order (entry, SL, TP) sent to Rithmic via subprocess (sample bracket-order script).
  • Multi-timeframe params (1m, 5m, 15m, 1h, 4h, 1d, renko) drive both backtest and chart.

Beyond the app — the research & execution stack

The charting app sits on top of a larger engine that drives history and live trading through the same code path.

Market-structure engine

  • An "unbroken level" engine tracks swing pivots per direction and timeframe, maintains the set of levels price hasn't broken, and emits structure/retest events.
  • A streaming bar aggregator builds arbitrary timeframes (including renko) tick-by-tick from one source stream — the same code path drives backtests and live.
  • Sessions are segmented with real timezones (NY / London / Asia) and chained next-session scheduling.
  • Trade accounting supports partial exits (per-leg SL/TP lists), running max-runup / max-drawdown recomputed on every extrema tick, and per-trade feature export.

Leakage-aware ML trade filtering

  • XGBoost classifiers trained on "did this trade profit", with splits grouped by futures contract and a whole held-out contract as out-of-time data.
  • The probability cutoff is chosen on validation only, then applied to test; calibration curves are part of every run.
  • Feature manifests are serialized next to each model so live inference can never silently reorder columns.
  • Before a feature is allowed to exist: mutual information, a depth-1 stump AUC with bootstrap 95% CI, and a label-permutation p-value.

Dual-broker live execution

  • Bracket orders (entry + SL + TP as OCO) submitted through Sierra Chart's SCBridge, or through Rithmic's R|Protocol directly — protobuf over TLS WebSocket (login, market data, order routing, order-update subscription).
  • The live consumer slices only genuinely-new bars from each overlapping realtime frame with an O(log n) watermark, idempotent under duplicate pushes.
  • Backtest entries are code-generated into both a Pine Script v6 indicator and a C++ study, so simulated fills can be visually reconciled on TradingView and Sierra charts.

Native C++ orderflow studies

  • ACSIL DLL studies read Sierra Chart's binary intraday file directly with shared buffered I/O — a fix for a documented production deadlock where holding the per-record API lock starved the live tick writer.
  • A bar is only finalized once the file provably contains the next bar's open, so a lagging disk flush can never truncate it.
  • Volume-anomaly detection scores bars against context-adaptive baselines rather than a single global average, with graceful fallback during warmup — the specific conditioning stays private.
  • Every bar is replayed tick-by-tick and the first signal print is latched permanently — a historical recalculation reproduces exactly what a live watcher saw.

Representative code excerpts

Chart setup with toolbox and live update, fetching bar data from Rithmic, binary-format parsing in the C++ studies, and idempotent live-stream consumption.

Chart with toolbox and live update

chart_code_new.py – start_charts
chart_var.chart = Chart(toolbox=True, width=1000, inner_width=0.75, inner_height=1)
chart_var.chart.legend(True)
chart_var.chart.topbar.switcher('mode', (symbol+' Live', symbol+' Hist'), ...)
chart_var.chart.topbar.menu('timezone', ('UTC', 'NY', 'JST', 'LON'), ...)
chart_var.chart.topbar.menu('indicators', ['None', ...], func=on_indicator_selection)
chart_var.chart.topbar.menu('timeframe', timeframes, func=on_timeframe_selection)
chart_var.chart.price_scale(mode='logarithmic')
chart_var.go_live()  # sets last bars and subscribes to live OHLC updates
# In go_live(): ser = pd.Series({'time': ..., 'open': ..., 'high': ..., 'low': ..., 'close': ...})
#              chart.update(ser)

The chart is created with the toolbox for drawing; topbar controls switch between live and historical data, timezone, indicators (e.g. RSI in a subchart), and timeframe. go_live() sets the initial range and then updates the chart from the live OHLC feed (e.g. from Rithmic).

Fetching bars from Rithmic

unbroken_main_delta_ids_new.py – get_rithmic_data
def get_rithmic_data(data_max):
    last_bar_time = int(data_max)
    current_time = int(time.time()) - 60
    while last_bar_time < (current_time - current_time % 60):
        reader_process = subprocess.Popen(
            [python_path, '-u', sampleTimeBar,
             "wss://rprotocol.rithmic.com:443",
             "Rithmic Paper Trading", "...", "CME", ticker, str(int(last_bar_time))],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ...
        )
        # ... read stdout for bar lines; append to all_bar_data
        last_bar_time = all_bar_data[-1][0] / 1000
    return all_bar_data

Historical gaps or live bars are requested by calling Rithmic’s time-bar sample script in a subprocess; the script connects to Rithmic, requests bars from last_bar_time, and streams them to stdout. The main app parses the output and returns a list of bars for backtest or chart update.

Binary .scid parsing, size-checked at compile time

Sierra study — binary intraday-file reader
#pragma pack(push, 1)
struct ScidRecord
{
    int64_t  DateTimeUS;
    float    Open, High, Low, Close;
    uint32_t NumTrades;
    uint32_t TotalVolume;
    uint32_t BidVolume;
    uint32_t AskVolume;
};
#pragma pack(pop)

static_assert(sizeof(ScidRecord) == 40, "scid record must be 40 bytes");

The C++ studies parse Sierra Chart's intraday file format themselves — with the record layout pinned by a static_assert so a platform header change can never silently corrupt the parse.

Idempotent live-stream consumption

final/unbroken_main_delta_ids_new.py
response = response_queue.get()
df = response.as_df()
idx = df.index
if last_ts is None:
    out = df.iloc[0:0]
else:
    # O(log n) binary search: position strictly after last_ts
    pos = idx.searchsorted(last_ts, side="right")
    out = df.iloc[pos:]

for ts, _is_closed, open_, high, low, last, net_volume in out.itertuples(index=True, name=None):
    process_bar([ts.value // 1_000_000, open_, high, low, last, net_volume], start_renko, renko=1)

last_ts = idx[-1]   # advance even on empty slices

Each realtime push overlaps the previous one; a binary-search watermark slices out only genuinely-new bars and advances even on empty slices, so duplicate pushes are harmless by construction.