A small, layered, broker-agnostic algorithmic-trading research engine — and an honest one, which mostly means it's very good at telling you your brilliant strategy is actually noise. It scans markets, tests strategies, validates them out-of-sample, allocates portfolios, and exposes the whole research surface to AI agents without ever handing them order-execution authority. It ships with an Alpaca adapter, but everything above the broker layer is vendor-neutral. It scans a universe of symbols, runs a strategy over them, and either backtests on history or trades live (paper by default) — with optional parameter optimization, walk-forward validation, and constraint-solver portfolio allocation.
Full docs — usage guide and engineering wiki — live at tradeflow.mk-dir.com.
Making money in markets is genuinely hard. This project won't change that — but it will at least stop you from fooling yourself quite so quickly, which is most of the battle.
Designed to be easy to try and easy to read:
- No TA-Lib / no native build step — indicators are pure pandas/numpy, so
uv syncis all you need and the Docker image carries no compiler toolchain. - Broker-agnostic — everything is written against a
Broker/MarketDataProviderinterface. Alpaca is the first implementation; dropping in another venue means writing one adapter, nothing else. - Strict separation of concerns — each layer does one job (see below).
⚠️ Educational software. Trading is risky; use paper trading. No warranty.
make demo runs the whole pipeline on synthetic data (no keys, no network) and
renders this: a strategy that looks profitable in-sample, and the out-of-sample
verdict that calls it noise. The refusal is the product. The same panels render
for any run — pass --chart PATH to backtest or walkforward.
The one idea that explains everything else — TradeFlow runs on two clocks that never touch:
- Research clock (offline, slow, exploratory): backtest → optimize → walk-forward, plus the optional AI agent. Non-determinism and LLMs are allowed here. It only ever proposes — writing provenance-stamped configs to disk.
- Trade clock (live, deterministic, LLM-free):
live bar → signal → order. No model sits in the order path, so there's nothing to prompt-inject and nothing non-deterministic to debug when real money is at stake.
Promotion is a manual human step — automation never flips PAPER_TRADE or
places an order. The MCP server enforces this
structurally: it builds only a data client, so it physically cannot trade. See
the architecture docs
for the full picture.
You need either of these — not both:
- uv — the Python package manager used to run everything locally, or
- Docker — to build and run the app in a container (no local Python or uv needed).
The Makefile targets run through uv (e.g. make backtest → uv run …), and
there are separate Docker targets (make docker-build, make docker-run).
Pick whichever you prefer.
Either way you'll need free Alpaca paper-trading API keys from the Alpaca dashboard → Paper Account → API Keys.
# 1. Install uv: https://docs.astral.sh/uv/
# 2. Install dependencies
make install # uv sync
# 3. See it work — no keys, no network
make demo # full pipeline on synthetic data + verdict
# 4. Point it at real data: add your free Alpaca paper keys
cp .env.example .env # then edit .env
# 5. Try it (preconfigured combos)
make scan # which symbols are flagged right now?
make backtest # scan -> volume_spike strategy -> report
make live # paper-trade the scanned universeNo local Python or uv required — just Docker:
cp .env.example .env # add your Alpaca paper keys
make docker-build # build the image (uv runs inside it)
make docker-run # paper live-trading; mounts your .env
# or run any command in the container directly:
docker run --rm -v $(pwd)/.env:/app/.env tradeflow \
uv run python main.py backtest --symbols NVDA,META --start 2024-01-02 --end 2024-04-01Run make help to see every target. Anything is overridable inline:
make backtest SYMBOLS=AAPL,MSFT,NVDA START=2024-06-01 END=2024-09-01 CAPITAL=50000Or call the CLI directly:
uv run python main.py backtest --strategy volume_spike --scanner volume \
--symbols NVDA,META,TSLA --start 2024-01-02 --end 2024-04-01make demo runs the entire pipeline on a seeded random walk — no keys, no
network — and ends in a promotion verdict. The point isn't a winning strategy;
it's that the refusal to promote a noise strategy is the product:
$ make demo
======================================================================
TradeFlow demo — synthetic data, no API keys, no network
(a seeded random walk: realistic-looking, no actual edge)
======================================================================
1) In-sample backtest of every registered strategy
In-sample, almost anything looks tradeable. That's the trap.
STRATEGY RETURN SHARPE TRADES
-------------------------------------------
volume_spike 0.00% 0.00 0
ma_crossover 16.80% 0.48 52
mean_reversion -15.77% -0.52 52
2) Walk-forward validation of 'ma_crossover' (the honest scorecard)
Optimize in-sample, score out-of-sample across folds, then gate it.
... (per-fold parameter search elided) ...
Best sharpe_ratio = 1.1499 with {fast_ema_period: 13, slow_ema_period: 60, ...}
OOS Sharpe (median): -0.42 efficiency (OOS/IS): -0.37 OOS trades: 29
Promotion gates:
[FAIL] oos_sharpe: -0.42 (threshold 1.0)
[FAIL] oos_profit_factor: 0.71 (threshold 1.3)
[FAIL] walk_forward_efficiency: -0.37 (threshold 0.4)
[FAIL] oos_drawdown_vs_is: 23.57 (threshold 15.43)
[FAIL] min_oos_trades: 29 (threshold 100)
[FAIL] deflated_sharpe: 0.00 (threshold 0.5)
Verdict: NOT promotable
No edge in a random walk -> the gates refuse to promote it. That refusal
is the product. Point TradeFlow at real data with `make backtest`.
Notice the arc: ma_crossover looks great in-sample (+16.8%, Sharpe 0.48), but
once it's optimized in-sample and scored out-of-sample the edge evaporates
(median OOS Sharpe −0.42) and every promotion gate fails. That's
walk-forward validation
doing its job.
| Command | What happens |
|---|---|
demo |
Run the whole pipeline on synthetic data — no keys, no network — ending in an honest promotion verdict |
scan |
Run the universe scanner and print flagged symbols |
backtest |
Scan → run a strategy over history → performance report |
live |
Scan → warm up indicators → stream bars → place paper/live orders |
optimize |
Search strategy parameters by backtest objective (grid / random / Bayesian) |
allocate |
Weight a portfolio: scalar-score sizing (OR-Tools), or --objective utility for mean-variance construction from alpha + Σ |
alphas |
Rank a universe by continuous alpha — a comparable, annualized residual-return forecast per name; --combine blends several signals, --neutralize-factors regresses out the risk model's factor exposures (read-only) |
risk |
Estimate the universe covariance Σ (Ledoit–Wolf shrinkage) and summarize its risk structure (read-only) |
info |
Information report: measure IC, breadth, and predicted-vs-realized IR — skill vs luck (read-only) |
horizon |
Measure alpha decay / half-life; recommend rebalance cadence + current/lagged blend (read-only) |
walkforward |
Out-of-sample validation: optimize in-sample, score out-of-sample across folds, with a sacred holdout and promotion gates |
mcp |
Serve TradeFlow over MCP so an agent (Claude Code / Desktop) can drive scan/backtest/optimize/walk-forward/alphas/risk/portfolio/info — read-only, no live trading |
Three strategies ship today — pick one with --strategy. Each defines a single
continuous score (its conviction); the trade clock's BUY/SELL/HOLD and the
continuous alpha are both
derived from it — one source of truth.
volume_spike— long/short EMA-trend strength scaled by volume confirmation (intraday, 5-minute bars).ma_crossover— long-only EMA trend follower: the normalized fast−slow gap, whose sign crossings are the golden / death cross (daily).mean_reversion— long-only RSI mean reversion: score is oversold-ness, enter the dip and exit on the rebound (daily).
Adding a fourth is a one-file change — see Extending.
Capabilities are optional extras so the base install stays lean:
make install-optimize # scikit-learn, for `optimize --method bayesian`
make install-portfolio # Google OR-Tools, for `allocate`
uv sync --extra mcp # the MCP SDK, for `python main.py mcp`This is an evolving research project, not a production trading platform. To keep that honest, here's what's load-bearing versus what's still maturing:
| Capability | Status |
|---|---|
| Broker / market-data abstractions | ✅ Stable |
| Offline backtesting + analytics | ✅ Stable |
| Pure pandas/numpy indicators | ✅ Stable |
| Universe scanning | ✅ Stable |
| Offline test suite (in-memory fakes) | ✅ Stable |
| Parameter optimization — grid / random | ✅ Stable |
| Walk-forward validation + promotion gates | 🧪 Experimental |
| Parameter optimization — Bayesian | 🧪 Experimental |
| Portfolio allocation (OR-Tools) | 🧪 Experimental |
| Live paper trading | 🧪 Experimental |
| MCP server | 🧪 Experimental |
| Research agent | 🧪 Experimental |
"Experimental" means the interfaces and gate thresholds may still change — not that the code is untested. Everything ships with offline tests.
AI-assisted research without AI-controlled trading.
python main.py mcp exposes TradeFlow's deterministic capabilities to any MCP
client (Claude Code / Claude Desktop) as tools: discovery, run_scan,
run_backtest, run_optimization, run_walk_forward, get_metrics_glossary,
summarize_bars, and save_config/load_config/list_configs. Every call is
logged to logs/mcp_audit.jsonl for replay.
The safety model is structural. The server constructs only a data client —
no trading client, no broker — so it is incapable of placing an order. There is
no place_order, start_live, cancel, or set_paper_trade tool; promoting a
config to live is a manual human step outside MCP. The capability simply isn't
wired in, so it can't be prompt-injected around. The agent works on the
research clock (offline, exploratory); the live order path stays LLM-free.
Register it with a client (Claude Desktop / Claude Code mcpServers):
{ "mcpServers": { "tradeflow": {
"command": "uv",
"args": ["run", "--extra", "mcp", "python", "main.py", "mcp"],
"cwd": "/path/to/tradeflow" } } }python main.py research runs a bounded, offline loop that proposes hypotheses,
validates them out-of-sample with walk-forward, and writes a shortlist of
provenance-stamped candidate configs to configs/ for a human to review. It never
promotes anything to live trading.
The proposer is provider-agnostic — choose with --provider:
| Provider | Install | Default model | Credential |
|---|---|---|---|
anthropic (default) |
uv sync --extra ai |
claude-opus-4-8 |
ANTHROPIC_API_KEY |
openai |
uv sync --extra openai |
gpt-4o |
OPENAI_API_KEY |
ollama (local) |
none | llama3.1 |
none |
Set the credential in .env (alongside your Alpaca keys — see .env.example)
or as the standard environment variable. Ollama runs locally and needs no key.
uv run python main.py research --provider ollama --model llama3.1 \
--symbols NVDA,AAPL --start 2024-01-01 --end 2025-12-31 \
--goal "improve OOS Sharpe without raising max drawdown" --holdout-days 60See the docs (Usage → AI agents) for the full tool surface, guardrails, and provider setup.
The codebase is organized into single-responsibility layers. Nothing above the broker layer imports a vendor SDK. The two clocks never touch — automation only ever proposes a config; a human promotes it:
flowchart LR
subgraph research["Research clock — offline · LLM-allowed"]
direction TB
H[hypothesis] --> B[backtest] --> O[optimize] --> W[walk-forward]
W --> C[(provenance-stamped<br/>config)]
end
subgraph trade["Trade clock — live · deterministic · LLM-free"]
direction TB
Bar[live bar] --> Sig[signal] --> Ord[broker order]
end
C -. "human promotes<br/>(nothing auto-flips)" .-> Bar
The layers themselves:
brokers/ Broker interface + domain types ── alpaca/ (AlpacaBroker, AlpacaMarketData)
marketdata/ MarketDataProvider interface, Timeframe, MarketDataClient
indicators/ Pure pandas/numpy technical indicators
strategies/ Strategy base + signals + volume_spike (signals, sizing, risk)
scanners/ ScannerStrategy base + volume scanner + SymbolScanner (universe)
execution/ LiveTrader (signals -> broker orders)
analytics/ Performance metrics + reporting
engine/ BacktestEngine + LiveEngine (orchestration only)
optimization/ ParameterSpace + ParameterOptimizer (tune params via backtest)
portfolio/ PortfolioAllocator (OR-Tools MIP position weighting)
utils/ logging, numeric, time helpers
Data flows the same way in both modes:
marketdata → strategy.process_data → strategy.generate_signals
→ engine (simulate fills | route to execution) → analytics
To work on them locally:
make docs # serve the Docusaurus site at http://localhost:3000make docker-build
make docker-run # paper live-trading; mounts your .envmake test # offline suite — no API keys or network requiredThe whole stack is testable offline because every layer depends on the broker/data abstractions; tests inject in-memory fakes.
Lint and format with ruff (what CI runs):
uv run ruff check . # lint
uv run ruff format --check . # formatContributions and ideas are very welcome — whether it's a bug fix, a new strategy or scanner, an additional broker/data adapter, an LLM provider, docs, or just a feature request or suggestion. If you have an idea, open an issue to start the conversation; if you have a fix, open a PR. No contribution is too small, and feedback on what would make TradeFlow more useful is always appreciated.
And hey — don't be greedy: share your algos, let's make money together. 📈 (Worst case, we lose money together, which is basically friendship.)
See CONTRIBUTING.md for setup, the pre-push checks, and the coding standards (layering rules, separation of concerns, no vendor SDK above the broker layer). CI runs ruff lint + format + the test suite on every PR.
make cancel-orders # cancel all open orders
make close-positions # liquidate all positions (and cancel orders)If this code somehow makes you money, I'd genuinely love to hear about it. If it loses you money — we've never met, and this is the first you're hearing of it.
Either way, if it saved you some time, you can buy me a coffee:
