Strong stances, formed from real user pain.
This document captures the design convictions that shape every Franklin Trading PR. Each is non-negotiable and traceable to specific failures in upstream LLM-trading projects (TradingAgents, AI-Trader, Vibe-Trading) or seven years of Hummingbot's production scar tissue.
When a PR conflicts with a conviction, the PR loses. When a model improves to the point a conviction's underlying gap closes, we delete the conviction (per ADR 0003) and the mechanism that enforced it — never the other way around.
Evidence:
- TradingAgents #814 — ticker
TOTDY(Japanese bathroom fixtures) hallucinated as TotalEnergies SE. 5 of 6 analysts failed. 1,500-line wrong report. - TradingAgents #781 — gold price returned
431vs actual4,715(10× wrong). - TradingAgents #830 — user quote: "Can I reliably trust the tool on basic things such as price data?"
- TradingAgents closed #828 (prompt-quality improvements) as "not planned" — a permanent reputation wound.
Position: Every persona output passes through a deterministic
entity resolver (the fact-check.ts moat). In the TUI, every claim shows
the source citation ("BTC from coingecko 2026-05-23T14:32Z") — not
just "BTC price." The fact-check isn't a backend gate, it's a UI element.
Sales-grade transparency, not best-effort hidden code.
Enforced by: src/agent/fact-check.ts (M3) + the persona output
schemas + a regression set that includes the TOTDY repro + 50 ambiguous
tickers.
Evidence:
- AI-Trader #188 — HD price returned $338.91 vs actual $350.34 (3.26% off); BA $218.88 vs $226.76 (3.48% off). User quote: "Trading bots record incorrect entry prices; followers cannot replicate signals."
- AI-Trader #185 — restricted symbol coverage and no real-time quotes.
- AI-Trader has not fixed #188; it sits open as a credibility wound.
Position: Every price the agent uses for a decision has a fetchedAtMs
timestamp surfaced in the response. If age > 60s for crypto or
age > 5 min for equities, the agent refuses to submit an order
against it (not "warns" — refuses). The slippage moat (src/trading/slippage.ts)
and the staleness check are the same code path: cost-of-uncertainty.
Enforced by: src/trading/slippage.ts + a connector contract requiring
fetchedAtMs on every price quote.
Evidence:
- AI-Trader #207 — a quantitative trader publicly demanding equity curves, Sharpe ratios, walk-forward validation, transaction-cost modeling, out-of-sample analysis. Maintainer response: silence. Issue remains open. AI-Trader is structurally unfit for institutional adoption until this is answered.
- TradingAgents #838 — user pointing at AlphArena.io as a verified P&L leaderboard, signaling distrust of backtest claims.
- TradingAgents #805 — temporal knowledge leakage: a 2026-trained model backtesting 2023 has hindsight bias baked into weights.
Position: Every strategy ships with an auto-generated
backtest-report.md containing: walk-forward Sharpe + Sortino + Calmar,
max drawdown, regime-broken-down results, slippage-adjusted vs raw,
monthly equity curve PNG, transaction-cost decomposition, temporal-leakage
disclosure (which knowledge cutoff the model has vs the backtest dates).
Published quarterly in docs/benchmark/<YYYY-Q>/<strategy>.md and
auto-regenerated on every tagged release.
The pitch: "Show me your Sharpe" gets answered in 30 seconds.
Enforced by: src/backtest/walk-forward.ts + src/backtest/regime-tagged-metrics.ts (M1) + CI artifact upload on tag.
Evidence:
- Vibe-Trading #100 — the OKX team offered to contribute live trading code via OKX Agent Trade Kit; maintainers have not landed it. Users with proven backtest strategies cannot deploy them.
- TradingAgents #880 — user building an external paid-review API because no live execution path exists.
- TradingAgents #867 — institutional trade-commitment framework requested.
Position: The Strategy DSL executes the same .strategy.ts
file in backtest | paper | live from M1 — not a deferred M5
feature. Slippage parity is enforced (the same slippage.ts model runs
in all three modes). One artifact, three contexts.
This conviction promotes Strategy DSL + unified lifecycle from M5 to M1. See ADR 0004.
Evidence:
- TradingAgents #479 — in long sessions, configured risk limits silently drop from context as compaction fires. The agent stops respecting them without erroring. A blow-up machine.
Position: Risk caps (per-position USD, max drawdown, kill switch
threshold, regime-conditional limits) live in ~/.blockrun/risk-config.json
and are checked by code, not by LLM, before every order submission.
The Risk Officer persona's schema is just a display of what the code
already decided. The LLM is never the last line of defense.
Enforced by: src/trading/risk.ts (extended in M3) + a runtime
invariant: every order through TradingEngine — openPosition() AND
closePosition() — is re-evaluated by RiskEngine.check (inputs, cash
including the exchange fee, caps, sell integrity) before it reaches the
venue, and the delivered fill is checked again after. Since 0.3.0 the
post-execution check is record-then-flag: a fill the venue already
executed is always booked and any breach is surfaced as a warning, because
a rejection after execution would leave real money at the venue that the
local book does not know about. Property-tested.
Evidence:
- TradingAgents #796 — Sentiment Analyst output varies per run. Sometimes
🟢 BULLISH (7.2/10), sometimes### Score: 6.5/10, sometimes no number at all. Downstream parsers accrete "a growing pile of fallback regexes."
Position: Each persona has a Zod schema. The runtime validates and re-prompts on schema failure (up to N retries, then escalates to a stronger model). Never accept free-form output and hope. This extends brcc's existing tool-call structured-output discipline to the persona layer.
Enforced by: src/personas/schemas/*.ts (M2) + runtime validation
in the Task subagent dispatcher.
Evidence:
- Hummingbot has fixed position-math bugs at least 4 times:
- AI-Trader #186 — position averaging bug (multi-leg short merged quantities but only showed first price).
Each connector reimplements position math; each one fails differently.
Position: One immutable PositionAccumulator shared across every
connector. Fills append to an event log; position state is derived. The
accumulator is property-tested with fast-check — 1000+ randomized
fill sequences, invariants asserted (total_cost_basis == sum(fills × prices),
qty ≥ 0, realized_pnl + unrealized_pnl == total_pnl). Each connector
just emits canonical Fill events.
We pay this tax once. Hummingbot has paid it forever.
See ADR 0005.
Enforced by: src/trading/portfolio.ts rewrite (M1) +
test/position-accumulator.property.mjs (fast-check).
Evidence:
- Hummingbot #7719 — kill switch config schema mismatch crashes startup. Users follow the docs, get
TypeError: unhashable type: 'dict', deploy unguarded because the safety mechanism failed silently. - Users mentally conflate kill switch ("halt everything") with stop loss ("close this position") — Hummingbot docs reinforce the confusion.
Position: Two distinct subsystems with no shared code:
KillSwitch— portfolio-wide, monitored in a separate watcher thread, triggersmarketCloseAll()on cumulative-loss threshold breach. Verified at startup with a test fire (assertion: the kill path is callable; fail fast if config is broken).StopLoss— per-position, attached to each Executor.
Documentation never uses the terms interchangeably. The risk.ts
section in PHILOSOPHY.md calls this out explicitly.
Enforced by: src/trading/kill-switch.ts (separate from
risk.ts, M3) + startup verification + a regression test.
Evidence:
- Hummingbot #8250 — after Hyperliquid WS disconnect+reconnect, bot logs "Subscribed to private channels" but stops trading. Live-in-logs, dead-in-practice.
- Hummingbot #7590 — Binance perpetual:
user_stream_initializedstaysfalseafter reconnect. Bot is blind to fills and margin events while believing it's connected.
Position: Every connector's WS layer implements:
- Heartbeat with 30-second silence timeout.
- On timeout: mark connector as
STALE. - While
STALE: refuse to submit orders. - Full state resync (positions + balances + subscriptions) before
returning to
READY.
Chaos-tested in test/chaos/ — the test kills the WS mid-trade and
asserts the bot stops cleanly, then recovers cleanly.
Enforced by: A connector contract type
(WsConnector<T> interface, M4) + a chaos harness.
Evidence:
- TradingAgents #832 — India support requested, unanswered.
- TradingAgents #861 — Tushare / AKShare A-share data requested, unanswered.
- TradingAgents #628 — hardcoded SPY benchmark breaks alpha for non-US tickers (mixes single-stock INR with USD index).
- AI-Trader #169 — user asking (in Mandarin) when A-shares will be supported — silenced.
- Vibe-Trading #62 — Tushare integration lacks fundamentals (income statements, balance sheets, cashflow); pre-filter strategies impossible.
HKUDS is a Hong Kong lab and still under-serves Asia. TradingAgents has PR #875 (A-shares) and #882 (Indian) in flight — they know it matters and ship slowly. We get there first.
Position: Native data integration from M4 for: A-shares
(Tushare with full fundamentals — income, balance sheet, cashflow),
HKEX, Indian NSE/BSE. Region-appropriate benchmarks (Nifty 50 for .NS,
Nikkei 225 for .T, HSI for .HK).
See ADR 0006.
Evidence: TradingAgents has 8+ provider-specific failure issues:
- #886 DeepSeek connection errors
- #851 MiniMax timeout
- #758 Qwen auth
- #843 Ollama routing to OpenAI despite config
- #831 Anthropic
effortparam 400 - #826 Unsupported keyword arg
Each PR adds a new provider; each adds a new edge case; integration debt compounds.
Position: We inherit Franklin's 55-model gateway routing for generic tasks but pin 3 production-blessed providers for the persona stack:
- Anthropic Claude Sonnet 4.6 — most personas
- Anthropic Claude Opus 4.7 — Risk Officer + Compliance Officer (highest stakes)
- DeepSeek V4 Pro — analyst-level work (cost-sensitive)
- Grok-4-fast-reasoning — real-time signal persona
Persona system prompts are regression-tested against these 4 models in CI. If a model breaks, we know within an hour. Other models are still selectable via the router for ad-hoc requests, but unsupported for the persona stack (warned at startup).
Enforced by: src/personas/index.ts + test/persona-matrix.mjs.
Evidence:
- AI-Trader #59 / #94 / #119 — committed log filenames contain
:characters (e.g.2025-10-01 15:00:00). Windows git clone fails. Three separate issues, none fixed. Windows users abandon the project.
HKUDS structurally cannot fix this because they don't dogfood on Windows.
Position: CI matrix runs build + tests on macOS, Linux, and Windows from M0. No colons, no leading dots, no symlinks in committed paths. Even though the wallet-native target user is more likely on macOS/Linux, the enterprise pitch requires a clean Windows install.
Cost: one GitHub Actions matrix row. Win: an entire competitor's blind spot.
Enforced by: .github/workflows/ci.yml matrix + a pre-commit hook
that rejects path components containing reserved Windows characters.
The convictions force four amendments to the original plan. See:
- ADR 0004 — Strategy DSL as unified backtest/paper/live lifecycle — promotes the lifecycle from M5 to M1 (convictions 4 + 2).
- ADR 0005 —
PositionAccumulatoras a property-tested primitive — adds a M1 deliverable (conviction 7). - ADR 0006 — Asia data integration on day one — extends M4 connectors (conviction 10).
- ADR 0007 — Quarterly competitive benchmark report — adds an M7 launch artifact (convictions 1 + 3).
If a future PR weakens any of these convictions, the PR description must explain why the underlying user pain has been solved upstream. "We changed our mind" is not a valid reason. The convictions retire when the gaps that created them close — not when we want to ship faster.