Skip to content

4 Role Committee ​

4 independent LLM sessions, information isolation + cross-challenge multi-round debate + REGIME neutral probability framing (only crash/unknown retain constraints). This chapter explains what each sees, what constrains them, and how they challenge each other.

← 01-architecture · Wiki Index · 03-dreaming →


Role Matrix ​

RoleCan SeeBlocked FromOutput FormatFile
Macro StrategistVIX / TNX / Global macro brief + Intraday event context event_brief (Macro only, event RAG strictly isolated)User holdings, technical indicatorsSIGNAL + STRENGTH + SCOREagents/macro_strategist.py
Quant AnalystTechnical indicators (Wilder RSI / MA / True Percentile / True TR ATR / RVOL) + REGIME neutral probability framing + Deterministic fact blocks (valuation/sentiment)User holdings, Dreaming insightsBullish/Bearish + Signal Strengthagents/quant.py
Wealth Context Officer (Role 5, cross-asset shared)wealth_context from user.md (family backup / emergency fund / account type) + portfolio cashTechnical indicators, Macro view, individual stock holding detailsReal Liquidity Rating (for Risk + CIO)agents/wealth_context_officer.py
Risk OfficerPosition concentration / unrealized profit buffer / Dreaming insights + Wealth Context Officer's real liquidity perspectiveTechnical indicators, Macro viewRisk Level + Concentration Scoreagents/risk_officer.py
CIOFull transcript of all three + Wealth Context perspective + User risk_level (included in portfolio_summary text)—5-choice verdict + confidence + allocagents/cio.py

Purpose of information isolation: Prevent LLMs from contaminating each other's views within the same context. Quant doesn't know how much the user has lost (won't bias towards "buy the dip"), Risk Officer doesn't know the RSI (won't be swayed by technical signals). Note that this "physical isolation" strictly applies only in Round 1 — see the round description in Cross-Challenge Protocol below.


One Complete Run in 6 Steps (Direct Path Perspective) ​

The round division below corresponds to the Direct path (core/committee/ package, including cron / Web GUI / skill run_committee). The Coordinator path (Claude Code spawns 4 subagents) differs slightly — see the "Two Paths LLM Call Count Comparison" at the end of this section.

Round 0  ─ Macro 1 LLM call (cross-asset shared, only 1 per run)
            ↓
Round 1  ─ Quant + Risk 2 LLM calls (parallel, independent statements)
            ↓
Round 2  ─ Quant sees Risk's last round + Risk sees Quant's last round (cross-challenge)
            ↓
Round 3  ─ Same as above (if not converged)
            ↓
[Convergence detection SIGNAL+STRENGTH stable for two rounds → early exit]
            ↓
Round 4  ─ CIO sees full transcript, 1 LLM call for verdict

Measured time (DeepSeek-Chat + 2 assets parallel + convergence exit): ~16 seconds (journal log evidence).

File: core/committee/debate.py:run_committee is the entry point; core/runner/session.py:run_committee_for_symbol is the end-to-end wrapper.

Two Paths LLM Call Count Comparison (to avoid ambiguity) ​

Common point of confusion: 4 prompt templates (roles) ≠ LLM call count. Rounds 2..N re-invoke Quant and Risk (the same roles, new prompts).

PathScenarioMacroR1R2..NCIOTypical Total per Asset
DirectMulti-asset parallel optimization / any agent single asset1 (cross-asset shared)2 (Quant+Risk)2/round15 (converged) ~ 9 (4 rounds) per asset
CoordinatorClaude Code single asset spawn subagent1 (spawned together in Round 1)3 (Macro+Quant+Risk)2/round16 (1 R2) ~ 10 (3 R2)

Why the Coordinator path doesn't share Macro: The user asks about one asset at a time, there's no "cross-asset reuse" pool; spawning Macro together in Round 1 is actually more parallel (Claude Agent({...}) sends 3 agents in one message, they run concurrently).

Direct path multi-asset measurement: 2 assets converged = 1 + 5×2 = 11 calls; 2 assets max=4 not converged = 1 + 9×2 = 19 calls; journal evidence shows 16 calls, falling between the two.

Before referencing this diagram, clarify which path you're on — the Coordinator documentation (skills/invest/references/committee-protocol.md) is written for the Coordinator path, this article is written for the Direct path. Both are correct but under different premises.


REGIME Classification and Neutral Probability Framing ​

Evolution (2026-05-31 Direction Lock Removed): The old version translated regime into a directional preset fed to Quant ("uptrend forbids bearish / no bottom fishing / sell on rallies"). Ablation on the same data proved: the entire direction lock layer = 1 leverage knob (uptrend lock) + 2 duds (downtrend lock backtest no-op, crash lock never triggered), not alpha. The current version treats regime as factual context: STRATEGY_HINT neutrally references the (asset, regime)'s OHLC 30d forward return probability framing (median / probability of falling below current price / sample count), leaving directional judgment to the LLM + data. The uptrend leverage effect is preserved as an explicit verdict.risk_profile=aggressive toggle (default steady, off). Flash crash defense is independent of regime (VIX sentinel + ATR volatility mutation ratio → deterministic buy-side downgrade in parse_cio_memo).

6 Regime Types (uptrend / downtrend / range_bound / crash / recovery / unknown) ​

Source: core/regime.py:THRESHOLDS (default thresholds + ASSET_OVERRIDES per-asset overrides)

RegimeTrigger ConditionFraming Provided to Quant
uptrend(MA20 − MA120) / MA120 ≥ +trend_ma_spread_pct% (default 3%, per-asset overrides: Gold 5% / Nasdaq 4% / Crypto 8%)Neutral probability framing (30d median/drawdown probability/n), self-determined direction
downtrend(MA20 − MA120) / MA120 ≤ −trend_ma_spread_pct%Neutral probability framing, self-determined direction
range_boundMA entanglement (spread within ±threshold) + normal volatilityNeutral probability framing + current percentile, self-determined direction
crashDual triggers (either triggers): ① Sharp drop atr_pct ≥ crash_atr_pct_min (per-asset) and return_30d ≤ −20%; ② Deep drop return_30d ≤ −30% (regardless of volatility)Forced neutral (executability constraint: any direction is hard to rationally execute during a crash)
recoverycrash not triggered + rebound ≥ 10% from recent 30-day low + price still low (true percentile < 50%)Neutral probability framing, self-determined direction

Note (aligned with core/regime.py implementation, 2026-05-26):

  • Trend determination uses deviation of MA20 from MA120 (single spread threshold), not the three-line bullish alignment of MA20>MA50>MA200; the code only calculates MA20/MA120/MA250, no MA50/MA200.
  • crash uses dual triggers (OR): Path 1 sharp drop (volatility leg AND 20% drawdown leg) + Path 2 deep drop (30-day drop ≥ 30%, regardless of volatility). Path 2 specifically catches "low-volatility slow declines" — a deep drop itself constitutes a crash, no high ATR needed.
  • recovery is a stateless implementation: approximates "just emerged from crash" using "rebounded ≥ 10% from recent 30-day low and percentile < 50%".
  • Priority: crash (highest) → recovery → uptrend/downtrend → range_bound → unknown (insufficient data).

The REGIME brief is presented at highest priority (first paragraph) in Quant's prompt.

CIO Sanity Check ​

After the CIO outputs a verdict, parse_cio_memo() automatically validates:

AnomalyAutomatic Correction
BUY and confidence ≥ buy_confidence_overdrive(0.95)Downgrade to ACCUMULATE + conf 0.6 (prevents overconfidence / prompt injection)
alloc_cny exceeds ceilingClamp (prevents LLM from reporting astronomical numbers)
Brief contains [WORKER_UNAVAILABLE]Force HOLD + conf capped (garbage in, no high-confidence verdict)
SOLVENCY=strong + TRIM(concentration)Force HOLD (sufficient buffer, concentration doesn't trigger reduction)
TRIM but buyback point missing / ≥ current priceDowngrade to HOLD (selling without buying back lower = pure loss)
Flash crash defense triggered (VIX sentinel OR ATR mutation ratio)BUY→ACCUMULATE / ACCUMULATE→HOLD (deterministic buy-side downgrade, independent of regime)
risk_profile=aggressive + uptrend + HOLDUpgrade to ACCUMULATE (explicit leverage gear, default steady unchanged)

Source: core/committee/cio_parse.py:parse_cio_memo (Sanity 0-5 + Defense + risk_profile, all deterministic post-processing).


Cross-Challenge Protocol ​

Round 1 Input (Independent Statements) ​

Quant sees:

# Asset: BetaShares Nasdaq 100 ETF (NDQ.AX)
# Market Regime (factual context + historical probability reference):
  uptrend · MA20 above MA120 +8.5% (≥ 4%) · Price percentile 78%
  → Historical 30d forward return for this regime: median +1.4%, probability of falling below current price 35%, n=1423
    (Use percentile / RSI to determine direction independently, no preset direction)
# Market Data (technical indicators + multi-cycle):
  RSI(14, Wilder): 67, MA120: 33.8, MA250: 30.1, Price percentile(2y): 78%, RVOL(20): 0.9x, ...
Please output technical signals in Quant Analyst format.

Risk Officer sees:

# Asset: BetaShares Nasdaq 100 ETF (NDQ.AX)
# User's current holdings:
  CNY: ¥50,000 / AUD: $1,000 / NDQ.AX 50 shares @ A$38.50 (unrealized profit +12%)
# Long-term patterns (Dreaming, learning from committee's own verdicts vs actual market):
  ⚠️ Repeated HOLD on NDQ.AX during downtrend, 100% missed rallies 30 days later (avg +12%, n=11) → Reduce HOLD tendency
Please output risk assessment in Risk Officer format.

Note: Quant doesn't know user holdings, Risk doesn't know RSI. This is physical isolation.

⚠️ Round nuance (important): Physical isolation only holds in Round 1. Entering Round 2+ cross-challenge, each agent sees the other's full transcript from the previous round (see next section), so Risk's holding numbers enter Quant's context via Risk's statement, and Quant's technical numbers enter Risk's context via Quant's statement — R2+ is no longer physically isolated, relying on prompt constraints: quant/SKILL_rebuttal.md and risk_officer/SKILL_rebuttal.md explicitly state "only reference the other's conclusions (SIGNAL/STRENGTH), do not recalculate using their raw numbers (RSI/percentile/concentration)".

Round 2..N Input (Cross-Challenge) ​

Each agent sees the other's previous round output and is asked to respond rather than restate independently:

# This is Round 2 of cross-challenge (max 4 rounds)
# Last round Quant said:
  [Quant Round 1 Full Text]
# Last round Risk said:
  [Risk Round 1 Full Text]

Please re-evaluate:
- Do you maintain your original judgment or are you convinced by the other? Why?
- What data is the other party ignoring?
- Output your new SIGNAL + STRENGTH (if changed, explain the basis for the change)

This step makes the LLM truly discuss, not just talk past each other.

Convergence Detection ​

Implementation: core/committee/debate.py:_check_convergence

python
# Two consecutive rounds, both Quant and Risk's respective SIGNAL + STRENGTH are stable
if (quant_signal_N == quant_signal_N-1 and
    abs(quant_strength_N - quant_strength_N-1) < 1.0 and
    risk_signal_N == risk_signal_N-1 and
    abs(risk_strength_N - risk_strength_N-1) < 1.0):
    converged = True  # Early exit, don't waste tokens

Most committees converge by Round 2-3 in practice (evidence: final_round=3, converged=True is a common value).


CIO's 5 Verdict Options ​

The CIO cannot fabricate; must choose from these 5:

VerdictMeaningTypical alloc Range
BUYStrongly recommend buying+3000 ~ +50000 CNY
ACCUMULATESlowly add positions (recommend in batches)+1000 ~ +5000 CNY
HOLDDo nothing0
TRIMPartially reduce positions (free up ammunition)-3000 ~ -20000 CNY
SELLSell everything (very rare)-20000 ~ -100000 CNY

The CIO also outputs:

  • confidence: 0.0~1.0 (clamped by sanity check)
  • alloc_cny: int (suggested amount)
  • dominant_view: macro|quant|risk (which party convinced the CIO)
  • execution_plan + risk_plan (detailed execution + stop-loss)

View Prompts Online ​

GUI /committee → "4 Roles + Rules" tab directly displays the complete system prompts for all 4 roles + REGIME threshold table. Anyone can see how the LLM is constrained, no black box.

Backend endpoint: GET /api/regime_rules → returns full prompts + REGIME thresholds + CIO sanity check checklist.


Dual Execution Paths ​

Note: The same 4 prompts + the same cross-challenge protocol, two execution implementations:

PathCoordinatorWorker ImplementationModel
SkillUser's ClaudeClaude Agent({subagent_type}) truly spawns 4 subagentsClaude 4
Web/Croncore/committee/4 SDKAgent + ThreadPoolExecutor same-process multi-threadingDeepSeek

See 04-execution-paths.md and adr/001-dual-execution-paths.md for details.


Next Steps ​

→ 03-dreaming.md — How long-term memory is accumulated (where the "historical behavior patterns" seen by the Risk Officer come from)

→ adr/001-dual-execution-paths.md — Why keep dual paths without merging

→ 07-extending.md#adding-new-agent-roles — How to add an ESG analyst