ADR-013: Concentration Does Not Trigger TRIM When SOLVENCY=strong (Deterministic Post-Processing)
⚠️ 2026-06-23 (ADR-019): The solvency auto-trigger from this ADR has been removed. Sanity check 4 no longer overrides position reduction due to
SOLVENCY_BUFFER_LEVEL=strong— it silently flipped the CIO's position reduction at the parse layer, masking real concentration risk. Whether concentration constitutes a constraint is now solely controlled by the explicit toggleverdict.concentration_lens_enabled(ADR-017). The solvency-related sections below are retained as historical records; theTRIM_REASONextraction, lens-off hard fallback, and TRIM pathing TODO remain valid.
Status
Superseded (solvency trigger portion) — Established 2026-05-28, Removed 2026-06-23 by ADR-019
Context
On 2026-05-28, the committee output a TRIM for Gold GC=F because the Risk Officer reported concentration > 60%. However, the user had a ¥4M family backup (SOLVENCY_BUFFER_LEVEL=strong), meaning the investment account going to zero would not affect their livelihood. High concentration only means "imbalance within the account," not "real wealth risk."
PR #14 (merged) added a "play money account TRIM constraint" at the CIO prompt layer, but this is a pure prompt constraint — the LLM might ignore it. This ADR introduces a deterministic post-processing fallback.
Decision
Core Principle
When SOLVENCY_BUFFER_LEVEL=strong (real wealth backup is sufficient, investment account going to zero does not affect livelihood):
- High concentration within the account should NOT trigger a position reduction (TRIM), it should only limit position increases (BUY/ACCUMULATE)
- The rationale for reducing a position is real wealth risk; when the backup is sufficient, real wealth risk does not exist
- However, if the account is too heavily skewed towards a single asset, no more should be added to it
Implementation: Deterministic Post-Processing (Not Prompt-Dependent)
- CIO prompt adds a format requirement: When outputting TRIM, must include
TRIM_REASON: concentration | stop_loss | bearish parse_cio_memo()addsTRIM_REASONextraction + Sanity check 4:if SOLVENCY=strong and verdict=TRIM and TRIM_REASON=concentration: → Force rewrite to HOLD + log override reasonportfolio_summary_text()addsbackup_cnyparameter, appending real wealth ratio- Uses the same mechanism as existing BUY→ACCUMULATE (Sanity 1), WORKER→HOLD (Sanity 3)
2026-06-17 (ADR-017): Sanity check 4 now has a dual trigger — when
solvency_strongorconcentration_lens_enabled=False(user disables the concentration lens via/api/config, for single-asset/deliberately concentrated strategies), it unconditionally force-HOLDsTRIM_REASON=concentration(the lens-off path does not depend on solvency). This decouples "I have a family backup (emergency_buffer)" from "Don't nag me about concentration for position reduction": previously, the only way to avoid this was to input a fakeemergency_buffer_cnyto triggerstrong. Now there is a real toggle. Theout["_concentration_lens"]flag distinguishes the two triggers (disabledvssanity4_solvency), allowing the intervention counterfactual ledger to bucket them separately (config_concentration_lens_offvssanity4_solvency_concentration), avoiding statistical confusion. The prompt layer (agents/cio.py/agents/risk_officer.py) also synchronizes soft suppression of overweight rules. See 017-config-via-api.2026-06-23 (ADR-019) Follow-up: The "dual trigger" above has been consolidated into a single trigger — the solvency path has been removed. Sanity 4 now only triggers when the lens is off;
_concentration_lensonly outputsdisabled, and the rule only outputsconfig_concentration_lens_off(historicalsanity4_solvency_concentrationis still merged into thetrim_blockedbucket byrule_family()).
Relationship with PR #14
PR #14 added a constraint at the prompt layer: "play money account + unrealized loss < 5% does not allow TRIM." The Sanity check 4 in this ADR is a post-processing fallback; the two do not conflict:
- PR #14's constraint relies on LLM compliance (may fail)
- Sanity check 4 is deterministic (if the LLM outputs TRIM + concentration, it will be overridden)
- Both can coexist: PR #14 blocks TRIM for unrealized loss < 5%, Sanity 4 blocks concentration TRIM for SOLVENCY=strong
Unchanged Parts
- Logic for SOLVENCY=weak/unknown remains unchanged (the account may represent the user's entire net worth, so concentration should trigger position reduction)
- TRIMs for stop_loss / bearish reasons are unaffected (these are real risk signals)
TODO: TRIM Pathing (Probability Table Upgrade)
The TRIM signal is currently a purely static snapshot decision, lacking a "post-sale expected path / re-entry point":
- The CIO output schema has no TRIM-specific re-entry fields
parse_cio_memo()does not extractEXECUTION_PLAN(it is all discarded)- Backtesting/review only looks at post-hoc price direction, not "whether the asset was bought back at a favorable price"
- Full codebase search for
buy.?back/re.?entry/price.?pathyields zero results
The current probability table is a 30-day single-point distribution ((asset, regime) → P(up > 5%) / P(down > 5%)), which needs to be upgraded to a path distribution (multi-time-step joint distribution) to support path-based decisions like "there is a Y% probability of falling to price Z within X days after selling." This is a major undertaking and will be done later.
Related Files
| File | Change |
|---|---|
utils/portfolio_summary.py | backup_cny parameter + real wealth ratio appendix |
agents/skills/cio/SKILL.md | TRIM_REASON output field |
core/committee.py (now core/committee/cio_parse.py) | parse_cio_memo() TRIM_REASON extraction + Sanity 4 |
core/committee_runner.py | Pre-load wealth_view + pass backup_cny |
jobs/daily_report.py | Cron path passes backup_cny |
scripts/skill_cmds/committee_cmds.py:cmd_run_committee | Skill path passes solvency_strong + backup_cny (entry façade remains scripts/skill.py) |