Skip to content

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 toggle verdict.concentration_lens_enabled (ADR-017). The solvency-related sections below are retained as historical records; the TRIM_REASON extraction, 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) ​

  1. CIO prompt adds a format requirement: When outputting TRIM, must include TRIM_REASON: concentration | stop_loss | bearish
  2. parse_cio_memo() adds TRIM_REASON extraction + Sanity check 4:
    if SOLVENCY=strong and verdict=TRIM and TRIM_REASON=concentration:
        → Force rewrite to HOLD + log override reason
  3. portfolio_summary_text() adds backup_cny parameter, appending real wealth ratio
  4. 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_strong orconcentration_lens_enabled=False (user disables the concentration lens via /api/config, for single-asset/deliberately concentrated strategies), it unconditionally force-HOLDs TRIM_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 fake emergency_buffer_cny to trigger strong. Now there is a real toggle. The out["_concentration_lens"] flag distinguishes the two triggers (disabled vs sanity4_solvency), allowing the intervention counterfactual ledger to bucket them separately (config_concentration_lens_off vs sanity4_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_lens only outputs disabled, and the rule only outputs config_concentration_lens_off (historical sanity4_solvency_concentration is still merged into the trim_blocked bucket by rule_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 extract EXECUTION_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.?path yields 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.

FileChange
utils/portfolio_summary.pybackup_cny parameter + real wealth ratio appendix
agents/skills/cio/SKILL.mdTRIM_REASON output field
core/committee.py (now core/committee/cio_parse.py)parse_cio_memo() TRIM_REASON extraction + Sanity 4
core/committee_runner.pyPre-load wealth_view + pass backup_cny
jobs/daily_report.pyCron path passes backup_cny
scripts/skill_cmds/committee_cmds.py:cmd_run_committeeSkill path passes solvency_strong + backup_cny (entry façade remains scripts/skill.py)