Section V Composition · Sizing · Risk Figures 8–10

FutureMarket

An autonomous equity-research loop — ingestion, scoring, execution, and self-grading — described from the source

The Desk

Five opinions, one number, five ways to refuse

The compositor is where the whole system's intent becomes visible. It fuses five prediction sources into a single score, weighting each by how well that source has actually performed — and then it spends most of its code looking for reasons not to act.

Five sources contribute. A technical reading refreshed every four hours; a daily sentiment composite; a weekly language-model verdict; an alpha-factor score; and a quantitative score which, absent its optional dependency, is arithmetic on indicators. Each is asked for a value, and each returns a score normalised into the interval minus one to plus one, so that the fusion step is unit-free.

Normalisation differs by source in a way that matters. The two prediction-log sources — technical and language-model — are converted as a direction sign multiplied by the stated confidence, so a high-confidence downward call becomes a strongly negative number. The three score-based sources are mapped linearly from their zero-to-hundred range by subtracting fifty and dividing by fifty. A source reporting exactly fifty contributes exactly nothing.

That last property is a quiet hazard. Three of the five sources return a neutral fifty when they fail, and two of those failures are common: the quantitative scorer returns fifty when its package is absent, and the alpha scorer returns fifty when no factors have been promoted to active. Both are indistinguishable, at the fusion step, from a source that looked carefully and concluded nothing.

The weighting is the genuinely novel part. Each source has a base weight — the language-model debate carries the most at thirty per cent — and that base is multiplied by two performance terms. The first is the source's Elo rating divided by the mean Elo across all rated strategies, so a source that beats its peers head-to-head is scaled above one. The second is the calibration multiplier: if a source says seventy per cent and is right eighty per cent of the time, it is scaled up; if it is right sixty, scaled down.

Both terms are computed from reconciled outcomes, so the compositor's opinion of its own inputs is derived from evidence rather than assumption. This is the design's best idea.

Then come the refusals. A composite inside a narrow deadband is a hold. A confidence below the threshold is skipped. A daily trade count at its cap is skipped. A missing entry price is skipped. And a position check against the broker is skipped if it fails — the exception path returns a skip, not a proceed, which is the correct direction for a system that can spend money.

The compositor's opinion of its own inputs is derived from evidence rather than assumption. That is the design's best idea.

On Elo-weighted and calibration-weighted fusion

Figure 8 · Composition

From five scores to one decision

Figure 8 Signal composition, performance weighting, and the five skip gates
Signal compositor: five sources, performance weighting, and rejection gates Five source lanes each produce a normalised score between minus one and plus one, with a base weight. A weighting stage multiplies each base weight by an Elo factor and a calibration factor, applies a floor, and renormalises. A weighted sum produces a composite score, which passes a deadband to become a direction, with confidence set to the absolute value of the composite. Five sequential gates then reject or admit the signal before it reaches the executor. FIVE SOURCES — awaited sequentially, each individually fault-tolerant technical from prediction_log freshness window 8 h sign × confidence w 0.25 sentiment live aggregator call 30-min internal cache (score − 50) / 50 w 0.15 nim_debate from prediction_log freshness window 7 days sign × confidence w 0.30 alpha_factor ICIR-weighted composite 50 if no active factors (score − 50) / 50 w 0.15 qlib optional ML package 50 if unavailable or errored (score − 50) / 50 w 0.15 PERFORMANCE WEIGHTING — the design's central mechanism effective[s] = base[s] × elo_factor[s] × calibration_factor[s] elo_factor = elo_rating(strategy) ÷ mean(all elo_ratings) — centred on 1.0; defaults to 1500 both sides when the table is empty calibration_factor = count-weighted mean of (bayesian_accuracy ÷ stated_confidence) per bin, hard-clamped to [0.5, 1.5] then floor each at 0.05 and renormalise to sum 1.0 — note the renormalise runs after the floor, so the floor is not preserved composite = Σ(scoreᵢ × wᵢ) ÷ Σ(wᵢ), clamped ±1.0 weights already sum to 1 over available sources, so absent sources do not dilute — one source alone becomes the whole composite DEADBAND HOLD SELL ← −1.0 +1.0 → BUY −0.05 +0.05 confidence := |composite| FIVE SKIP GATES — evaluated in this order, first match wins 1 · direction HOLD → skip logged, not discarded 2 · confidence < 0.55 → skip a genuinely high bar 3 · daily cap ≥ 10 trades → skip counter rolls at UTC midnight 4 · entry price ≤ 0 → skip quote, then DB fallback 5 · duplicate same-side position → skip fails closed on error AND BEFORE ANY OF IT — three cycle-level preconditions autonomy flag enabled else the job is never even registered inside market hours 09:30–16:00 ET, weekdays — no holiday calendar circuit breaker permits trading see Figure 10 ADMITTED → trade_executor direction mapped BUY→UP, everything else→DOWN logged twice under compositor_v1 — once here, once in the executor SKIPPED → still written to prediction_log every decision is recorded, including HOLD, with a suffixed model_version so skipped signals later enter the compositor's own backtest
Figure 8. Composition and refusal. Two properties are worth carrying forward. Because confidence is defined as the magnitude of the composite, the 0.55 gate demands a weighted average of normalised source scores exceeding 0.55 — a high bar that few multi-source readings will clear. And because skipped decisions are logged under the same strategy identifier as executed ones, the compositor's own backtest is evaluating a population dominated by trades it declined to make. backend/services/signal_compositor.py:55–64 (weights), :95–181 (compose), :183–278 (cycle), :536–580 (effective weights), :584–611 (gates), :633–651 (logging), :693–718 (market hours)

Figure 9 · Execution

Sizing the position

Sizing uses half-Kelly against a fixed reward-to-risk ratio derived from the default take-profit and stop-loss percentages, then caps the result at a fixed fraction of equity at risk. Both constants are marked in the source as provisional — a comment notes they will move to configuration in a later phase.

Figure 9 Execution chain: shrinkage, half-Kelly sizing, pre-trade checks, bracket
Trade execution chain from validated signal to bracket order A left-to-right chain. A validated signal passes a minimum confidence check, then a Bayesian calibration shrinkage step, then half-Kelly position sizing against a two per cent risk cap, then a circuit-breaker size multiplier, then four pre-trade checks, then bracket price construction and order submission. A lower band shows the half-Kelly formula expanded with its constants and notes which constraint binds. EXECUTION CHAIN — every rejection is an early return, never an exception 1 · validate direction ∈ UP, DOWN confidence ≥ 0.55 2 · shrinkage 0.5 + (c − 0.5) × factor runs after the 0.55 gate 3 · half-Kelly shares from equity capped at 2% risk 4 · breaker scale multiplier 1.0 / 0.5 / 0.0 applied after Kelly 5 · pre-trade four checks see below 6 · submit bracket order no fill check STEP 3 EXPANDED — half-Kelly against a fixed reward-to-risk ratio b = take_profit_pct / stop_loss_pct = 0.05 / 0.03 = 1.667 full_kelly = (confidence × (b + 1) − 1) / b half_kelly = max(0, full_kelly × 0.5) risk_shares = (equity × 0.02) / (price × stop_loss_pct) kelly_shares= (equity × half_kelly) / price qty = int(min(kelly_shares, risk_shares)) WHICH CONSTRAINT BINDS The 2% risk cap permits notional up to 0.02 ÷ 0.03 ≈ 67% of equity while half-Kelly at the minimum admissible confidence of 0.55 already yields ≈14%. Kelly is therefore the binding constraint in normal operation; the risk cap rarely bites. STEP 5 EXPANDED — broker-side pre-trade checks buying power buys only; qty × price must clear pattern day trader blocked under $25k with 3+ day trades concentration single name capped at 5% of portfolio position count maximum 10 simultaneous names STEP 6 — bracket construction UP take_profit = entry × 1.05 stop_loss = entry × 0.97 DOWN take_profit = entry × 0.95 stop_loss = entry × 1.03 Note — the executed flag is set on submission success alone. There is no fill confirmation, no polling, no partial-fill handling. Note — the reward-to-risk ratio always uses the default take-profit constant, even when a caller supplies its own. Bracket prices honour the caller's value; the sizing that produced them does not. With absent broker credentials the whole chain runs against a mock account and reports success.
Figure 9. The execution chain. The ordering of steps one and two is a real defect: the minimum-confidence gate runs on the raw confidence, and the Bayesian shrinkage runs afterwards. A signal can therefore pass the gate at 0.56 and then be sized on a shrunk confidence well below it. The shrinkage itself is sound — it pulls confidence toward one half by the calibration multiplier, and halves the distance outright for any strategy with fewer than ten reconciled predictions. backend/services/trade_executor.py:44–48 (constants), :50–231 (execute_signal), :126–136 (shrinkage), :233–283 (half-Kelly) · services/alpaca_trading_service.py:268–375 (pre-trade) · services/calibration_tracker.py:158–177

Figure 10 · Circuit breaker

A state machine that declares more than it enforces

Five states, three loss thresholds, and an hourly job that feeds it the account's profit and loss. The design is conventional and correct in outline: a bad day tightens, a bad week reduces size, a deep drawdown halts, and a cooldown returns the system to normal after a day.

Three things prevent it from working as described, and all three are worth stating rather than glossing.

The cooldown state is never entered. Nothing in the codebase assigns it; the only code that mentions it is the expiry check that would leave it. A halt is therefore terminal until someone calls the manual reset endpoint.

There is no de-escalation at all. Neither the caution nor the reduced state returns to normal on its own, however well the account subsequently performs. Once tripped, the system stays degraded until reset by hand.

And the actions the breaker emits — close all positions, cancel all orders, reduce sizes, tighten stops — are returned to the caller as a list of strings, and the only production caller writes them to a log. Nothing executes them. The breaker's real effect is entirely negative: it prevents new entries. Existing positions in a fifteen-per-cent drawdown are not closed.

The input is also degraded. The caller computes daily profit and loss from the account's equity against its previous close, and then assigns that same value to the weekly figure, with a comment acknowledging that a real implementation would track five days. Since the weekly threshold is checked first in the ladder, the caution state is effectively unreachable in production.

Thresholds, as coded

Daily loss
2% → caution
Weekly loss
5% → reduced
Drawdown from peak
15% → halted
Cooldown
24 hours
Size reduction
×0.5 in the reduced state

All breaker state — the current state, the equity peak, the trigger history — lives in process memory. The module docstring says so plainly: it resets on restart. This has a second-order consequence, because one of the live-trading graduation criteria is “the circuit breaker has been tested,” evidenced by a non-zero trigger count. That evidence is erased by every deployment.

Figure 10 Circuit breaker state machine, as implemented versus as documented
Circuit breaker state machine Five states arranged left to right: normal, caution, reduced, halted, and cooldown. Solid arrows show implemented transitions driven by daily loss, weekly loss and drawdown thresholds. Dashed grey arrows show transitions that appear in the documentation but are not implemented, including the entry into cooldown and any return from caution or reduced to normal. A manual reset arrow returns any state to normal. A lower panel lists the trading permissions and position size multiplier for each state. NORMAL full size, trading on CAUTION unreachable in practice REDUCED size × 0.5 HALTED terminal until reset COOLDOWN never entered −2% day −5% week (checked first in the ladder) −15% drawdown from peak no assignment documented cooldown expiry → normal (the exit exists; the entry does not) no de-escalation path manual reset — an HTTP endpoint, the only implemented way out PER-STATE EFFECT ON THE EXECUTION PATH STATE NEW ENTRIES SIZE MULTIPLIER EXISTING POSITIONS normal permitted 1.0 untouched caution blocked 0.0 untouched reduced permitted 0.5 untouched halted blocked 0.0 still untouched — this is the gap The breaker emits an action list on every trip. Its only production caller writes that list to a warning log and takes no further step. A separate emergency-halt endpoint does close all positions — but it is manual, and it does not trip the breaker or cancel resting orders.
Figure 10. The breaker as built. Solid vermilion arrows are implemented; dashed grey arrows appear in the class docstring's stated lifecycle and have no corresponding assignment in the code. The caution state is drawn as reachable because the transition exists, but the production caller supplies the weekly figure as a copy of the daily one, and the weekly branch is tested first — so in practice a bad day jumps straight to reduced. backend/services/circuit_breaker.py:31–37 (states), :50–55 (thresholds), :78–100 (properties), :102–167 (ladder), :169–173 (reset) · caller at backend/main.py:320–343

Adjacent machinery

The factor engine and the formula generator

Five-factor composite

A separate engine fans out concurrently to five scorers — momentum, fundamentals, value, alternative data, and risk — and combines them with fixed weights that sum to one. Any scorer that raises degrades to a neutral fifty rather than failing the request.

Confidence is derived from how many scorers succeeded, with bonuses for unanimity and for extreme readings. That confidence then penalises the score before bucketing: the composite is multiplied by a factor running from 0.70 at zero confidence to 1.00 at full confidence, and the product is compared against thresholds. Because the multiplier caps at one, the strongest bucket requires a raw composite of at least seventy-five even under perfect confidence.

The module's docstring promises adaptive weighting based on recent factor performance. There is no adaptation in the file. The weights are static, and the normalisation method that exists for rebalancing them is never called.

LLM-authored alpha

The alpha factor service is the repository's most ambitious component. It asks a language model to write formulaic alpha expressions in a small domain language — rolling means, rank transforms, time-series deltas, correlations — then evaluates each expression against real price history and keeps the ones that work.

Expressions are evaluated in a sandbox: a whitelist regular expression rejects brackets, quotes and assignment; the namespace contains twenty-two vetted functions and the available price columns; builtins are emptied. Every generated expression is validated against a random synthetic frame before it is stored.

Each factor is scored by information coefficient — the rank correlation between the factor's value and forward returns — and by the ratio of that coefficient's mean to its standard deviation across rolling windows. A factor promotes to active on a single good evaluation; retirement requires a poor ratio and at least three evaluations. A separate decay rule marks a factor as decayed when its most recent coefficient falls both below an absolute floor and below half its own historical mean.

Live scoring z-scores each active factor's current value against its own history, weights by information ratio, and maps the result through a normal cumulative distribution to a zero-to-hundred score. Negative-ratio factors are floored at a small positive weight rather than excluded or sign-flipped, which means a reliably wrong factor still contributes in its wrong direction.

Fixed weights and thresholds across the three scoring layers
LayerComponents and weightsDecision thresholds
Signal compositor LLM debate 0.30 · technical 0.25 · sentiment 0.15 · alpha 0.15 · quant 0.15 (then scaled by Elo and calibration) Deadband ±0.05; act above 0.55 confidence
Factor scoring engine Momentum 0.30 · fundamentals 0.25 · alternative 0.20 · value 0.15 · risk 0.10 (static) 75 / 60 / 40 / 25 on the confidence-adjusted score
  ↪ alternative sub-blend Insider 0.40 · options flow 0.35 · sentiment 0.25
Sentiment aggregator Transformer news 0.50 · earnings tone 0.30 · filing risk 0.20 (renormalised over available) ≥60 bullish, ≤40 bearish
Alpha composite Per-factor information ratio, floored at 0.01 ≥65 buy, ≤35 sell
Quantitative score Rule-based additive: RSI bands, MACD histogram, 20/50 crossover, Bollinger position ≥65 buy, ≤35 sell

One sandbox caveat, stated honestly

The expression whitelist permits dots, underscores and parentheses, which means attribute traversal is not lexically blocked; emptying builtins blunts the classic escape but does not close it definitively. The practical exposure is limited because the expressions are authored by the model rather than by an end user, and no route accepts a user-supplied expression. It remains a place where a stricter parser would be better than a regular expression.

Separately, one alias in the function table shifts a series by the negation of its argument. With a negative argument it correctly returns a past value, which is what the accompanying comment describes. With a positive argument it returns a future one. Nothing validates the sign, and the expressions are model-authored.