From aedabdd37b5da27024dccc05f5a443df89abcf12 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Thu, 18 Jun 2026 00:06:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(quant-engine):=20v8.9=20=EC=A0=9C=EC=95=88?= =?UTF-8?q?=EC=84=9C=20P0-P3=20=EB=A1=9C=EB=93=9C=EB=A7=B5=20=EC=B1=84?= =?UTF-8?q?=ED=83=9D=20=E2=80=94=2015=EA=B0=9C=20=EC=9D=98=EC=82=AC?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EC=97=94=EC=A7=84=20=EC=8B=A0=EA=B7=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml의 implementation_todo_v8_9(P0~P4) 전체를 spec/tool/golden case 레벨로 구현. - P0: PORTFOLIO_TRANSITION_UTILITY_V1, SELL_LOT_PARETO_SELECTOR_V1, FORECAST_SIMULATION_ENGINE_V1 - P1: SECTOR_EXPOSURE_GRAPH_V1/LEADER_LIFECYCLE_GATE_V1, EXECUTION_CAPACITY_LADDER_V1, MODEL_GOVERNANCE_KILL_SWITCH_V1 - P2: SCENARIO_SHOCK_MATRIX_V1, TRANSITION_SET_ENUMERATOR_V1, IMMUTABLE_DECISION_LEDGER_V1, EXECUTION_PLAN_COMPILER_V1 - P3: STATE_VECTOR_CONSTRUCTOR_V1, WALK_FORWARD_BOOTSTRAP_V1, TRANSITION_SET_ENUMERATOR_V1(MRC/CVaR 확장), REBALANCE_CADENCE_GATE_V1, WEEKLY_LEGACY_TRANSFER_PLAN_V1 기존 regime/cluster 연동 정책 수치(현금방어선, 반도체 cap)는 그대로 유지하고 신규 cap 필드만 추가. spec/09_decision_flow.yaml과 runtime/active_artifact_manifest.yaml에 전 엔진 배선 완료. governance/todo/v8_9_p{0,1,2,3}_adoption_plan.yaml에 각 단계 작업 추적 기록. 검증: validate_specs/validate_golden_coverage_100(100%)/validate_calibration_registry_v1/ validate_schema_model_generation_v1/validate_agents_shrink_v1 전부 PASS. golden test 53/53 PASS. Co-Authored-By: Claude Sonnet 4.6 --- RetirementAssetPortfolio.yaml | 4 + governance/todo/v8_9_p0_adoption_plan.yaml | 166 +++ governance/todo/v8_9_p1_adoption_plan.yaml | 110 ++ governance/todo/v8_9_p2_adoption_plan.yaml | 55 + governance/todo/v8_9_p3_adoption_plan.yaml | 60 + runtime/active_artifact_manifest.yaml | 48 + .../execution_capacity_ladder_v1.schema.json | 46 + .../execution_plan_compiler_v1.schema.json | 16 + .../immutable_decision_ledger_v1.schema.json | 16 + ...odel_governance_kill_switch_v1.schema.json | 51 + ...tfolio_transition_optimizer_v1.schema.json | 48 + .../rebalance_cadence_gate_v1.schema.json | 16 + .../scenario_shock_matrix_v1.schema.json | 16 + .../sector_exposure_graph_v1.schema.json | 49 + .../state_vector_constructor_v1.schema.json | 16 + .../transition_set_enumerator_v1.schema.json | 16 + .../walk_forward_bootstrap_v1.schema.json | 16 + ...weekly_legacy_transfer_plan_v1.schema.json | 16 + spec/09_decision_flow.yaml | 120 +- spec/12_field_dictionary.yaml | 496 +++++++ spec/13_formula_registry.yaml | 460 +++++++ spec/formula_golden_cases_v4.yaml | 248 ++++ spec/formulas/domains/cash.yaml | 114 ++ spec/formulas/domains/execution.yaml | 144 ++ spec/formulas/domains/governance.yaml | 163 +++ spec/formulas/domains/portfolio.yaml | 261 ++++ spec/formulas/domains/sector.yaml | 137 ++ spec/formulas/domains/simulation.yaml | 205 +++ spec/risk/portfolio_exposure.yaml | 24 + .../execution_capacity_ladder_v1_schema.py | 33 + ...tion_capacity_ladder_v1_schema.schema.json | 46 + .../execution_plan_compiler_v1_schema.py | 33 + ...cution_plan_compiler_v1_schema.schema.json | 16 + .../immutable_decision_ledger_v1_schema.py | 33 + ...able_decision_ledger_v1_schema.schema.json | 16 + .../model_governance_kill_switch_v1_schema.py | 33 + ...vernance_kill_switch_v1_schema.schema.json | 51 + ...ortfolio_transition_optimizer_v1_schema.py | 33 + ...transition_optimizer_v1_schema.schema.json | 48 + .../rebalance_cadence_gate_v1_schema.py | 33 + ...balance_cadence_gate_v1_schema.schema.json | 16 + .../scenario_shock_matrix_v1_schema.py | 33 + ...cenario_shock_matrix_v1_schema.schema.json | 16 + .../sector_exposure_graph_v1_schema.py | 33 + ...ector_exposure_graph_v1_schema.schema.json | 49 + .../state_vector_constructor_v1_schema.py | 33 + ...e_vector_constructor_v1_schema.schema.json | 16 + .../transition_set_enumerator_v1_schema.py | 33 + ...ition_set_enumerator_v1_schema.schema.json | 16 + .../walk_forward_bootstrap_v1_schema.py | 33 + ...lk_forward_bootstrap_v1_schema.schema.json | 16 + .../weekly_legacy_transfer_plan_v1_schema.py | 33 + ...legacy_transfer_plan_v1_schema.schema.json | 16 + ...tfolio_optimizer_canonical_refactored.yaml | 1162 +++++++++++++++++ .../execution_capacity_ladder_v1_golden.py | 61 + .../execution_plan_compiler_v1_golden.py | 52 + .../forecast_simulation_engine_v1_golden.py | 62 + .../immutable_decision_ledger_v1_golden.py | 60 + .../model_governance_kill_switch_v1_golden.py | 56 + ...ortfolio_transition_optimizer_v1_golden.py | 69 + .../rebalance_cadence_gate_v1_golden.py | 57 + .../scenario_shock_matrix_v1_golden.py | 47 + .../sector_exposure_graph_v1_golden.py | 64 + .../sell_waterfall_engine_v4_golden.py | 53 + .../state_vector_constructor_v1_golden.py | 46 + .../transition_set_enumerator_v1_golden.py | 63 + .../walk_forward_bootstrap_v1_golden.py | 60 + .../weekly_legacy_transfer_plan_v1_golden.py | 41 + tools/build_execution_capacity_ladder_v1.py | 84 ++ tools/build_execution_plan_compiler_v1.py | 108 ++ tools/build_forecast_simulation_engine_v1.py | 155 +++ tools/build_immutable_decision_ledger_v1.py | 109 ++ .../build_model_governance_kill_switch_v1.py | 118 ++ ...build_portfolio_transition_optimizer_v1.py | 209 +++ tools/build_rebalance_cadence_gate_v1.py | 90 ++ tools/build_scenario_shock_matrix_v1.py | 112 ++ tools/build_sector_exposure_graph_v1.py | 149 +++ tools/build_sell_waterfall_engine_v4.py | 154 +++ tools/build_state_vector_constructor_v1.py | 91 ++ tools/build_transition_set_enumerator_v1.py | 153 +++ tools/build_walk_forward_bootstrap_v1.py | 124 ++ tools/build_weekly_legacy_transfer_plan_v1.py | 56 + 82 files changed, 7515 insertions(+), 5 deletions(-) create mode 100644 governance/todo/v8_9_p0_adoption_plan.yaml create mode 100644 governance/todo/v8_9_p1_adoption_plan.yaml create mode 100644 governance/todo/v8_9_p2_adoption_plan.yaml create mode 100644 governance/todo/v8_9_p3_adoption_plan.yaml create mode 100644 schemas/generated/execution_capacity_ladder_v1.schema.json create mode 100644 schemas/generated/execution_plan_compiler_v1.schema.json create mode 100644 schemas/generated/immutable_decision_ledger_v1.schema.json create mode 100644 schemas/generated/model_governance_kill_switch_v1.schema.json create mode 100644 schemas/generated/portfolio_transition_optimizer_v1.schema.json create mode 100644 schemas/generated/rebalance_cadence_gate_v1.schema.json create mode 100644 schemas/generated/scenario_shock_matrix_v1.schema.json create mode 100644 schemas/generated/sector_exposure_graph_v1.schema.json create mode 100644 schemas/generated/state_vector_constructor_v1.schema.json create mode 100644 schemas/generated/transition_set_enumerator_v1.schema.json create mode 100644 schemas/generated/walk_forward_bootstrap_v1.schema.json create mode 100644 schemas/generated/weekly_legacy_transfer_plan_v1.schema.json create mode 100644 spec/formulas/domains/execution.yaml create mode 100644 spec/formulas/domains/governance.yaml create mode 100644 spec/formulas/domains/sector.yaml create mode 100644 spec/formulas/domains/simulation.yaml create mode 100644 src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.py create mode 100644 src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/execution_plan_compiler_v1_schema.py create mode 100644 src/quant_engine/models/generated/execution_plan_compiler_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.py create mode 100644 src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.py create mode 100644 src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.py create mode 100644 src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.py create mode 100644 src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.py create mode 100644 src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/sector_exposure_graph_v1_schema.py create mode 100644 src/quant_engine/models/generated/sector_exposure_graph_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/state_vector_constructor_v1_schema.py create mode 100644 src/quant_engine/models/generated/state_vector_constructor_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/transition_set_enumerator_v1_schema.py create mode 100644 src/quant_engine/models/generated/transition_set_enumerator_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.py create mode 100644 src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.schema.json create mode 100644 src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.py create mode 100644 src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.schema.json create mode 100644 suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml create mode 100644 tests/golden/generated/execution_capacity_ladder_v1_golden.py create mode 100644 tests/golden/generated/execution_plan_compiler_v1_golden.py create mode 100644 tests/golden/generated/forecast_simulation_engine_v1_golden.py create mode 100644 tests/golden/generated/immutable_decision_ledger_v1_golden.py create mode 100644 tests/golden/generated/model_governance_kill_switch_v1_golden.py create mode 100644 tests/golden/generated/portfolio_transition_optimizer_v1_golden.py create mode 100644 tests/golden/generated/rebalance_cadence_gate_v1_golden.py create mode 100644 tests/golden/generated/scenario_shock_matrix_v1_golden.py create mode 100644 tests/golden/generated/sector_exposure_graph_v1_golden.py create mode 100644 tests/golden/generated/sell_waterfall_engine_v4_golden.py create mode 100644 tests/golden/generated/state_vector_constructor_v1_golden.py create mode 100644 tests/golden/generated/transition_set_enumerator_v1_golden.py create mode 100644 tests/golden/generated/walk_forward_bootstrap_v1_golden.py create mode 100644 tests/golden/generated/weekly_legacy_transfer_plan_v1_golden.py create mode 100644 tools/build_execution_capacity_ladder_v1.py create mode 100644 tools/build_execution_plan_compiler_v1.py create mode 100644 tools/build_forecast_simulation_engine_v1.py create mode 100644 tools/build_immutable_decision_ledger_v1.py create mode 100644 tools/build_model_governance_kill_switch_v1.py create mode 100644 tools/build_portfolio_transition_optimizer_v1.py create mode 100644 tools/build_rebalance_cadence_gate_v1.py create mode 100644 tools/build_scenario_shock_matrix_v1.py create mode 100644 tools/build_sector_exposure_graph_v1.py create mode 100644 tools/build_sell_waterfall_engine_v4.py create mode 100644 tools/build_state_vector_constructor_v1.py create mode 100644 tools/build_transition_set_enumerator_v1.py create mode 100644 tools/build_walk_forward_bootstrap_v1.py create mode 100644 tools/build_weekly_legacy_transfer_plan_v1.py diff --git a/RetirementAssetPortfolio.yaml b/RetirementAssetPortfolio.yaml index 981ce50..c7e1161 100644 --- a/RetirementAssetPortfolio.yaml +++ b/RetirementAssetPortfolio.yaml @@ -193,6 +193,10 @@ spec_files: formula_domain_fundamental: "spec/formulas/domains/fundamental.yaml" formula_domain_smart_money: "spec/formulas/domains/smart_money.yaml" formula_domain_macro: "spec/formulas/domains/macro.yaml" + formula_domain_simulation: "spec/formulas/domains/simulation.yaml" + formula_domain_sector: "spec/formulas/domains/sector.yaml" + formula_domain_execution: "spec/formulas/domains/execution.yaml" + formula_domain_governance: "spec/formulas/domains/governance.yaml" harness_formula_registry: "spec/13b_harness_formulas.yaml" raw_workbook_mapping: "spec/14_raw_workbook_mapping.yaml" account_snapshot_contract: "spec/15_account_snapshot_contract.yaml" diff --git a/governance/todo/v8_9_p0_adoption_plan.yaml b/governance/todo/v8_9_p0_adoption_plan.yaml new file mode 100644 index 0000000..21efa4b --- /dev/null +++ b/governance/todo/v8_9_p0_adoption_plan.yaml @@ -0,0 +1,166 @@ +schema_version: v8_9_p0_adoption_plan.v1 +meta: + title: v8_9_p0_adoption_plan + source_proposal: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml + decision_basis: > + 정책 수치는 기존 regime/cluster 연동 수치를 유지하고 v8.9 신규 구조만 추가한다. + 작업 범위는 운영에 즉시 필요한 3개 구멍(portfolio_transition_optimizer, + sell lot Pareto selector, CE70/CE90/CVaR95 simulation engine)만 우선 차단한다. + (사용자 확인: 2026-06-17 plan-mode 대화) + superseded_or_deferred_from_proposal: + - sector_graph_engine_v8_9 (factor residualization, leader lifecycle, ETF lookthrough) + - execution_plan_compiler_v8_9 (broker_microstructure_packet capacity 강화) + - model_governance_v8_9 (kill switch 자동화 전체 세트) + note: 이 항목들은 별도 후속 제안으로 분리한다. 이 TODO 파일의 범위 밖이다. + not_adopted_policy_values: + reason: 기존 spec/risk/portfolio_exposure.yaml의 regime/cluster_state 연동 수치가 v8.9의 고정값보다 더 정교함. + examples: + - "cash_floor: v8.9 GROWTH=12.5% 고정 → 기존 regime 7~25% 단계 유지" + - "semiconductor_cap: v8.9 35% 고정 → 기존 cluster_state 연동 25/35/60% 유지" + adopted_new_fields_only: + - single_stock_hard_cap_pct: 20 + - single_stock_soft_cap_pct: 15 + - top3_hard_cap_pct: 65 + - top3_soft_cap_pct: 50 + +tasks: + - id: P0-1.1 + title: 신규 cap 필드 추가 (교체 아님, 추가) + output_file: spec/risk/portfolio_exposure.yaml + detail: > + single_stock_hard_cap_pct=20/soft=15, top3_hard_cap_pct=65/soft=50 신규 필드 추가. + 기존 regime cash_floor·cluster_state 반도체 cap은 불변. + depends_on: [] + acceptance_criteria: + - 신규 필드가 portfolio_exposure_framework 하위에 추가됨 + - 기존 cash_floor.regime_numbers, cluster_states 수치 변경 없음 (git diff로 확인) + + - id: P0-1.2 + title: formula registry에 PORTFOLIO_TRANSITION_UTILITY_V1 등록 + output_file: + - spec/formulas/domains/portfolio.yaml + - spec/13_formula_registry.yaml + detail: > + state_vector, candidate_action_schema, hard_veto_order, transition_utility 공식, + acceptance_margin, NO_TRADE/solver_failure/rank_tie/conflicting_packets fallback을 + 명시. spec/13_formula_registry.yaml의 python_harness_supplements.formulas에 + PORTFOLIO_TRANSITION_UTILITY_V1 ID 추가. + depends_on: [P0-1.1] + acceptance_criteria: + - PORTFOLIO_TRANSITION_UTILITY_V1 공식이 spec/formulas/domains/portfolio.yaml에 존재 + - 공식 ID가 spec/13_formula_registry.yaml에 등록됨 + - default_action: NO_TRADE 명시 + + - id: P0-1.3 + title: portfolio_transition_optimizer 빌드 도구 작성 + output_file: tools/build_portfolio_transition_optimizer_v1.py + inputs: + - Temp/final_decision_packet_active.json + - Temp/sell_waterfall_engine_v3.json + - Temp/smart_cash_recovery_v9.json + detail: > + 기존 tools/build_sell_waterfall_engine_v3.py 패턴(_load, argparse --base/--out)을 따른다. + candidate_actions를 입력 아티팩트에서 구성하고 hard_constraint_pass 평가 후 + transition_utility_krw, acceptance_margin_krw를 계산해 selected_transition 또는 + NO_TRADE를 결정론적으로 출력한다. + depends_on: [P0-1.2] + acceptance_criteria: + - 입력 아티팩트가 없으면 NO_TRADE_AND_QUARANTINE 반환 (추정값 생성 금지) + - 출력에 formula_id, gate, reason_codes, source_paths 포함 + + - id: P0-1.4 + title: schema + generated model + output_file: + - schemas/portfolio_transition_optimizer_v1.schema.json + - src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.py + depends_on: [P0-1.3] + acceptance_criteria: + - schemas/generated와 src/quant_engine/models/generated parity 유지 (AGENTS.md §5) + + - id: P0-1.5 + title: golden case 작성 + output_file: tests/golden/generated/portfolio_transition_optimizer_v1_golden.py + detail: v8.9 제안서 V89_002(NO_TRADE default), V89_048(solver_failure), V89_049(rank_tie), V89_050(conflicting_packets) 대응 케이스. + depends_on: [P0-1.4] + acceptance_criteria: + - 4개 이상의 실질 assertion이 있는 테스트 (auto-generated stub 패턴 탈피) + + - id: P0-1.6 + title: runtime manifest·decision flow 배선 + output_file: + - runtime/active_artifact_manifest.yaml + - spec/09_decision_flow.yaml + depends_on: [P0-1.5] + acceptance_criteria: + - manifest_rows 또는 source_precedence에 portfolio_transition_optimizer_v1 등록 + + - id: P0-2.1 + title: SELL_LOT_PARETO_SELECTOR_V1 공식 정의 + output_file: spec/formulas/domains/cash.yaml + detail: > + 기존 SELL_WATERFALL_ENGINE_V1 옆에 신규 섹션 추가. + tax_loss_benefit, missed_upside_penalty, reentry_cost 항을 LOT_SELL_SCORE에 추가하고 + 동순위 후보 간 Pareto 비교(avoided_tail_loss vs missed_upside 등 다목적) 규칙 명시. + depends_on: [] + acceptance_criteria: + - LOT_SELL_SCORE 공식에 3개 신규 항 모두 포함 + - Pareto 비교 규칙(어떤 후보가 다른 후보를 dominate하는지)이 결정론적으로 기술됨 + + - id: P0-2.2 + title: build_sell_waterfall_engine_v4.py + output_file: tools/build_sell_waterfall_engine_v4.py + detail: tools/build_sell_waterfall_engine_v3.py를 확장. v3 출력을 입력으로 받아 Pareto 선택 단계를 추가. + depends_on: [P0-2.1] + acceptance_criteria: + - v3 출력과 하위호환 (기존 rows 구조 보존, 신규 필드만 추가) + + - id: P0-2.3 + title: sell waterfall v4 golden case + output_file: tests/golden/generated/sell_waterfall_engine_v4_golden.py + detail: V89_029(deconcentration_trim), V89_030(profit_lock), V89_031(tax_drag_too_high) 대응. + depends_on: [P0-2.2] + acceptance_criteria: + - 3개 케이스 모두 실질 assertion 포함 + + - id: P0-3.1 + title: CE70/CE90/CVaR95 공식 계약 정의 + output_file: spec/formulas/domains/simulation.yaml + detail: > + spec/29_backtest_harness_contract.yaml의 insufficient_data 상태를 입력으로 받아 + sample_count_total/same_regime이 v8.9 minimum_sample_rules(SHADOW 30/10, PILOT 80/20, + LIVE_LIMITED 150/30, LIVE_FULL 300/50) 미달이면 WATCH_ONLY 또는 DATA_MISSING 반환. + 표본이 충분할 때만 walk_forward_bootstrap 실행 경로를 명시. 가짜 분포 생성 금지. + depends_on: [] + acceptance_criteria: + - minimum_sample_rules 표본 기준이 AGENTS.md §6b(T+20 30건)와 일치 + - 표본 부족 시 반환값이 DATA_MISSING 또는 WATCH_ONLY로만 표시됨 (추정 금지) + + - id: P0-3.2 + title: build_forecast_simulation_engine_v1.py + output_file: tools/build_forecast_simulation_engine_v1.py + inputs: + - Temp/prediction_accuracy_harness_v2.json + - spec/29_backtest_harness_contract.yaml + depends_on: [P0-3.1] + acceptance_criteria: + - 현재 T+20 표본 0건 상태에서 실행 시 CE70/CE90/CVaR95가 DATA_MISSING으로 출력됨 + + - id: P0-3.3 + title: forecast simulation golden case + output_file: tests/golden/generated/forecast_simulation_engine_v1_golden.py + detail: V89_013(missing_CVaR → QUARANTINE), V89_014(same_regime_sample_low → WATCH_ONLY) 대응. + depends_on: [P0-3.2] + acceptance_criteria: + - 2개 케이스 모두 실질 assertion 포함 + + - id: P0-4.1 + title: 전체 검증 스위트 실행 + command: | + python tools/validate_specs.py + python tools/validate_golden_coverage_100.py + python tools/validate_calibration_registry_v1.py + python tools/validate_schema_model_generation_v1.py + python tools/validate_agents_shrink_v1.py + depends_on: [P0-1.6, P0-2.3, P0-3.3] + acceptance_criteria: + - 5개 validator 모두 PASS diff --git a/governance/todo/v8_9_p1_adoption_plan.yaml b/governance/todo/v8_9_p1_adoption_plan.yaml new file mode 100644 index 0000000..efd838c --- /dev/null +++ b/governance/todo/v8_9_p1_adoption_plan.yaml @@ -0,0 +1,110 @@ +schema_version: v8_9_p1_adoption_plan.v1 +meta: + title: v8_9_p1_adoption_plan + source_proposal: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml + predecessor: governance/todo/v8_9_p0_adoption_plan.yaml + decision_basis: > + P0(portfolio_transition_optimizer, sell lot Pareto, CE70/CE90/CVaR95 simulation) 완료 후 + 사용자가 "로드맵에 따라 제안한 모든 작업을 순차적으로 진행"하도록 지시(2026-06-17). + 탐색 결과(에이전트 보고) 3개 항목 모두 spec 레벨 구현이 없음을 확인 — 신규 구현. + scope: + - sector_graph_engine_v8_9 (canonical sector ID, ETF lookthrough, factor residualization, leader lifecycle) + - execution_plan_compiler_v8_9.broker_microstructure_packet (order_capacity formula) + - model_governance_v8_9 (kill switches, promotion_ladder 자동화) + +tasks: + - id: P1-A.1 + title: SECTOR_EXPOSURE_GRAPH_V1 공식 정의 + output_file: spec/formulas/domains/sector.yaml (신규) + detail: > + canonical_sector_id 포맷(L1:L2:L3:L4), ETF lookthrough_weight_pct 산식, + factor_beta_residualization(AI/반도체/전력 중복 베타 제거) 정의. + depends_on: [] + + - id: P1-A.2 + title: LEADER_LIFECYCLE_GATE_V1 공식 정의 + output_file: spec/formulas/domains/sector.yaml + detail: CAPTAIN/CORE_LEADER/ENABLER/CYCLICAL_BETA/LAGGARD/DISTRIBUTION_RISK promotion/demotion 결정론적 상태머신. + depends_on: [P1-A.1] + + - id: P1-A.3 + title: build_sector_exposure_graph_v1.py + output_file: tools/build_sector_exposure_graph_v1.py + depends_on: [P1-A.2] + + - id: P1-A.4 + title: schema + generated model (sector) + output_file: + - schemas/generated/sector_exposure_graph_v1.schema.json + - src/quant_engine/models/generated/sector_exposure_graph_v1_schema.py + depends_on: [P1-A.3] + + - id: P1-A.5 + title: sector golden case + detail: V89_044(sector_overlap), V89_045(ETF_direct_overlap), V89_046(leader_distribution) + output_file: tests/golden/generated/sector_exposure_graph_v1_golden.py + depends_on: [P1-A.4] + + - id: P1-B.1 + title: EXECUTION_CAPACITY_LADDER_V1 공식 정의 + output_file: spec/formulas/domains/execution.yaml (신규) + detail: > + order_capacity_krw = min(planned_order_amount_krw, avg_trade_value_20d_krw*0.003, + intraday_trade_value_krw*0.01, orderbook_top3_depth_krw*0.30). + spread_bps, orderbook_top3_depth_krw 신규 필드. + depends_on: [] + + - id: P1-B.2 + title: build_execution_capacity_ladder_v1.py + output_file: tools/build_execution_capacity_ladder_v1.py + depends_on: [P1-B.1] + + - id: P1-B.3 + title: schema + generated model (execution) + output_file: + - schemas/generated/execution_capacity_ladder_v1.schema.json + - src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.py + depends_on: [P1-B.2] + + - id: P1-B.4 + title: execution capacity golden case + detail: V89_019(broker_packet_missing), V89_020(capacity_too_low), V89_022(spread_widens) + output_file: tests/golden/generated/execution_capacity_ladder_v1_golden.py + depends_on: [P1-B.3] + + - id: P1-C.1 + title: MODEL_GOVERNANCE_KILL_SWITCH_V1 공식 정의 + output_file: spec/formulas/domains/governance.yaml (신규) + detail: > + data_quarantine_rate_above_5pct, implementation_shortfall_above_2x_expected, + T5_hit_rate_below_50pct_30trades, calibration_error_above_limit, drawdown_breach + kill switch 조건 + promotion_ladder(AUDIT_ONLY→SHADOW→PILOT→LIVE_LIMITED→LIVE_FULL) 자동화. + depends_on: [] + + - id: P1-C.2 + title: build_model_governance_kill_switch_v1.py + output_file: tools/build_model_governance_kill_switch_v1.py + depends_on: [P1-C.1] + + - id: P1-C.3 + title: schema + generated model (governance) + output_file: + - schemas/generated/model_governance_kill_switch_v1.schema.json + - src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.py + depends_on: [P1-C.2] + + - id: P1-C.4 + title: kill switch golden case + detail: V89_035(hit_rate), V89_036(slippage), V89_037(quarantine_rate) + output_file: tests/golden/generated/model_governance_kill_switch_v1_golden.py + depends_on: [P1-C.3] + + - id: P1-4 + title: 전체 검증 스위트 재실행 + command: | + python tools/validate_specs.py + python tools/validate_golden_coverage_100.py + python tools/validate_calibration_registry_v1.py + python tools/validate_schema_model_generation_v1.py + python tools/validate_agents_shrink_v1.py + depends_on: [P1-A.5, P1-B.4, P1-C.4] diff --git a/governance/todo/v8_9_p2_adoption_plan.yaml b/governance/todo/v8_9_p2_adoption_plan.yaml new file mode 100644 index 0000000..8077288 --- /dev/null +++ b/governance/todo/v8_9_p2_adoption_plan.yaml @@ -0,0 +1,55 @@ +schema_version: v8_9_p2_adoption_plan.v1 +meta: + title: v8_9_p2_adoption_plan + source_proposal: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml + predecessor: + - governance/todo/v8_9_p0_adoption_plan.yaml + - governance/todo/v8_9_p1_adoption_plan.yaml + decision_basis: > + P0+P1 완료(spec/tool/schema/golden case/decision_flow/manifest 배선까지) 후 사용자가 + "로드맵에 따라서 제안한 모든 작업을 순차적으로 진행"을 재요청(2026-06-17). + 원본 제안서의 implementation_todo_v8_9 중 P0/P1에서 다루지 않은 4개 세부 항목을 P2로 채택. + scope: + - forecast_and_simulation_engine_v8_9.simulation_grid (scenario shock matrix) + - portfolio_transition_optimizer_v8_9.selection_algorithm (transition-set 조합 열거) + - model_governance_v8_9.immutable_decision_log_required_fields + - execution_plan_compiler_v8_9 (LIMIT_SPLIT 전체 컴파일) + +tasks: + - id: P2-A + title: SCENARIO_SHOCK_MATRIX_V1 + output_file: spec/formulas/domains/simulation.yaml + detail: > + base/adverse/liquidity_drought/crisis/fx_shock/tax_cost 6개 시나리오로 net_profit_distribution을 + 스트레스 적용. 표본 부족 시 DATA_MISSING(가짜 분포 생성 금지). + implementation: tools/build_scenario_shock_matrix_v1.py + + - id: P2-B + title: TRANSITION_SET_ENUMERATOR_V1 + output_file: spec/formulas/domains/portfolio.yaml + detail: > + 개별 candidate가 아니라 candidate 조합(transition set) 단위로 hard_constraint_pass, + post_trade_cash_floor_pct, transition_utility_krw를 평가하고 최고 utility set을 선택. + solver_failure/rank_tie/conflicting_packets fallback은 PORTFOLIO_TRANSITION_UTILITY_V1과 동일. + implementation: tools/build_transition_set_enumerator_v1.py + + - id: P2-C + title: IMMUTABLE_DECISION_LEDGER_V1 + output_file: spec/formulas/domains/governance.yaml + detail: > + decision_id, timestamp, input_hash_bundle, selected_transition_id, T1/T5/T20_return, + MAE, MFE를 append-only로 기록. 기존 레코드 수정 금지(append-only 검증). + implementation: tools/build_immutable_decision_ledger_v1.py + + - id: P2-D + title: EXECUTION_PLAN_COMPILER_V1 + output_file: spec/formulas/domains/execution.yaml + detail: > + EXECUTION_CAPACITY_LADDER_V1 산출물을 LIMIT_SPLIT 30/30/40 슬라이스로 컴파일하고, + 슬라이스 실행 전 cash_floor/capacity/spread 재검증, cancel_remaining_if 조건 평가. + implementation: tools/build_execution_plan_compiler_v1.py + + - id: P2-E + title: schema/model + decision_flow/manifest 배선 + 전체 검증 + detail: 4개 신규 공식의 schemas/generated + src/quant_engine/models/generated 생성, spec/09_decision_flow.yaml 및 runtime/active_artifact_manifest.yaml 배선, 5개 validator 재실행. + depends_on: [P2-A, P2-B, P2-C, P2-D] diff --git a/governance/todo/v8_9_p3_adoption_plan.yaml b/governance/todo/v8_9_p3_adoption_plan.yaml new file mode 100644 index 0000000..4bfda6b --- /dev/null +++ b/governance/todo/v8_9_p3_adoption_plan.yaml @@ -0,0 +1,60 @@ +schema_version: v8_9_p3_adoption_plan.v1 +meta: + title: v8_9_p3_adoption_plan + source_proposal: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml + predecessor: + - governance/todo/v8_9_p0_adoption_plan.yaml + - governance/todo/v8_9_p1_adoption_plan.yaml + - governance/todo/v8_9_p2_adoption_plan.yaml + decision_basis: > + P0+P1+P2 완료 후 사용자가 "로드맵에 따라서 제안한 모든 작업을 순차적으로 진행"을 3회 재요청(2026-06-17). + 원본 제안서의 implementation_todo_v8_9 중 P1_optimizer_and_simulation, P3_sell_and_rebalance에서 + 아직 다루지 않은 5개 항목을 P3로 채택. 이번이 로드맵의 마지막 라운드다 — 이후 implementation_todo_v8_9 + 전체가 소진된다. + scope: + - P1_optimizer_and_simulation: state_vector constructor, walk-forward bootstrap, post-trade MRC/CVaR per set + - P3_sell_and_rebalance: mandatory cadence gate, legacy-to-CMA transfer plan as planning input + +tasks: + - id: P3-A + title: STATE_VECTOR_CONSTRUCTOR_V1 + output_file: spec/formulas/domains/portfolio.yaml + detail: holdings, cash, tax_lots, sector_graph, factor_exposures, macro_regime_probabilities를 단일 state_vector로 통합. + implementation: tools/build_state_vector_constructor_v1.py + + - id: P3-B + title: WALK_FORWARD_BOOTSTRAP_V1 + output_file: spec/formulas/domains/simulation.yaml + detail: > + historical_returns 표본에서 walk-forward(시간순 비복원 윈도우) 및 regime-matched(동일 레짐 + 필터) 리샘플링으로 net_profit_distribution_after_tax_fee_slippage를 생성. + FORECAST_SIMULATION_ENGINE_V1의 입력을 채우는 상류 엔진. 표본 부족 시 DATA_MISSING. + implementation: tools/build_walk_forward_bootstrap_v1.py + + - id: P3-C + title: TRANSITION_SET_ENUMERATOR_V1 확장 — set 단위 MRC/CVaR/concentration/cash_floor + output_file: spec/formulas/domains/portfolio.yaml + detail: 기존 cash_floor/concentration delta 합산만 하던 것을 post_trade_MRC, post_trade_CVaR95까지 포함하도록 확장. + implementation: tools/build_transition_set_enumerator_v1.py (확장) + + - id: P3-D + title: REBALANCE_CADENCE_GATE_V1 + output_file: spec/formulas/domains/portfolio.yaml + detail: > + 주간(토/일) 및 매월 1/11/21일 점검을 의무 실행하되, transition_utility_after_tax_cost가 + 양수이거나 hard_risk_block이 active일 때만 실제 리밸런싱을 허용. 그 외에는 점검 결과만 + emit하고 NO_TRADE. + implementation: tools/build_rebalance_cadence_gate_v1.py + + - id: P3-E + title: WEEKLY_LEGACY_TRANSFER_PLAN_V1 + output_file: spec/formulas/domains/cash.yaml + detail: > + 주간 레거시종목→CMA 이전 계획(weekly_legacy_to_cma_transfer_plan_krw)을 입금 확인 전까지 + deployable_cash_krw에 합산하지 않는다. 계획 단계와 확정 단계를 분리. + implementation: tools/build_weekly_legacy_transfer_plan_v1.py + + - id: P3-F + title: schema/model + decision_flow/manifest 배선 + 전체 검증 + detail: 5개 신규/확장 공식의 schemas/generated + src/quant_engine/models/generated 생성, spec/09_decision_flow.yaml 및 runtime/active_artifact_manifest.yaml 배선, 5개 validator 재실행. + depends_on: [P3-A, P3-B, P3-C, P3-D, P3-E] diff --git a/runtime/active_artifact_manifest.yaml b/runtime/active_artifact_manifest.yaml index 36d8431..2d32188 100644 --- a/runtime/active_artifact_manifest.yaml +++ b/runtime/active_artifact_manifest.yaml @@ -15,6 +15,18 @@ active_aliases: final_decision_packet_active: Temp/final_decision_packet_active.json source_precedence: - final_decision_packet_active +- model_governance_kill_switch_v1 +- state_vector_constructor_v1 +- weekly_legacy_transfer_plan_v1 +- portfolio_transition_optimizer_v1 +- walk_forward_bootstrap_v1 +- transition_set_enumerator_v1 +- rebalance_cadence_gate_v1 +- scenario_shock_matrix_v1 +- sector_exposure_graph_v1 +- execution_capacity_ladder_v1 +- execution_plan_compiler_v1 +- immutable_decision_ledger_v1 - final_execution_decision_v2 - smart_cash_recovery_v7 - smart_cash_recovery_v6 @@ -42,3 +54,39 @@ manifest_rows: - formula_id: smart_cash_recovery_v7 active_artifact: Temp/smart_cash_recovery_v9.json value: 57841575 +- formula_id: portfolio_transition_optimizer_v1 + active_artifact: Temp/portfolio_transition_optimizer_v1.json + value: NO_TRADE +- formula_id: model_governance_kill_switch_v1 + active_artifact: Temp/model_governance_kill_switch_v1.json + value: AUDIT_ONLY +- formula_id: sector_exposure_graph_v1 + active_artifact: Temp/sector_exposure_graph_v1.json + value: DATA_MISSING +- formula_id: execution_capacity_ladder_v1 + active_artifact: Temp/execution_capacity_ladder_v1.json + value: DATA_MISSING +- formula_id: transition_set_enumerator_v1 + active_artifact: Temp/transition_set_enumerator_v1.json + value: NO_TRADE +- formula_id: scenario_shock_matrix_v1 + active_artifact: Temp/scenario_shock_matrix_v1.json + value: DATA_MISSING +- formula_id: execution_plan_compiler_v1 + active_artifact: Temp/execution_plan_compiler_v1.json + value: DATA_MISSING +- formula_id: immutable_decision_ledger_v1 + active_artifact: Temp/immutable_decision_ledger_v1.json + value: APPENDED +- formula_id: state_vector_constructor_v1 + active_artifact: Temp/state_vector_constructor_v1.json + value: 0.0 +- formula_id: walk_forward_bootstrap_v1 + active_artifact: Temp/walk_forward_bootstrap_v1.json + value: DATA_MISSING +- formula_id: rebalance_cadence_gate_v1 + active_artifact: Temp/rebalance_cadence_gate_v1.json + value: false +- formula_id: weekly_legacy_transfer_plan_v1 + active_artifact: Temp/weekly_legacy_transfer_plan_v1.json + value: 0.0 diff --git a/schemas/generated/execution_capacity_ladder_v1.schema.json b/schemas/generated/execution_capacity_ladder_v1.schema.json new file mode 100644 index 0000000..89da6b3 --- /dev/null +++ b/schemas/generated/execution_capacity_ladder_v1.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/EXECUTION_CAPACITY_LADDER_V1", + "title": "EXECUTION_CAPACITY_LADDER_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "EXECUTION_CAPACITY_LADDER_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "planned_order_amount_krw", + "avg_trade_value_20d_krw", + "intraday_trade_value_krw", + "orderbook_top3_depth_krw", + "spread_bps" + ], + "x_formula_outputs": [ + "order_capacity_krw" + ] +} diff --git a/schemas/generated/execution_plan_compiler_v1.schema.json b/schemas/generated/execution_plan_compiler_v1.schema.json new file mode 100644 index 0000000..ea7f93e --- /dev/null +++ b/schemas/generated/execution_plan_compiler_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/EXECUTION_PLAN_COMPILER_V1", + "title": "EXECUTION_PLAN_COMPILER_V1", + "type": "object", + "properties": { + "formula_id": { "const": "EXECUTION_PLAN_COMPILER_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["order_capacity_krw", "revalidation_snapshot", "baseline_snapshot"], + "x_formula_outputs": ["compiled_slices"] +} diff --git a/schemas/generated/immutable_decision_ledger_v1.schema.json b/schemas/generated/immutable_decision_ledger_v1.schema.json new file mode 100644 index 0000000..c11c702 --- /dev/null +++ b/schemas/generated/immutable_decision_ledger_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/IMMUTABLE_DECISION_LEDGER_V1", + "title": "IMMUTABLE_DECISION_LEDGER_V1", + "type": "object", + "properties": { + "formula_id": { "const": "IMMUTABLE_DECISION_LEDGER_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["decision_id", "input_hash_bundle", "execution_mode", "candidate_ids", "selected_transition_id"], + "x_formula_outputs": ["ledger_append_status"] +} diff --git a/schemas/generated/model_governance_kill_switch_v1.schema.json b/schemas/generated/model_governance_kill_switch_v1.schema.json new file mode 100644 index 0000000..1f09a09 --- /dev/null +++ b/schemas/generated/model_governance_kill_switch_v1.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/MODEL_GOVERNANCE_KILL_SWITCH_V1", + "title": "MODEL_GOVERNANCE_KILL_SWITCH_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "MODEL_GOVERNANCE_KILL_SWITCH_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "data_quarantine_rate_pct", + "implementation_shortfall_ratio", + "t5_hit_rate_pct", + "t5_sample_count", + "calibration_error", + "calibration_error_limit", + "account_mdd_pct", + "account_mdd_budget_pct" + ], + "x_formula_outputs": [ + "execution_mode", + "kill_switch_triggered", + "kill_switch_reason_codes" + ] +} diff --git a/schemas/generated/portfolio_transition_optimizer_v1.schema.json b/schemas/generated/portfolio_transition_optimizer_v1.schema.json new file mode 100644 index 0000000..9fbea37 --- /dev/null +++ b/schemas/generated/portfolio_transition_optimizer_v1.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/PORTFOLIO_TRANSITION_UTILITY_V1", + "title": "PORTFOLIO_TRANSITION_UTILITY_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "PORTFOLIO_TRANSITION_UTILITY_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "ce70_net_profit_krw", + "tax_fee_slippage_krw", + "cash_repair_benefit_krw", + "concentration_reduction_benefit_krw", + "turnover_penalty_krw" + ], + "x_formula_outputs": [ + "transition_utility_krw", + "acceptance_margin_krw", + "selected_transition" + ] +} diff --git a/schemas/generated/rebalance_cadence_gate_v1.schema.json b/schemas/generated/rebalance_cadence_gate_v1.schema.json new file mode 100644 index 0000000..a99f469 --- /dev/null +++ b/schemas/generated/rebalance_cadence_gate_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/REBALANCE_CADENCE_GATE_V1", + "title": "REBALANCE_CADENCE_GATE_V1", + "type": "object", + "properties": { + "formula_id": { "const": "REBALANCE_CADENCE_GATE_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["today_date", "transition_utility_after_tax_cost_krw", "hard_risk_block_active"], + "x_formula_outputs": ["rebalance_execution_allowed", "cadence_check_required", "review_emitted"] +} diff --git a/schemas/generated/scenario_shock_matrix_v1.schema.json b/schemas/generated/scenario_shock_matrix_v1.schema.json new file mode 100644 index 0000000..32c45d7 --- /dev/null +++ b/schemas/generated/scenario_shock_matrix_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/SCENARIO_SHOCK_MATRIX_V1", + "title": "SCENARIO_SHOCK_MATRIX_V1", + "type": "object", + "properties": { + "formula_id": { "const": "SCENARIO_SHOCK_MATRIX_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["net_profit_distribution_after_tax_fee_slippage", "scenario_id"], + "x_formula_outputs": ["scenario_results"] +} diff --git a/schemas/generated/sector_exposure_graph_v1.schema.json b/schemas/generated/sector_exposure_graph_v1.schema.json new file mode 100644 index 0000000..83fb22f --- /dev/null +++ b/schemas/generated/sector_exposure_graph_v1.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/SECTOR_EXPOSURE_GRAPH_V1", + "title": "SECTOR_EXPOSURE_GRAPH_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "SECTOR_EXPOSURE_GRAPH_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "direct_weight_pct", + "etf_constituents_json", + "etf_weight_pct", + "sector_id", + "peer_sector_betas" + ], + "x_formula_outputs": [ + "sector_family_total_pct", + "lookthrough_etf_weight_pct", + "factor_beta_residualized", + "leader_role" + ] +} diff --git a/schemas/generated/state_vector_constructor_v1.schema.json b/schemas/generated/state_vector_constructor_v1.schema.json new file mode 100644 index 0000000..5fccbd4 --- /dev/null +++ b/schemas/generated/state_vector_constructor_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/STATE_VECTOR_CONSTRUCTOR_V1", + "title": "STATE_VECTOR_CONSTRUCTOR_V1", + "type": "object", + "properties": { + "formula_id": { "const": "STATE_VECTOR_CONSTRUCTOR_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["cash_ladder", "positions", "sector_exposure_graph", "factor_exposures", "tax_lots", "risk_bucket_weights", "macro_regime_probabilities", "goal_progress_pct"], + "x_formula_outputs": ["state_vector", "state_vector_completeness_pct", "missing_components"] +} diff --git a/schemas/generated/transition_set_enumerator_v1.schema.json b/schemas/generated/transition_set_enumerator_v1.schema.json new file mode 100644 index 0000000..8f232e8 --- /dev/null +++ b/schemas/generated/transition_set_enumerator_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/TRANSITION_SET_ENUMERATOR_V1", + "title": "TRANSITION_SET_ENUMERATOR_V1", + "type": "object", + "properties": { + "formula_id": { "const": "TRANSITION_SET_ENUMERATOR_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["evaluated_candidates", "max_set_size"], + "x_formula_outputs": ["selected_transition_set"] +} diff --git a/schemas/generated/walk_forward_bootstrap_v1.schema.json b/schemas/generated/walk_forward_bootstrap_v1.schema.json new file mode 100644 index 0000000..2c7b55e --- /dev/null +++ b/schemas/generated/walk_forward_bootstrap_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/WALK_FORWARD_BOOTSTRAP_V1", + "title": "WALK_FORWARD_BOOTSTRAP_V1", + "type": "object", + "properties": { + "formula_id": { "const": "WALK_FORWARD_BOOTSTRAP_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["historical_returns", "current_regime_state", "bootstrap_method"], + "x_formula_outputs": ["net_profit_distribution_after_tax_fee_slippage", "sample_count_total", "sample_count_same_regime"] +} diff --git a/schemas/generated/weekly_legacy_transfer_plan_v1.schema.json b/schemas/generated/weekly_legacy_transfer_plan_v1.schema.json new file mode 100644 index 0000000..1ad9cc9 --- /dev/null +++ b/schemas/generated/weekly_legacy_transfer_plan_v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/WEEKLY_LEGACY_TRANSFER_PLAN_V1", + "title": "WEEKLY_LEGACY_TRANSFER_PLAN_V1", + "type": "object", + "properties": { + "formula_id": { "const": "WEEKLY_LEGACY_TRANSFER_PLAN_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["weekly_legacy_to_cma_transfer_plan_krw", "transfer_confirmed", "transfer_confirmed_amount_krw"], + "x_formula_outputs": ["deployable_cash_contribution_krw", "plan_status"] +} diff --git a/spec/09_decision_flow.yaml b/spec/09_decision_flow.yaml index 7abc927..8244242 100644 --- a/spec/09_decision_flow.yaml +++ b/spec/09_decision_flow.yaml @@ -9,7 +9,7 @@ meta: 각 상태는 통과 조건, 실패 시 행동, 참조 파일을 가진다. decision_flow: - initial_state: "INPUT_VALIDATION" + initial_state: "MODEL_GOVERNANCE_GATE" terminal_states: ["FINAL_DECISION", "INSUFFICIENT_DATA", "BLOCKED"] deterministic_execution_control: purpose: "텍스트 해석 차이로 매번 다른 결론이 나오는 것을 줄이기 위한 결정 추적·동률 처리·first-match 규칙." @@ -37,6 +37,18 @@ decision_flow: null_propagation_rule: "필수 입력이 null이면 해당 계산은 null로 유지하고 prohibited_calculations에 사유를 남긴다. null을 0으로 대체 금지." output_requirement: "OUTPUT_VALIDATION에서 decision_trace 누락 시 schema_validation_status=FAIL." states: + MODEL_GOVERNANCE_GATE: # [governance/todo/v8_9_p1_adoption_plan.yaml P1-5] + purpose: > + 매 의사결정 사이클 시작 전 kill switch(data_quarantine_rate, implementation_shortfall, + T5_hit_rate, calibration_error, drawdown)를 평가해 execution_mode를 확정한다. + 이후 모든 단계는 이 execution_mode를 기준으로 동작한다. + required_refs: + - "spec/formulas/domains/governance.yaml:MODEL_GOVERNANCE_KILL_SWITCH_V1" + - "tools/build_model_governance_kill_switch_v1.py" + required_inputs: ["data_quarantine_rate_pct", "implementation_shortfall_ratio", "t5_hit_rate_pct", "t5_sample_count", "calibration_error", "account_mdd_pct"] + computed_outputs: ["execution_mode", "kill_switch_triggered", "kill_switch_reason_codes"] + pass_condition: "execution_mode 확정(kill switch 평가 완료 또는 DATA_MISSING)" + fail_state: "INSUFFICIENT_DATA" INPUT_VALIDATION: purpose: "요청, 기준일, 계좌, 보유수량, 가격/수급/ATR 입력 존재 여부 확인" required_refs: @@ -57,6 +69,20 @@ decision_flow: computed_outputs: ["data_completeness_matrix", "missing_fields", "field_unit_conflicts"] pass_condition: "data_completeness_matrix produced" fail_state: "INSUFFICIENT_DATA" + STATE_VECTOR_CONSTRUCTION: # [governance/todo/v8_9_p3_adoption_plan.yaml P3-F] + purpose: > + holdings, cash, tax_lots, sector_graph, factor_exposures, macro_regime_probabilities를 + 단일 state_vector로 통합한다. cash_ladder 구성 시 WEEKLY_LEGACY_TRANSFER_PLAN_V1을 + 통해 미확정 레거시 이전계획을 deployable_cash에서 제외한다. + required_refs: + - "spec/formulas/domains/portfolio.yaml:STATE_VECTOR_CONSTRUCTOR_V1" + - "spec/formulas/domains/cash.yaml:WEEKLY_LEGACY_TRANSFER_PLAN_V1" + - "tools/build_state_vector_constructor_v1.py" + - "tools/build_weekly_legacy_transfer_plan_v1.py" + required_inputs: ["data_completeness_matrix", "account_snapshot"] + computed_outputs: ["state_vector", "state_vector_completeness_pct", "missing_components", "deployable_cash_contribution_krw"] + pass_condition: "state_vector 산출(결측 component 포함 가능)" + fail_state: "INSUFFICIENT_DATA" HARD_FILTER_CHECK: purpose: "하드 필터를 점수보다 먼저 적용" required_refs: @@ -130,6 +156,18 @@ decision_flow: - "trim_assignments 없이 현금 부족 해소 주장 금지" - "QEH_AUDIT_BLOCK.SELL_PRIORITY_V1 행에 배정 결과 요약 필수" - "1순위 소진 전 2순위 배정 금지 (sell_priority_engine 순서 준수)" + SECTOR_EXPOSURE_REVIEW: # [governance/todo/v8_9_p1_adoption_plan.yaml P1-5] + purpose: > + 섹터를 canonical ID로 분류하고 ETF lookthrough·factor beta residualization을 적용해 + 실질노출을 산출하며, 리더 생명주기(CAPTAIN~DISTRIBUTION_RISK)를 평가한다. + required_refs: + - "spec/formulas/domains/sector.yaml:SECTOR_EXPOSURE_GRAPH_V1" + - "spec/formulas/domains/sector.yaml:LEADER_LIFECYCLE_GATE_V1" + - "tools/build_sector_exposure_graph_v1.py" + required_inputs: ["exposure_limit_amounts", "current_exposures", "etf_constituents_json"] + computed_outputs: ["sector_family_total_pct", "lookthrough_etf_weight_pct", "leader_role"] + pass_condition: "섹터 노출 산출 또는 ETF_BUY_BLOCKED/DATA_MISSING 명시" + fail_state: "INSUFFICIENT_DATA" POSITION_SIZING: purpose: "ATR20·현금·목표비중·유동성으로 정수 수량 산출" required_refs: @@ -175,6 +213,44 @@ decision_flow: - "CRITICAL_ALERT 시 코어 포지션 포함 전면 재검토 강제" - "LLM이 레이더 결과를 완화하는 서사 출력 금지 (Section B 해설만 허용)" - "RADAR_MISSING(데이터 부족) 시 soft-block: 보유 포지션 수동 점검 권고 문구 출력" + PORTFOLIO_TRANSITION_REVIEW: # [governance/todo/v8_9_p0_adoption_plan.yaml P0-1.6, v8_9_p2_adoption_plan.yaml P2-E] + purpose: > + 개별 종목 단위 결정을 포트폴리오 전체 전환 효용으로 재평가한다. + EXIT_POLICY_CHECK에서 나온 sell 후보들을 단일 전환 패킷으로 비교해 + selected_transition_set 또는 NO_TRADE를 결정론적으로 확정한다. CE70/CVaR95 분포는 + WALK_FORWARD_BOOTSTRAP_V1으로 생성하고 SCENARIO_SHOCK_MATRIX_V1의 5개 스트레스 + 시나리오를 반영하며, candidate 1건이 아니라 TRANSITION_SET_ENUMERATOR_V1으로 조합 단위 + hard_constraint(MRC/CVaR95 포함)를 재검증한다. 실제 실행은 REBALANCE_CADENCE_GATE_V1의 + rebalance_execution_allowed=true일 때만 허용된다. + required_refs: + - "spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1" + - "spec/formulas/domains/portfolio.yaml:TRANSITION_SET_ENUMERATOR_V1" + - "spec/formulas/domains/portfolio.yaml:REBALANCE_CADENCE_GATE_V1" + - "spec/formulas/domains/simulation.yaml:SCENARIO_SHOCK_MATRIX_V1" + - "spec/formulas/domains/simulation.yaml:WALK_FORWARD_BOOTSTRAP_V1" + - "tools/build_portfolio_transition_optimizer_v1.py" + - "tools/build_transition_set_enumerator_v1.py" + - "tools/build_scenario_shock_matrix_v1.py" + - "tools/build_walk_forward_bootstrap_v1.py" + - "tools/build_rebalance_cadence_gate_v1.py" + required_inputs: ["stop_order", "take_profit_order", "sell_waterfall_rows", "cash_repair_benefit_krw", "rebalance_execution_allowed"] + computed_outputs: ["transition_utility_krw", "acceptance_margin_krw", "selected_transition", "portfolio_transition_final_action", "selected_transition_set", "scenario_results", "post_trade_mrc", "post_trade_cvar95_krw"] + pass_condition: "selected_transition_set 결정(rebalance_execution_allowed=true 필요) 또는 default_action=NO_TRADE 명시" + fail_state: "INSUFFICIENT_DATA" + EXECUTION_CAPACITY_CHECK: # [governance/todo/v8_9_p1_adoption_plan.yaml P1-5, v8_9_p2_adoption_plan.yaml P2-E] + purpose: > + selected_transition_set의 주문금액이 실제 체결 가능 용량(20일 평균거래대금, 당일 거래대금, + 호가창 깊이)을 초과하지 않는지 확인하고, broker_microstructure_packet 결측 시 차단한다. + 용량이 확인되면 EXECUTION_PLAN_COMPILER_V1으로 30/30/40 LIMIT_SPLIT 슬라이스를 컴파일한다. + required_refs: + - "spec/formulas/domains/execution.yaml:EXECUTION_CAPACITY_LADDER_V1" + - "spec/formulas/domains/execution.yaml:EXECUTION_PLAN_COMPILER_V1" + - "tools/build_execution_capacity_ladder_v1.py" + - "tools/build_execution_plan_compiler_v1.py" + required_inputs: ["selected_transition_set", "avg_trade_value_20d_krw", "intraday_trade_value_krw", "orderbook_top3_depth_krw", "spread_bps"] + computed_outputs: ["order_capacity_krw", "execution_plan_status", "compiled_slices"] + pass_condition: "order_capacity_krw 산출 또는 EXECUTION_PLAN_BLOCKED 명시" + fail_state: "INSUFFICIENT_DATA" OUTPUT_VALIDATION: purpose: "JSON Schema와 HTS 표 필드 검증" required_refs: @@ -188,25 +264,38 @@ decision_flow: purpose: "BUY/HOLD/SELL/TRIM/ROTATE/AVOID/WATCH/INSUFFICIENT_DATA 중 하나로 결론" required_refs: - "spec/07_output_schema.yaml:recommendation_grade" + - "spec/formulas/domains/governance.yaml:IMMUTABLE_DECISION_LEDGER_V1" output_required: - "final_action" - "grade" - "orders or prohibited_calculations" - "rules_used" + - "ledger_append_status" + post_state_action: "tools/build_immutable_decision_ledger_v1.py 호출 — decision_id 재기록 시도는 DUPLICATE_DECISION_ID로 거부." INSUFFICIENT_DATA: purpose: "데이터 부족으로 산출 불가. 다음 확인 출처를 제시." output_required: - "missing_fields" - "prohibited_calculations" - "next_source_to_check" + - "ledger_append_status" + post_state_action: "INSUFFICIENT_DATA로 종료해도 IMMUTABLE_DECISION_LEDGER_V1에 기록한다(결정 없음도 기록 대상)." BLOCKED: purpose: "하드 필터 또는 리스크 정책으로 행동 차단." output_required: - "triggered_rules" - "blocked_action" - "allowed_alternative" + - "ledger_append_status" + post_state_action: "BLOCKED로 종료해도 IMMUTABLE_DECISION_LEDGER_V1에 기록한다." transitions: + - from: "MODEL_GOVERNANCE_GATE" + to: "INPUT_VALIDATION" + condition: "execution_mode 확정(kill switch 평가 완료 또는 DATA_MISSING)" + - from: "MODEL_GOVERNANCE_GATE" + to: "INSUFFICIENT_DATA" + condition: "kill switch 평가에 필요한 모든 지표가 결측" - from: "INPUT_VALIDATION" to: "DATA_COMPLETENESS_CHECK" condition: "minimum request context exists" @@ -214,11 +303,14 @@ transitions: to: "INSUFFICIENT_DATA" condition: "account/request context missing and cannot be inferred" - from: "DATA_COMPLETENESS_CHECK" - to: "HARD_FILTER_CHECK" + to: "STATE_VECTOR_CONSTRUCTION" condition: "data_completeness_matrix produced" - from: "DATA_COMPLETENESS_CHECK" to: "INSUFFICIENT_DATA" condition: "matrix cannot be produced" + - from: "STATE_VECTOR_CONSTRUCTION" + to: "HARD_FILTER_CHECK" + condition: "state_vector 산출(결측 component 포함 가능)" - from: "HARD_FILTER_CHECK" to: "BLOCKED" condition: "blocking hard_filter failed" @@ -235,17 +327,35 @@ transitions: to: "BLOCKED" condition: "cash_floor, duplicate exposure, account limit, or Total_Heat blocks requested action" - from: "PORTFOLIO_CONSTRAINT_CHECK" - to: "POSITION_SIZING" - condition: "requested action requires quantity" + to: "SECTOR_EXPOSURE_REVIEW" + condition: "requested action requires quantity or affects sector exposure" - from: "PORTFOLIO_CONSTRAINT_CHECK" to: "EXIT_POLICY_CHECK" condition: "requested action is hold/trim/sell review" + - from: "SECTOR_EXPOSURE_REVIEW" + to: "POSITION_SIZING" + condition: "섹터 노출 산출 또는 ETF_BUY_BLOCKED/DATA_MISSING 명시" + - from: "SECTOR_EXPOSURE_REVIEW" + to: "INSUFFICIENT_DATA" + condition: "sector exposure 계산에 필요한 입력 누락" - from: "POSITION_SIZING" to: "EXIT_POLICY_CHECK" condition: "quantity calculated or NO_QUANTITY reason emitted" - from: "EXIT_POLICY_CHECK" - to: "OUTPUT_VALIDATION" + to: "PORTFOLIO_TRANSITION_REVIEW" condition: "order/hold/watch decision prepared" + - from: "PORTFOLIO_TRANSITION_REVIEW" + to: "EXECUTION_CAPACITY_CHECK" + condition: "selected_transition 결정 또는 NO_TRADE 명시" + - from: "PORTFOLIO_TRANSITION_REVIEW" + to: "INSUFFICIENT_DATA" + condition: "transition utility 계산에 필요한 입력 누락" + - from: "EXECUTION_CAPACITY_CHECK" + to: "OUTPUT_VALIDATION" + condition: "order_capacity_krw 산출 또는 EXECUTION_PLAN_BLOCKED 명시 (NO_TRADE인 경우 즉시 통과)" + - from: "EXECUTION_CAPACITY_CHECK" + to: "INSUFFICIENT_DATA" + condition: "broker_microstructure_packet 필드 누락" - from: "OUTPUT_VALIDATION" to: "FINAL_DECISION" condition: "schema and required output fields valid" diff --git a/spec/12_field_dictionary.yaml b/spec/12_field_dictionary.yaml index 852b60b..fb908bb 100644 --- a/spec/12_field_dictionary.yaml +++ b/spec/12_field_dictionary.yaml @@ -2189,6 +2189,502 @@ field_dictionary: aliases: ["Base_Qty", "base_sell_qty"] note: "SELL_QUANTITY_ALLOCATOR_V1 산출 기준 매도 수량. CASH_PRESERVATION_SELL_ENGINE_V2 입력." + # ── [2026-06-17_P0_v8_9_ADOPTION] PORTFOLIO_TRANSITION_UTILITY_V1 신규 필드 ── + ce70_net_profit_krw: + canonical_name: "ce70_net_profit_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["CE70_NET_PROFIT_KRW", "ce70_profit"] + note: "forecast_simulation_engine_v1 산출 — 세후비용 차감 손익분포의 30%분위(CE70). 표본 부족 시 null(DATA_MISSING)." + tax_fee_slippage_krw: + canonical_name: "tax_fee_slippage_krw" + type: "number" + unit: "KRW" + aliases: ["TAX_FEE_SLIPPAGE_KRW", "tax_fee_slippage"] + note: "sell_waterfall_engine_v4 산출 — 세금·수수료·슬리피지 합산 비용." + cash_repair_benefit_krw: + canonical_name: "cash_repair_benefit_krw" + type: "number" + unit: "KRW" + aliases: ["CASH_REPAIR_BENEFIT_KRW"] + note: "smart_cash_recovery_v9 연동 — 현금방어선 회복으로 인한 효용 가치." + concentration_reduction_benefit_krw: + canonical_name: "concentration_reduction_benefit_krw" + type: "number" + unit: "KRW" + aliases: ["CONCENTRATION_REDUCTION_BENEFIT_KRW"] + note: "portfolio_exposure.concentration_caps_v8_9_supplement 초과 해소로 인한 효용 가치." + turnover_penalty_krw: + canonical_name: "turnover_penalty_krw" + type: "number" + unit: "KRW" + aliases: ["TURNOVER_PENALTY_KRW"] + note: "회전율 예산 초과분에 대한 페널티. rebalancing_engine_v8_9.turnover_budget 참조." + transition_utility_krw: + canonical_name: "transition_utility_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["TRANSITION_UTILITY_KRW"] + note: "PORTFOLIO_TRANSITION_UTILITY_V1 산출 — 양수일 때만 전환 후보 채택 검토. 입력 결측 시 null(NO_TRADE_AND_QUARANTINE)." + avoided_tail_loss_krw: + canonical_name: "avoided_tail_loss_krw" + type: "number" + unit: "KRW" + aliases: ["AVOIDED_TAIL_LOSS_KRW"] + note: "SELL_LOT_PARETO_SELECTOR_V1 입력 — 해당 lot을 매도하지 않았을 때 예상되는 꼬리위험 손실 회피액." + tax_loss_benefit_krw: + canonical_name: "tax_loss_benefit_krw" + type: "number" + unit: "KRW" + aliases: ["TAX_LOSS_BENEFIT_KRW"] + note: "SELL_LOT_PARETO_SELECTOR_V1 입력 — 손실 lot 매도 시 세금 절감 효과. 계좌유형 미확인 시 0(DATA_MISSING 표기)." + reentry_cost_krw: + canonical_name: "reentry_cost_krw" + type: "number" + unit: "KRW" + aliases: ["REENTRY_COST_KRW"] + note: "SELL_LOT_PARETO_SELECTOR_V1 입력 — 매도 후 재진입 시 예상 거래비용·스프레드." + missed_upside_penalty_krw: + canonical_name: "missed_upside_penalty_krw" + type: "number" + unit: "KRW" + aliases: ["MISSED_UPSIDE_PENALTY_KRW"] + note: "SELL_LOT_PARETO_SELECTOR_V1 입력 — CE70_NET_PROFIT_KRW 분포 기반 추정 상승분. 분포 없으면 0(보수적 하한)." + lot_sell_score_krw: + canonical_name: "lot_sell_score_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["LOT_SELL_SCORE_KRW"] + note: "SELL_LOT_PARETO_SELECTOR_V1 산출 — 동일 hard_precedence 단계 내 lot 우선순위 점수." + ce90_net_profit_krw: + canonical_name: "ce90_net_profit_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["CE90_NET_PROFIT_KRW"] + note: "FORECAST_SIMULATION_ENGINE_V1 산출 — 손익분포 10%분위(CE90). 표본 부족 시 null(WATCH_ONLY)." + cvar95_loss_krw: + canonical_name: "cvar95_loss_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["CVAR95_LOSS_KRW"] + note: "FORECAST_SIMULATION_ENGINE_V1 산출 — 95% 신뢰구간 꼬리손실 평균. 표본 부족 시 null(WATCH_ONLY)." + sample_count_total: + canonical_name: "sample_count_total" + type: "integer" + unit: "count" + aliases: ["SAMPLE_COUNT_TOTAL"] + note: "FORECAST_SIMULATION_ENGINE_V1 입력 — 전체 손익 표본 수. spec/29_backtest_harness_contract.yaml 연동." + sample_count_same_regime: + canonical_name: "sample_count_same_regime" + type: "integer" + unit: "count" + aliases: ["SAMPLE_COUNT_SAME_REGIME"] + note: "FORECAST_SIMULATION_ENGINE_V1 입력 — 동일 레짐 손익 표본 수." + net_profit_distribution_after_tax_fee_slippage: + canonical_name: "net_profit_distribution_after_tax_fee_slippage" + type: "list_or_null" + unit: "list_of_KRW" + aliases: ["NET_PROFIT_DISTRIBUTION"] + note: "FORECAST_SIMULATION_ENGINE_V1 입력 — 세후·비용 차감 손익 표본 분포. spec/29_backtest_harness_contract.yaml 연동." + execution_mode: + canonical_name: "execution_mode" + type: "string" + unit: "none" + aliases: ["EXECUTION_MODE", "global_execution_gate"] + note: "AUDIT_ONLY | SHADOW | PILOT | LIVE_LIMITED | LIVE_FULL. PORTFOLIO_TRANSITION_UTILITY_V1·FORECAST_SIMULATION_ENGINE_V1 입력." + + # ── [2026-06-17_P1_v8_9_ADOPTION] SECTOR_EXPOSURE_GRAPH_V1 / LEADER_LIFECYCLE_GATE_V1 ── + direct_weight_pct: + canonical_name: "direct_weight_pct" + type: "number" + unit: "percent" + aliases: ["DIRECT_WEIGHT_PCT"] + note: "SECTOR_EXPOSURE_GRAPH_V1 입력 — 종목 직접보유 비중." + etf_constituents_json: + canonical_name: "etf_constituents_json" + type: "list_or_null" + unit: "json" + aliases: ["ETF_CONSTITUENTS_JSON"] + note: "SECTOR_EXPOSURE_GRAPH_V1 입력 — ETF 구성종목 [{ticker, weight_pct, sector_id}]. 미확인 시 ETF_BUY_BLOCKED." + etf_weight_pct: + canonical_name: "etf_weight_pct" + type: "number" + unit: "percent" + aliases: ["ETF_WEIGHT_PCT"] + note: "SECTOR_EXPOSURE_GRAPH_V1 입력 — 포트폴리오 내 ETF 비중." + sector_id: + canonical_name: "sector_id" + type: "string" + unit: "none" + aliases: ["SECTOR_ID"] + note: "canonical_sector_id_format(L1:L2:L3:L4) 준수. 예: EQ:TECH:SEMIS:HBM." + peer_sector_betas: + canonical_name: "peer_sector_betas" + type: "list_or_null" + unit: "list_of_ratio" + aliases: ["PEER_SECTOR_BETAS"] + note: "SECTOR_EXPOSURE_GRAPH_V1 입력 — 동일 macro_driver 공유 섹터 베타 목록. 미확인 시 raw beta 사용 + PARTIAL 표기." + sector_family_total_pct: + canonical_name: "sector_family_total_pct" + type: "number_or_null" + unit: "percent" + aliases: ["SECTOR_FAMILY_TOTAL_PCT"] + note: "SECTOR_EXPOSURE_GRAPH_V1 산출 — direct_weight_pct + lookthrough_etf_weight_pct." + relative_strength_leads_sector: + canonical_name: "relative_strength_leads_sector" + type: "boolean" + unit: "none" + aliases: ["RELATIVE_STRENGTH_LEADS_SECTOR"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — promotion_requires_all 항목." + volume_quality_confirmed: + canonical_name: "volume_quality_confirmed" + type: "boolean" + unit: "none" + aliases: ["VOLUME_QUALITY_CONFIRMED"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — promotion_requires_all 항목." + above_ma60_or_reclaim_confirmed: + canonical_name: "above_ma60_or_reclaim_confirmed" + type: "boolean" + unit: "none" + aliases: ["ABOVE_MA60_OR_RECLAIM_CONFIRMED"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — promotion_requires_all 항목, demotion_triggers_any 항목." + earnings_revision_status: + canonical_name: "earnings_revision_status" + type: "string" + unit: "none" + aliases: ["EARNINGS_REVISION_STATUS"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — positive | neutral | negative." + institutional_flow_status: + canonical_name: "institutional_flow_status" + type: "string" + unit: "none" + aliases: ["INSTITUTIONAL_FLOW_STATUS"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — accumulation | neutral | distribution." + current_role: + canonical_name: "current_role" + type: "string" + unit: "none" + aliases: ["CURRENT_ROLE"] + note: "LEADER_LIFECYCLE_GATE_V1 입력 — 직전 평가 leader_role. 최초 평가 시 LAGGARD." + leader_role: + canonical_name: "leader_role" + type: "string" + unit: "none" + aliases: ["LEADER_ROLE"] + note: "LEADER_LIFECYCLE_GATE_V1 산출 — CAPTAIN | CORE_LEADER | ENABLER | CYCLICAL_BETA | LAGGARD | DISTRIBUTION_RISK." + + # ── [2026-06-17_P1_v8_9_ADOPTION] EXECUTION_CAPACITY_LADDER_V1 신규 필드 ── + planned_order_amount_krw: + canonical_name: "planned_order_amount_krw" + type: "number" + unit: "KRW" + aliases: ["PLANNED_ORDER_AMOUNT_KRW"] + note: "EXECUTION_CAPACITY_LADDER_V1 입력 — 계획된 주문금액." + avg_trade_value_20d_krw: + canonical_name: "avg_trade_value_20d_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["AVG_TRADE_VALUE_20D_KRW", "AvgTradeValue_20D"] + note: "EXECUTION_CAPACITY_LADDER_V1 입력 — 20일 평균거래대금. 미확인 시 EXECUTION_PLAN_BLOCKED." + intraday_trade_value_krw: + canonical_name: "intraday_trade_value_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["INTRADAY_TRADE_VALUE_KRW"] + note: "EXECUTION_CAPACITY_LADDER_V1 입력 — 당일 누적 거래대금." + orderbook_top3_depth_krw: + canonical_name: "orderbook_top3_depth_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["ORDERBOOK_TOP3_DEPTH_KRW"] + note: "EXECUTION_CAPACITY_LADDER_V1 입력 — 호가창 상위 3단계 누적 깊이." + spread_bps: + canonical_name: "spread_bps" + type: "number_or_null" + unit: "basis_points" + aliases: ["SPREAD_BPS"] + note: "EXECUTION_CAPACITY_LADDER_V1 입력 — 매수/매도 호가 스프레드. spread_widen_cancel_rule 연동." + order_capacity_krw: + canonical_name: "order_capacity_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["ORDER_CAPACITY_KRW"] + note: "EXECUTION_CAPACITY_LADDER_V1 산출 — 체결 가능 용량 상한. 결측 입력 시 null(EXECUTION_PLAN_BLOCKED)." + + # ── [2026-06-17_P1_v8_9_ADOPTION] MODEL_GOVERNANCE_KILL_SWITCH_V1 신규 필드 ── + data_quarantine_rate_pct: + canonical_name: "data_quarantine_rate_pct" + type: "number_or_null" + unit: "percent" + aliases: ["DATA_QUARANTINE_RATE_PCT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — 결측/충돌로 quarantine된 입력 비율. >5%면 kill switch." + implementation_shortfall_ratio: + canonical_name: "implementation_shortfall_ratio" + type: "number_or_null" + unit: "ratio" + aliases: ["IMPLEMENTATION_SHORTFALL_RATIO"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — 실제/기대 슬리피지 비율. >2.0이면 kill switch." + t5_hit_rate_pct: + canonical_name: "t5_hit_rate_pct" + type: "number_or_null" + unit: "percent" + aliases: ["T5_HIT_RATE_PCT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — spec/29_backtest_harness_contract.yaml:t5_op_rate 연동." + t5_sample_count: + canonical_name: "t5_sample_count" + type: "integer" + unit: "count" + aliases: ["T5_SAMPLE_COUNT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — t5_hit_rate_pct 표본 수. 30건 미만이면 hit_rate kill switch 미적용." + calibration_error: + canonical_name: "calibration_error" + type: "number_or_null" + unit: "ratio" + aliases: ["CALIBRATION_ERROR"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — spec/calibration_registry.yaml 연동." + calibration_error_limit: + canonical_name: "calibration_error_limit" + type: "number" + unit: "ratio" + aliases: ["CALIBRATION_ERROR_LIMIT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — calibration_error 허용 상한." + account_mdd_pct: + canonical_name: "account_mdd_pct" + type: "number_or_null" + unit: "percent" + aliases: ["ACCOUNT_MDD_PCT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — 현재 계좌 MDD." + account_mdd_budget_pct: + canonical_name: "account_mdd_budget_pct" + type: "number" + unit: "percent" + aliases: ["ACCOUNT_MDD_BUDGET_PCT"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 입력 — spec/risk/aggregate_risk.yaml MDD 예산." + kill_switch_triggered: + canonical_name: "kill_switch_triggered" + type: "boolean" + unit: "none" + aliases: ["KILL_SWITCH_TRIGGERED"] + note: "MODEL_GOVERNANCE_KILL_SWITCH_V1 산출 — kill_switch_conditions 중 하나 이상 true." + + # ── [2026-06-17_P2_v8_9_ADOPTION] SCENARIO_SHOCK_MATRIX_V1 신규 필드 ── + scenario_id: + canonical_name: "scenario_id" + type: "string" + unit: "none" + aliases: ["SCENARIO_ID"] + note: "SCENARIO_SHOCK_MATRIX_V1 입력 — base_case | adverse_case | liquidity_drought_case | crisis_case | fx_shock_case | tax_cost_case." + scenario_results: + canonical_name: "scenario_results" + type: "list_or_null" + unit: "list_of_object" + aliases: ["SCENARIO_RESULTS"] + note: "SCENARIO_SHOCK_MATRIX_V1 산출 — [{scenario_id, scenario_ce70_krw, scenario_cvar95_krw}]. 분포 결측 시 null." + + # ── [2026-06-17_P2_v8_9_ADOPTION] TRANSITION_SET_ENUMERATOR_V1 신규 필드 ── + evaluated_candidates: + canonical_name: "evaluated_candidates" + type: "list" + unit: "list_of_object" + aliases: ["EVALUATED_CANDIDATES"] + note: "TRANSITION_SET_ENUMERATOR_V1 입력 — PORTFOLIO_TRANSITION_UTILITY_V1.candidate_actions 산출물." + max_set_size: + canonical_name: "max_set_size" + type: "integer" + unit: "count" + aliases: ["MAX_SET_SIZE"] + note: "TRANSITION_SET_ENUMERATOR_V1 입력 — 조합 폭발 방지 상한. 기본값 3." + selected_transition_set: + canonical_name: "selected_transition_set" + type: "list" + unit: "list_of_string" + aliases: ["SELECTED_TRANSITION_SET"] + note: "TRANSITION_SET_ENUMERATOR_V1 산출 — 최종 선택된 candidate_id 조합. 빈 리스트면 NO_TRADE." + + # ── [2026-06-17_P2_v8_9_ADOPTION] IMMUTABLE_DECISION_LEDGER_V1 신규 필드 ── + decision_id: + canonical_name: "decision_id" + type: "string" + unit: "none" + aliases: ["DECISION_ID"] + note: "IMMUTABLE_DECISION_LEDGER_V1 입력 — 동일 ID 재기록 시 DUPLICATE_DECISION_ID." + input_hash_bundle: + canonical_name: "input_hash_bundle" + type: "string" + unit: "none" + aliases: ["INPUT_HASH_BUNDLE"] + note: "IMMUTABLE_DECISION_LEDGER_V1 입력 — 의사결정 시점 입력 데이터 해시 묶음." + candidate_ids: + canonical_name: "candidate_ids" + type: "list" + unit: "list_of_string" + aliases: ["CANDIDATE_IDS"] + note: "IMMUTABLE_DECISION_LEDGER_V1 입력 — 평가 대상이 된 candidate_id 목록." + selected_transition_id: + canonical_name: "selected_transition_id" + type: "string_or_null" + unit: "none" + aliases: ["SELECTED_TRANSITION_ID"] + note: "IMMUTABLE_DECISION_LEDGER_V1 입력 — NO_TRADE면 null." + ledger_append_status: + canonical_name: "ledger_append_status" + type: "string" + unit: "none" + aliases: ["LEDGER_APPEND_STATUS"] + note: "IMMUTABLE_DECISION_LEDGER_V1 산출 — APPENDED | DUPLICATE_DECISION_ID | REJECTED_MISSING_FIELDS." + + # ── [2026-06-17_P2_v8_9_ADOPTION] EXECUTION_PLAN_COMPILER_V1 신규 필드 ── + revalidation_snapshot: + canonical_name: "revalidation_snapshot" + type: "object_or_null" + unit: "json" + aliases: ["REVALIDATION_SNAPSHOT"] + note: "EXECUTION_PLAN_COMPILER_V1 입력 — slice 직전 시점 {cash_floor_pct, deployable_cash_krw, order_capacity_krw, spread_bps}." + baseline_snapshot: + canonical_name: "baseline_snapshot" + type: "object_or_null" + unit: "json" + aliases: ["BASELINE_SNAPSHOT"] + note: "EXECUTION_PLAN_COMPILER_V1 입력 — slice 1 컴파일 시점 스냅샷. cancel_remaining_if 기준값." + compiled_slices: + canonical_name: "compiled_slices" + type: "list_or_null" + unit: "list_of_object" + aliases: ["COMPILED_SLICES"] + note: "EXECUTION_PLAN_COMPILER_V1 산출 — [{slice_index, slice_amount_krw, status}]." + + # ── [2026-06-17_P3_v8_9_ADOPTION] STATE_VECTOR_CONSTRUCTOR_V1 / REBALANCE_CADENCE_GATE_V1 신규 필드 ── + cash_ladder: + canonical_name: "cash_ladder" + type: "object_or_null" + unit: "json" + aliases: ["CASH_LADDER"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/formulas/domains/cash.yaml:CASH_RATIOS_V1 산출." + positions: + canonical_name: "positions" + type: "list_or_null" + unit: "list_of_object" + aliases: ["POSITIONS"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/15_account_snapshot_contract.yaml 보유종목 목록." + sector_exposure_graph: + canonical_name: "sector_exposure_graph" + type: "list_or_null" + unit: "list_of_object" + aliases: ["SECTOR_EXPOSURE_GRAPH"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — SECTOR_EXPOSURE_GRAPH_V1.rows 산출." + goal_progress_pct: + canonical_name: "goal_progress_pct" + type: "number_or_null" + unit: "percent" + aliases: ["GOAL_PROGRESS_PCT"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — total_asset_krw / target_asset_krw * 100." + factor_exposures: + canonical_name: "factor_exposures" + type: "list_or_null" + unit: "list_of_object" + aliases: ["FACTOR_EXPOSURES"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/risk/factor_risk.yaml 연동." + tax_lots: + canonical_name: "tax_lots" + type: "list_or_null" + unit: "list_of_object" + aliases: ["TAX_LOTS"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/15_account_snapshot_contract.yaml 연동." + risk_bucket_weights: + canonical_name: "risk_bucket_weights" + type: "object_or_null" + unit: "json" + aliases: ["RISK_BUCKET_WEIGHTS"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/risk/portfolio_exposure.yaml 연동." + macro_regime_probabilities: + canonical_name: "macro_regime_probabilities" + type: "object_or_null" + unit: "json" + aliases: ["MACRO_REGIME_PROBABILITIES"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 입력 — spec/risk/market_risk_cash.yaml 연동." + state_vector: + canonical_name: "state_vector" + type: "object_or_null" + unit: "json" + aliases: ["STATE_VECTOR"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 산출 — 결측 component는 null + missing_components 기록." + missing_components: + canonical_name: "missing_components" + type: "list" + unit: "list_of_string" + aliases: ["MISSING_COMPONENTS"] + note: "STATE_VECTOR_CONSTRUCTOR_V1 산출 — null로 남은 component 이름 목록." + transition_utility_after_tax_cost_krw: + canonical_name: "transition_utility_after_tax_cost_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["TRANSITION_UTILITY_AFTER_TAX_COST_KRW"] + note: "REBALANCE_CADENCE_GATE_V1 입력 — PORTFOLIO_TRANSITION_UTILITY_V1.transition_utility_krw와 동일 출처." + hard_risk_block_active: + canonical_name: "hard_risk_block_active" + type: "boolean_or_null" + unit: "none" + aliases: ["HARD_RISK_BLOCK_ACTIVE"] + note: "REBALANCE_CADENCE_GATE_V1 입력 — spec/risk/aggregate_risk.yaml 연동." + rebalance_execution_allowed: + canonical_name: "rebalance_execution_allowed" + type: "boolean" + unit: "none" + aliases: ["REBALANCE_EXECUTION_ALLOWED"] + note: "REBALANCE_CADENCE_GATE_V1 산출 — true여야 실제 리밸런싱 실행 가능." + + # ── [2026-06-17_P3_v8_9_ADOPTION] WALK_FORWARD_BOOTSTRAP_V1 신규 필드 ── + historical_returns: + canonical_name: "historical_returns" + type: "list_or_null" + unit: "list_of_object" + aliases: ["HISTORICAL_RETURNS"] + note: "WALK_FORWARD_BOOTSTRAP_V1 입력 — [{date, regime_state, net_return_after_cost_pct}]. spec/29_backtest_harness_contract.yaml 연동." + current_regime_state: + canonical_name: "current_regime_state" + type: "string" + unit: "none" + aliases: ["CURRENT_REGIME_STATE"] + note: "WALK_FORWARD_BOOTSTRAP_V1 입력 — regime_matched 리샘플링 필터 기준." + bootstrap_method: + canonical_name: "bootstrap_method" + type: "string" + unit: "none" + aliases: ["BOOTSTRAP_METHOD"] + note: "WALK_FORWARD_BOOTSTRAP_V1 입력 — walk_forward | regime_matched." + + # ── [2026-06-17_P3_v8_9_ADOPTION] WEEKLY_LEGACY_TRANSFER_PLAN_V1 신규 필드 ── + weekly_legacy_to_cma_transfer_plan_krw: + canonical_name: "weekly_legacy_to_cma_transfer_plan_krw" + type: "number" + unit: "KRW" + aliases: ["WEEKLY_LEGACY_TO_CMA_TRANSFER_PLAN_KRW"] + note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 입력 — spec/risk/portfolio_exposure.yaml operator_cashflow_config 고정 계획값." + transfer_confirmed: + canonical_name: "transfer_confirmed" + type: "boolean_or_null" + unit: "none" + aliases: ["TRANSFER_CONFIRMED"] + note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 입력 — null은 false로 간주(보수적)." + transfer_confirmed_amount_krw: + canonical_name: "transfer_confirmed_amount_krw" + type: "number_or_null" + unit: "KRW" + aliases: ["TRANSFER_CONFIRMED_AMOUNT_KRW"] + note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 입력 — transfer_confirmed=true일 때만 값 존재." + deployable_cash_contribution_krw: + canonical_name: "deployable_cash_contribution_krw" + type: "number" + unit: "KRW" + aliases: ["DEPLOYABLE_CASH_CONTRIBUTION_KRW"] + note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 산출 — 확정 전이면 0, 확정 후 transfer_confirmed_amount_krw." + plan_status: + canonical_name: "plan_status" + type: "string" + unit: "none" + aliases: ["PLAN_STATUS"] + note: "WEEKLY_LEGACY_TRANSFER_PLAN_V1 산출 — PLANNED_NOT_DEPLOYABLE | CONFIRMED_DEPLOYABLE." + normalization_rules: - id: "FIELD_ALIAS_CANONICALIZATION" rule: "모든 입력은 계산 전 canonical_name으로 변환한다." diff --git a/spec/13_formula_registry.yaml b/spec/13_formula_registry.yaml index 061d7ca..a755009 100644 --- a/spec/13_formula_registry.yaml +++ b/spec/13_formula_registry.yaml @@ -97,6 +97,21 @@ formula_registry: - HORIZON_REBALANCE_PLAN_V1 - PIPELINE_RUNTIME_PROFILE_V1 - STRATEGY_ROUTING_AUDIT_V1 + - PORTFOLIO_TRANSITION_UTILITY_V1 + - SELL_LOT_PARETO_SELECTOR_V1 + - FORECAST_SIMULATION_ENGINE_V1 + - SECTOR_EXPOSURE_GRAPH_V1 + - LEADER_LIFECYCLE_GATE_V1 + - EXECUTION_CAPACITY_LADDER_V1 + - MODEL_GOVERNANCE_KILL_SWITCH_V1 + - SCENARIO_SHOCK_MATRIX_V1 + - TRANSITION_SET_ENUMERATOR_V1 + - IMMUTABLE_DECISION_LEDGER_V1 + - EXECUTION_PLAN_COMPILER_V1 + - STATE_VECTOR_CONSTRUCTOR_V1 + - REBALANCE_CADENCE_GATE_V1 + - WALK_FORWARD_BOOTSTRAP_V1 + - WEEKLY_LEGACY_TRANSFER_PLAN_V1 implementation_map: REGIME_CONDITIONAL_MACRO_FACTOR_V1: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1 REBOUND_CAPTURE_THESIS_FACTOR_V1: tools/build_predictive_alpha_dialectic_engine_v2.py:NF2 @@ -121,6 +136,21 @@ formula_registry: HORIZON_REBALANCE_PLAN_V1: tools/build_horizon_rebalance_plan_v1.py PIPELINE_RUNTIME_PROFILE_V1: src/quant_engine/pipeline_runtime_anomaly_lib_v1.py STRATEGY_ROUTING_AUDIT_V1: tools/build_strategy_routing_audit_v1.py + PORTFOLIO_TRANSITION_UTILITY_V1: tools/build_portfolio_transition_optimizer_v1.py + SELL_LOT_PARETO_SELECTOR_V1: tools/build_sell_waterfall_engine_v4.py + FORECAST_SIMULATION_ENGINE_V1: tools/build_forecast_simulation_engine_v1.py + SECTOR_EXPOSURE_GRAPH_V1: tools/build_sector_exposure_graph_v1.py + LEADER_LIFECYCLE_GATE_V1: tools/build_sector_exposure_graph_v1.py + EXECUTION_CAPACITY_LADDER_V1: tools/build_execution_capacity_ladder_v1.py + MODEL_GOVERNANCE_KILL_SWITCH_V1: tools/build_model_governance_kill_switch_v1.py + SCENARIO_SHOCK_MATRIX_V1: tools/build_scenario_shock_matrix_v1.py + TRANSITION_SET_ENUMERATOR_V1: tools/build_transition_set_enumerator_v1.py + IMMUTABLE_DECISION_LEDGER_V1: tools/build_immutable_decision_ledger_v1.py + EXECUTION_PLAN_COMPILER_V1: tools/build_execution_plan_compiler_v1.py + STATE_VECTOR_CONSTRUCTOR_V1: tools/build_state_vector_constructor_v1.py + REBALANCE_CADENCE_GATE_V1: tools/build_rebalance_cadence_gate_v1.py + WALK_FORWARD_BOOTSTRAP_V1: tools/build_walk_forward_bootstrap_v1.py + WEEKLY_LEGACY_TRANSFER_PLAN_V1: tools/build_weekly_legacy_transfer_plan_v1.py formulas: FLOW_CREDIT_V1: purpose: 가격·거래량·5D 수급 품질을 0~1 점수로 계산 @@ -2740,6 +2770,398 @@ formula_registry: - rebound_tp_price가 있으면 HTS 주문표에 '반등 익절가' 컬럼 필수 표기 canonical_ref: AGENTS.md:Direction C1, K2 version: 2026-05-22_3RD_HARNESS + SELL_LOT_PARETO_SELECTOR_V1: + purpose: > + SELL_WATERFALL_ENGINE_V1의 동일 hard_precedence 단계 안에서 후보 lot을 점수화하고, + 세금 회피 효과·재진입 비용·놓친 상승분까지 포함한 다목적(Pareto) 비교로 + 동순위 후보 중 어느 lot을 먼저 매도할지 결정론적으로 선택한다. + (governance/todo/v8_9_p0_adoption_plan.yaml P0-2.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sell_and_cash_repair_optimizer_v8_9) + applicable: SELL_WATERFALL_ENGINE_V1의 동일 stage 내 후보가 2개 이상일 때. + inputs: + - field: avoided_tail_loss_krw + unit: KRW + - field: cash_repair_benefit_krw + unit: KRW + - field: concentration_reduction_benefit_krw + unit: KRW + - field: tax_loss_benefit_krw + unit: KRW + - field: tax_fee_slippage_krw + unit: KRW + - field: reentry_cost_krw + unit: KRW + - field: missed_upside_penalty_krw + unit: KRW + expression: > + LOT_SELL_SCORE_KRW = avoided_tail_loss_krw + cash_repair_benefit_krw + concentration_reduction_benefit_krw + + tax_loss_benefit_krw - tax_fee_slippage_krw - reentry_cost_krw - missed_upside_penalty_krw + output: + field: lot_sell_score_krw + unit: KRW + pareto_dominance_rule: + dominates_if: > + A가 모든 maximize 목표(avoided_tail_loss_krw, cash_repair_benefit_krw, concentration_reduction_benefit_krw, + tax_loss_benefit_krw)에서 B 이상이고 모든 minimize 목표(tax_fee_slippage_krw, reentry_cost_krw, + missed_upside_penalty_krw)에서 B 이하이며, 적어도 한 항목에서 우월하면 A dominates B. + tie_breaker_if_no_dominance: [lot_sell_score_krw 높은 순, tax_fee_slippage_krw 낮은 순, reentry_cost_krw 낮은 순] + missing_policy: missed_upside_penalty_krw·tax_loss_benefit_krw 미확인 시 0(보수적 하한). DATA_MISSING으로 표기. + canonical_ref: spec/risk/portfolio_exposure.yaml:sell_priority_engine.candidate_scoring + implementation: tools/build_sell_waterfall_engine_v4.py + version: 2026-06-17_P0_v8_9_adoption + FORECAST_SIMULATION_ENGINE_V1: + purpose: > + 개별 종목의 점 추정 기대수익률이 아니라 레짐별 손익분포에서 CE70(30%분위)·CE90(10%분위)· + CVaR95(95% 신뢰구간 꼬리손실 평균)를 산출한다. 표본 부족 시 가짜 분포를 만들지 않고 + WATCH_ONLY 또는 DATA_MISSING으로 정직하게 반환한다. + (governance/todo/v8_9_p0_adoption_plan.yaml P0-3.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:forecast_and_simulation_engine_v8_9) + applicable: PORTFOLIO_TRANSITION_UTILITY_V1의 ce70_net_profit_krw 입력 직전. + inputs: + - field: net_profit_distribution_after_tax_fee_slippage + source: spec/29_backtest_harness_contract.yaml:current_metrics + unit: list_of_KRW + - field: sample_count_total + unit: count + - field: sample_count_same_regime + unit: count + - field: execution_mode + unit: enum + minimum_sample_rules: + AUDIT_ONLY: {sample_count_total_min: 0, sample_count_same_regime_min: 0} + SHADOW: {sample_count_total_min: 30, sample_count_same_regime_min: 10} + PILOT: {sample_count_total_min: 80, sample_count_same_regime_min: 20} + LIVE_LIMITED: {sample_count_total_min: 150, sample_count_same_regime_min: 30} + LIVE_FULL: {sample_count_total_min: 300, sample_count_same_regime_min: 50} + agents_md_cross_check: "AGENTS.md §6b: Live T+20 표본 30건 미만이면 active/PASS_100 승격 금지" + expression: + ce70_net_profit_krw: quantile(net_profit_distribution_after_tax_fee_slippage, 0.30) + ce90_net_profit_krw: quantile(net_profit_distribution_after_tax_fee_slippage, 0.10) + cvar95_loss_krw: mean(losses beyond 95th percentile loss threshold in net_profit_distribution_after_tax_fee_slippage) + output: + field: ce70_net_profit_krw + unit: KRW_or_null + missing_policy: 표본이 minimum_sample_rules 미달이면 모든 출력 null + gate=WATCH_ONLY. 0으로 대체 금지. + canonical_ref: spec/29_backtest_harness_contract.yaml:current_metrics.walk_forward + implementation: tools/build_forecast_simulation_engine_v1.py + version: 2026-06-17_P0_v8_9_adoption + SECTOR_EXPOSURE_GRAPH_V1: + purpose: > + 섹터를 L1:L2:L3:L4 canonical ID로 분류하고 ETF 구성종목을 lookthrough하여 직접보유와 + 합산한 실질노출을 계산하며, 테마 간 중복 베타를 residualize한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.1) + canonical_sector_id_format: 'L1:L2:L3:L4, 예: EQ:TECH:SEMIS:HBM' + inputs: + - field: direct_weight_pct + unit: percent + - field: etf_constituents_json + unit: json + - field: etf_weight_pct + unit: percent + - field: sector_id + unit: string + - field: peer_sector_betas + unit: list_of_ratio + expression: + lookthrough_etf_weight_pct: "sum(constituent.weight_pct * etf_weight_pct / 100 for constituent in etf_constituents_json if constituent.sector_id == sector_id)" + sector_family_total_pct: "direct_weight_pct + lookthrough_etf_weight_pct" + output: + field: sector_family_total_pct + unit: percent + missing_policy: etf_constituents_json 미확인 시 ETF_BUY_BLOCKED. lookthrough를 0으로 추정 금지. + canonical_ref: spec/risk/portfolio_exposure.yaml:duplicate_exposure_rule + implementation: tools/build_sector_exposure_graph_v1.py + version: 2026-06-17_P1_v8_9_adoption + LEADER_LIFECYCLE_GATE_V1: + purpose: > + 종목의 시장 주도력을 CAPTAIN/CORE_LEADER/ENABLER/CYCLICAL_BETA/LAGGARD/DISTRIBUTION_RISK로 + 분류하고 승급·강등을 결정론적으로 평가한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.2) + roles: [CAPTAIN, CORE_LEADER, ENABLER, CYCLICAL_BETA, LAGGARD, DISTRIBUTION_RISK] + inputs: + - field: relative_strength_leads_sector + unit: boolean + - field: volume_quality_confirmed + unit: boolean + - field: above_ma60_or_reclaim_confirmed + unit: boolean + - field: earnings_revision_status + unit: 'enum: positive | neutral | negative' + - field: institutional_flow_status + unit: 'enum: accumulation | neutral | distribution' + - field: current_role + unit: enum + output: + field: leader_role + unit: enum + missing_policy: 입력 결측 시 current_role 유지. 임의 승급/강등 금지. + canonical_ref: spec/strategy/leader_scan.yaml + implementation: tools/build_sector_exposure_graph_v1.py + version: 2026-06-17_P1_v8_9_adoption + EXECUTION_CAPACITY_LADDER_V1: + purpose: > + 계획된 주문금액이 종목의 실제 체결 가능 용량을 초과하지 않도록 캡핑하고, + broker_microstructure_packet이 없으면 주문 계획을 차단한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-B.1) + inputs: + - field: planned_order_amount_krw + unit: KRW + - field: avg_trade_value_20d_krw + unit: KRW + - field: intraday_trade_value_krw + unit: KRW + - field: orderbook_top3_depth_krw + unit: KRW + - field: spread_bps + unit: basis_points + expression: > + order_capacity_krw = min(planned_order_amount_krw, avg_trade_value_20d_krw * 0.003, + intraday_trade_value_krw * 0.01, orderbook_top3_depth_krw * 0.30) + output: + field: order_capacity_krw + unit: KRW + missing_policy: broker_microstructure_packet 필드 결측 시 EXECUTION_PLAN_BLOCKED. 추정 금지. + canonical_ref: spec/05_position_sizing.yaml + implementation: tools/build_execution_capacity_ladder_v1.py + version: 2026-06-17_P1_v8_9_adoption + MODEL_GOVERNANCE_KILL_SWITCH_V1: + purpose: > + data_quarantine_rate, implementation_shortfall, T5_hit_rate, calibration_error, + drawdown 5개 지표를 감시해 기준 이탈 시 execution_mode를 자동으로 한 단계 강등한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-C.1) + promotion_ladder: [AUDIT_ONLY, SHADOW, PILOT, LIVE_LIMITED, LIVE_FULL] + inputs: + - field: data_quarantine_rate_pct + unit: percent + - field: implementation_shortfall_ratio + unit: ratio + - field: t5_hit_rate_pct + unit: percent + - field: t5_sample_count + unit: count + - field: calibration_error + unit: ratio + - field: calibration_error_limit + unit: ratio + - field: account_mdd_pct + unit: percent + - field: account_mdd_budget_pct + unit: percent + kill_switch_conditions: + - id: data_quarantine_rate_above_5pct + condition: data_quarantine_rate_pct > 5.0 + - id: implementation_shortfall_above_2x_expected + condition: implementation_shortfall_ratio > 2.0 + - id: t5_hit_rate_below_50pct_for_30_trades + condition: t5_sample_count >= 30 AND t5_hit_rate_pct < 50.0 + - id: calibration_error_above_limit + condition: calibration_error > calibration_error_limit + - id: unexpected_drawdown_breach + condition: account_mdd_pct > account_mdd_budget_pct + output: + field: execution_mode + unit: enum + missing_policy: 입력 결측 시 평가 가능한 지표만으로 판정. 전부 결측이면 execution_mode 변경 없음(DATA_MISSING). + canonical_ref: spec/57_shadow_promotion_scorecard.yaml + implementation: tools/build_model_governance_kill_switch_v1.py + version: 2026-06-17_P1_v8_9_adoption + SCENARIO_SHOCK_MATRIX_V1: + purpose: > + base_case 분포에 adverse/liquidity_drought/crisis/fx_shock/tax_cost 5개 스트레스를 + 결정론적으로 적용해 시나리오별 CE70/CVaR95를 산출한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-A) + scenario_definitions: + base_case: {shock_multiplier: 1.0} + adverse_case: {shock_multiplier: 1.5} + liquidity_drought_case: {shock_multiplier: 1.3, capacity_derate_pct: 40} + crisis_case: {shock_multiplier: 2.0, correlation_to_one: true} + fx_shock_case: {shock_multiplier: 1.2, applies_only_to: foreign_assets} + tax_cost_case: {shock_multiplier: 1.0, additional_cost_pct: 5} + inputs: + - field: net_profit_distribution_after_tax_fee_slippage + unit: list_of_KRW + - field: scenario_id + unit: enum + output: + field: scenario_results + unit: 'list_of_{scenario_id, scenario_ce70_krw, scenario_cvar95_krw}' + missing_policy: 분포 없으면 전체 DATA_MISSING. 시나리오별 임의 분포 생성 금지. + canonical_ref: spec/formulas/domains/simulation.yaml:FORECAST_SIMULATION_ENGINE_V1 + implementation: tools/build_scenario_shock_matrix_v1.py + version: 2026-06-17_P2_v8_9_adoption + TRANSITION_SET_ENUMERATOR_V1: + purpose: > + candidate 1건씩이 아니라 조합(transition_set) 단위로 hard_constraint_pass와 + transition_utility_krw를 재평가해, 개별로는 통과하는 후보들의 조합이 cash_floor· + concentration cap을 넘는 경우를 차단한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-B) + inputs: + - field: evaluated_candidates + unit: list_of_object + - field: max_set_size + unit: count + default: 3 + output: + field: selected_transition_set + unit: list_of_candidate_id + missing_policy: evaluated_candidates 비어있으면 selected_transition_set=[] + NO_TRADE. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_transition_set_enumerator_v1.py + version: 2026-06-17_P2_v8_9_adoption + IMMUTABLE_DECISION_LEDGER_V1: + purpose: > + 모든 의사결정을 append-only로 기록한다. 동일 decision_id 재기록은 거부하고, + T1/T5/T20/MAE/MFE는 원본 레코드를 수정하지 않고 별도 append로만 추가한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-C) + inputs: + - field: decision_id + unit: string + - field: input_hash_bundle + unit: string + - field: execution_mode + unit: enum + - field: candidate_ids + unit: list_of_string + - field: selected_transition_id + unit: string_or_null + required_fields: + - decision_id + - timestamp + - engine_version + - input_hash_bundle + - execution_mode + - candidate_ids + - selected_transition_id + - hard_blocks + - transition_utility_krw + - operator_override + - order_ids + - fill_prices + - slippage + - T1_return + - T5_return + - T20_return + - MAE + - MFE + output: + field: ledger_append_status + unit: 'enum: APPENDED | DUPLICATE_DECISION_ID | REJECTED_MISSING_FIELDS' + missing_policy: required_fields 결측 시 REJECTED_MISSING_FIELDS. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_immutable_decision_ledger_v1.py + version: 2026-06-17_P2_v8_9_adoption + EXECUTION_PLAN_COMPILER_V1: + purpose: > + order_capacity_krw를 30/30/40 LIMIT_SPLIT 슬라이스로 컴파일하고, 슬라이스 실행 직전마다 + cash_floor·capacity·spread를 재검증해 cancel_remaining_if 조건 충족 시 잔여 슬라이스를 취소한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-D) + inputs: + - field: order_capacity_krw + unit: KRW + - field: revalidation_snapshot + unit: json + - field: baseline_snapshot + unit: json + cancel_remaining_if: + - spread_widens_beyond_limit: "revalidation_snapshot.spread_bps > baseline_snapshot.spread_bps * 1.5" + - cash_floor_after_fill_breached: "revalidation_snapshot.cash_floor_pct < required_cash_pct" + - orderbook_capacity_collapses: "revalidation_snapshot.order_capacity_krw < baseline_snapshot.order_capacity_krw * 0.5" + output: + field: compiled_slices + unit: 'list_of_{slice_index, slice_amount_krw, status}' + missing_policy: order_capacity_krw 또는 baseline_snapshot 결측 시 EXECUTION_PLAN_BLOCKED. + canonical_ref: spec/formulas/domains/execution.yaml:EXECUTION_CAPACITY_LADDER_V1 + implementation: tools/build_execution_plan_compiler_v1.py + version: 2026-06-17_P2_v8_9_adoption + STATE_VECTOR_CONSTRUCTOR_V1: + purpose: > + holdings, cash, tax_lots, sector_graph, factor_exposures, macro_regime_probabilities를 + 단일 state_vector로 통합한다. 결측 component는 null로 유지하고 추정 보완 금지. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-A) + inputs: + - field: cash_ladder + unit: json + - field: positions + unit: list_of_object + - field: sector_exposure_graph + unit: list_of_object + - field: factor_exposures + unit: list_of_object + - field: tax_lots + unit: list_of_object + - field: risk_bucket_weights + unit: object + - field: macro_regime_probabilities + unit: object + - field: goal_progress_pct + unit: percent + output: + field: state_vector + unit: object + missing_policy: 결측 component는 null + missing_components 기록. 추정 보완 금지. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_state_vector_constructor_v1.py + version: 2026-06-17_P3_v8_9_adoption + REBALANCE_CADENCE_GATE_V1: + purpose: > + 주간/1·11·21일 점검을 의무 실행하되, transition_utility_after_tax_cost가 양수이거나 + hard_risk_block이 active일 때만 실제 리밸런싱 실행을 허용한다. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-D) + mandatory_schedule: + weekly_days: [SATURDAY, SUNDAY] + monthly_mid_check_days: [1, 11, 21] + inputs: + - field: today_date + unit: date + - field: transition_utility_after_tax_cost_krw + unit: number_or_null + - field: hard_risk_block_active + unit: boolean + output: + field: rebalance_execution_allowed + unit: boolean + missing_policy: 양쪽 입력 모두 결측 시 rebalance_execution_allowed=false + DATA_MISSING. + canonical_ref: spec/risk/aggregate_risk.yaml + implementation: tools/build_rebalance_cadence_gate_v1.py + version: 2026-06-17_P3_v8_9_adoption + WALK_FORWARD_BOOTSTRAP_V1: + purpose: > + historical_returns에서 walk-forward(비복원, in/out-of-sample 분리) 및 regime-matched + (동일 레짐 필터 + 복원추출) 리샘플링으로 net_profit_distribution을 생성한다. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-B) + inputs: + - field: historical_returns + unit: list_of_object + - field: current_regime_state + unit: string + - field: bootstrap_method + unit: enum + output: + field: net_profit_distribution_after_tax_fee_slippage + unit: list_of_KRW_or_null + missing_policy: historical_returns 결측 또는 표본 1건 이하면 null + DATA_MISSING. + canonical_ref: spec/29_backtest_harness_contract.yaml:current_metrics.walk_forward + implementation: tools/build_walk_forward_bootstrap_v1.py + version: 2026-06-17_P3_v8_9_adoption + WEEKLY_LEGACY_TRANSFER_PLAN_V1: + purpose: > + 주간 레거시→CMA 이전 계획을 입금 확인 전까지 deployable_cash_krw에 합산하지 않는다. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-E) + inputs: + - field: weekly_legacy_to_cma_transfer_plan_krw + unit: KRW + default: 4000000 + - field: transfer_confirmed + unit: boolean + - field: transfer_confirmed_amount_krw + unit: KRW_or_null + output: + field: deployable_cash_contribution_krw + unit: KRW + missing_policy: transfer_confirmed null이면 false로 간주. + canonical_ref: spec/risk/portfolio_exposure.yaml:cash_floor + implementation: tools/build_weekly_legacy_transfer_plan_v1.py + version: 2026-06-17_P3_v8_9_adoption SELL_EXECUTION_TIMING_V1: purpose: '장중 가격 움직임에 따라 매도 주문 유형과 타이밍을 결정론적으로 판정. 장초반 패닉 매도, 반등 직전 저점 투매 방지. @@ -4530,6 +4952,44 @@ formula_registry: implementation: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1 calibration_ref: spec/calibration_registry.yaml:NF1 (EXPERT_PRIOR) version: 2026-06-04_NF1 + PORTFOLIO_TRANSITION_UTILITY_V1: + purpose: > + 개별 매수·매도 추천이 아니라 포트폴리오 전체의 사후 상태(전환 후 cash floor, 집중도, CVaR, + 세후비용, 회전율)를 비교해 단일 최선 전환 또는 NO_TRADE를 결정론적으로 선택한다. + (governance/todo/v8_9_p0_adoption_plan.yaml P0-1.2, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:portfolio_transition_optimizer_v8_9) + default_action: NO_TRADE + inputs: + - field: ce70_net_profit_krw + source: Temp/forecast_simulation_engine_v1.json + unit: KRW + missing_policy: DATA_MISSING — candidate excluded, not assumed zero + - field: tax_fee_slippage_krw + source: Temp/sell_waterfall_engine_v4.json + unit: KRW + - field: cash_repair_benefit_krw + source: Temp/smart_cash_recovery_v9.json + unit: KRW + - field: concentration_reduction_benefit_krw + unit: KRW + - field: turnover_penalty_krw + unit: KRW + expression: > + transition_utility_krw = ce70_net_profit_krw - tax_fee_slippage_krw - cvar_penalty_krw + - drawdown_penalty_krw + cash_repair_benefit_krw + concentration_reduction_benefit_krw + - turnover_penalty_krw + output: + field: transition_utility_krw + unit: KRW + deterministic_fallbacks: + missing_optimizer_inputs: NO_TRADE_AND_QUARANTINE + solver_failure: NO_TRADE_AND_LOG_SOLVER_FAILURE + rank_tie: choose_lower_turnover_lower_tax_lower_marginal_risk_contribution + conflicting_runtime_packets: BLOCK_AND_REQUIRE_MANIFEST_REPAIR + missing_policy: hard_constraint_input_missing 시 NO_TRADE_AND_QUARANTINE + implementation: tools/build_portfolio_transition_optimizer_v1.py + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + version: 2026-06-17_P0_v8_9_adoption REBOUND_CAPTURE_THESIS_FACTOR_V1: purpose: 과매도 반등 진입을 thesis 팩터로 명시 — 영구 약세편향 해소 (Direction SFP1) agents_md_ref: 'Direction SFP1: SINGLE_FACTOR_DOMINANCE_CAP_V1 — REBOUND_CAPTURE diff --git a/spec/formula_golden_cases_v4.yaml b/spec/formula_golden_cases_v4.yaml index 6e4869b..a1e7dad 100644 --- a/spec/formula_golden_cases_v4.yaml +++ b/spec/formula_golden_cases_v4.yaml @@ -6,6 +6,254 @@ note: > golden_cases: + # ── PORTFOLIO_TRANSITION_UTILITY_V1: v8.9 P0 채택 (governance/todo/v8_9_p0_adoption_plan.yaml) ── + - formula_id: PORTFOLIO_TRANSITION_UTILITY_V1 + id: GV4_PTU_001 + name: V89_002 — 전환 입력 없음 (decision_packet 없음) → NO_TRADE 기본값 + input: {decision_packet: null, sell_waterfall: null} + expected: {final_action: NO_TRADE, gate: NO_TRADE_AND_QUARANTINE} + + - formula_id: PORTFOLIO_TRANSITION_UTILITY_V1 + id: GV4_PTU_002 + name: V89_048 — candidate 전부 hard_constraint_pass=false (solver_failure 등가) → NO_TRADE + input: {candidates: [{hard_constraint_pass: false}]} + expected: {final_action: NO_TRADE, selected_transition: null} + + - formula_id: PORTFOLIO_TRANSITION_UTILITY_V1 + id: GV4_PTU_003 + name: V89_049 — utility 동률 후보 → 낮은 turnover 후보 선택 + input: {candidates: [{transition_utility_krw: 100000, turnover_pct: 5}, {transition_utility_krw: 100000, turnover_pct: 2}]} + expected: {tie_breaker: lower_turnover} + + - formula_id: PORTFOLIO_TRANSITION_UTILITY_V1 + id: GV4_PTU_004 + name: V89_050 — 충돌하는 runtime packet → BLOCK_AND_REQUIRE_MANIFEST_REPAIR + input: {conflicting_packets: true} + expected: {final_action: NO_TRADE, reason_code: BLOCK_AND_REQUIRE_MANIFEST_REPAIR} + + # ── SELL_LOT_PARETO_SELECTOR_V1: v8.9 P0 채택 (governance/todo/v8_9_p0_adoption_plan.yaml) ── + - formula_id: SELL_LOT_PARETO_SELECTOR_V1 + id: GV4_SLP_001 + name: V89_029 — deconcentration_trim 후보가 cash_repair 후보를 dominate + input: {candidate_a: {avoided_tail_loss_krw: 100000, tax_fee_slippage_krw: 10000}, candidate_b: {avoided_tail_loss_krw: 50000, tax_fee_slippage_krw: 20000}} + expected: {dominates: true, pareto_rank_a: 1} + + - formula_id: SELL_LOT_PARETO_SELECTOR_V1 + id: GV4_SLP_002 + name: V89_030 — profit_lock 후보, missed_upside_penalty 미확인 시 0 보수적 하한 적용 + input: {missed_upside_penalty_krw: null} + expected: {missed_upside_penalty_krw_used: 0.0, missing_fields_includes: missed_upside_penalty_krw} + + - formula_id: SELL_LOT_PARETO_SELECTOR_V1 + id: GV4_SLP_003 + name: V89_031 — tax_drag_too_high, tax_fee_slippage가 benefit을 초과하면 score 음수 + input: {avoided_tail_loss_krw: 10000, tax_fee_slippage_krw: 50000} + expected: {lot_sell_score_krw: -40000.0} + + # ── FORECAST_SIMULATION_ENGINE_V1: v8.9 P0 채택 (governance/todo/v8_9_p0_adoption_plan.yaml) ── + - formula_id: FORECAST_SIMULATION_ENGINE_V1 + id: GV4_FSE_001 + name: V89_013 — 분포 없음(missing_CVaR) → QUARANTINE 등가 WATCH_ONLY, null 출력 + input: {distribution: null, execution_mode: SHADOW} + expected: {gate: WATCH_ONLY, cvar95_loss_krw: null, ce70_net_profit_krw: null} + + - formula_id: FORECAST_SIMULATION_ENGINE_V1 + id: GV4_FSE_002 + name: V89_014 — same_regime 표본 SHADOW 기준(10건) 미달 → WATCH_ONLY + input: {sample_count_total: 30, sample_count_same_regime: 5, execution_mode: SHADOW} + expected: {gate: WATCH_ONLY} + + # ── SECTOR_EXPOSURE_GRAPH_V1 / LEADER_LIFECYCLE_GATE_V1: v8.9 P1 채택 (governance/todo/v8_9_p1_adoption_plan.yaml) ── + - formula_id: SECTOR_EXPOSURE_GRAPH_V1 + id: GV4_SEG_001 + name: V89_044 — sector_overlap, ETF lookthrough가 direct weight와 합산되어 cap 초과 감지 + input: {direct_weight_pct: 20.0, etf_constituents_json: [{ticker: X, weight_pct: 50, sector_id: EQ:TECH:SEMIS:HBM}], etf_weight_pct: 10.0, sector_id: EQ:TECH:SEMIS:HBM} + expected: {sector_family_total_pct: 25.0, gate: PASS} + + - formula_id: SECTOR_EXPOSURE_GRAPH_V1 + id: GV4_SEG_002 + name: V89_045 — ETF_direct_overlap, constituents 미확인 시 ETF_BUY_BLOCKED (0 추정 금지) + input: {etf_constituents_json: null, etf_weight_pct: null} + expected: {gate: ETF_BUY_BLOCKED, sector_family_total_pct: null} + + - formula_id: LEADER_LIFECYCLE_GATE_V1 + id: GV4_LLG_001 + name: V89_046 — leader_distribution, CAPTAIN이 MA60 이탈+distribution이면 즉시 DISTRIBUTION_RISK + input: {current_role: CAPTAIN, above_ma60_or_reclaim_confirmed: false, institutional_flow_status: distribution} + expected: {leader_role: DISTRIBUTION_RISK, role_changed: true} + + # ── EXECUTION_CAPACITY_LADDER_V1: v8.9 P1 채택 (governance/todo/v8_9_p1_adoption_plan.yaml) ── + - formula_id: EXECUTION_CAPACITY_LADDER_V1 + id: GV4_ECL_001 + name: V89_019 — broker_packet_missing, 필드 결측 시 EXECUTION_PLAN_BLOCKED (capacity 0 추정 금지) + input: {avg_trade_value_20d_krw: null, intraday_trade_value_krw: 500000000, orderbook_top3_depth_krw: 100000000, spread_bps: 5} + expected: {gate: EXECUTION_PLAN_BLOCKED, order_capacity_krw: null} + + - formula_id: EXECUTION_CAPACITY_LADDER_V1 + id: GV4_ECL_002 + name: V89_020 — capacity_too_low, 계획 주문금액이 용량보다 크면 ORDER_SIZE_CAPPED + input: {planned_order_amount_krw: 50000000, avg_trade_value_20d_krw: 1000000000, intraday_trade_value_krw: 500000000, orderbook_top3_depth_krw: 100000000, spread_bps: 5} + expected: {gate: ORDER_SIZE_CAPPED, order_capacity_krw: 3000000.0} + + - formula_id: EXECUTION_CAPACITY_LADDER_V1 + id: GV4_ECL_003 + name: V89_022 — spread_widens, 기준 spread의 1.5배 초과 시 잔여 slice 취소 + input: {current_spread_bps: 16, baseline_spread_bps: 10} + expected: {cancel_remaining: true} + + # ── MODEL_GOVERNANCE_KILL_SWITCH_V1: v8.9 P1 채택 (governance/todo/v8_9_p1_adoption_plan.yaml) ── + - formula_id: MODEL_GOVERNANCE_KILL_SWITCH_V1 + id: GV4_MGK_001 + name: V89_035 — T5 hit rate 50% 미달(30건 이상) → kill switch 발동, 1단계 강등 + input: {t5_hit_rate_pct: 40.0, t5_sample_count: 30, current_mode: PILOT} + expected: {kill_switch_triggered: true, execution_mode: SHADOW} + + - formula_id: MODEL_GOVERNANCE_KILL_SWITCH_V1 + id: GV4_MGK_002 + name: V89_036 — implementation_shortfall 기대치 2배 초과 → kill switch 발동 + input: {implementation_shortfall_ratio: 2.5} + expected: {kill_switch_triggered: true, reason_code: implementation_shortfall_above_2x_expected} + + - formula_id: MODEL_GOVERNANCE_KILL_SWITCH_V1 + id: GV4_MGK_003 + name: V89_037 — data_quarantine_rate 5% 초과 → kill switch 발동 + input: {data_quarantine_rate_pct: 7.0} + expected: {kill_switch_triggered: true, reason_code: data_quarantine_rate_above_5pct} + + # ── SCENARIO_SHOCK_MATRIX_V1: v8.9 P2 채택 (governance/todo/v8_9_p2_adoption_plan.yaml) ── + - formula_id: SCENARIO_SHOCK_MATRIX_V1 + id: GV4_SSM_001 + name: V89_010 — candidate_good_portfolio_bad, crisis_case가 base_case보다 손실 확대 + input: {scenario_id: crisis_case, base_distribution_present: true} + expected: {gate: PASS, crisis_worse_than_base: true} + + - formula_id: SCENARIO_SHOCK_MATRIX_V1 + id: GV4_SSM_002 + name: 분포 없음 → 전체 시나리오 DATA_MISSING (가짜 분포 생성 금지) + input: {distribution: null} + expected: {gate: DATA_MISSING, scenario_ce70_krw: null} + + # ── TRANSITION_SET_ENUMERATOR_V1: v8.9 P2 채택 (governance/todo/v8_9_p2_adoption_plan.yaml) ── + - formula_id: TRANSITION_SET_ENUMERATOR_V1 + id: GV4_TSE_001 + name: V89_010 — 개별 PASS 후보 2개의 조합이 cash_floor를 위반하면 조합 거부, 단일 후보만 선택 + input: {candidates: [{id: A, cash_floor_delta: 1.0}, {id: B, cash_floor_delta: -2.0}]} + expected: {selected_transition_set: [A], rejected_sets_count_gte: 1} + + - formula_id: TRANSITION_SET_ENUMERATOR_V1 + id: GV4_TSE_002 + name: V89_048 — candidate_actions 없음 → NO_TRADE, 빈 조합 임의 생성 금지 + input: {candidate_actions: []} + expected: {gate: NO_TRADE, selected_transition_set: []} + + - formula_id: TRANSITION_SET_ENUMERATOR_V1 + id: GV4_TSE_003 + name: V89_049 — utility 동률 시 더 작은 조합(낮은 복잡도) 선택 + input: {set_a_utility: 100000, set_a_size: 1, set_b_utility: 100000, set_b_size: 2} + expected: {selected: set_a} + + # ── IMMUTABLE_DECISION_LEDGER_V1: v8.9 P2 채택 (governance/todo/v8_9_p2_adoption_plan.yaml) ── + - formula_id: IMMUTABLE_DECISION_LEDGER_V1 + id: GV4_IDL_001 + name: V89_039 — operator_override 기록 필수, 신규 decision_id append 성공 + input: {decision_id: D1, engine_version: PORTFOLIO_TRANSITION_UTILITY_V1, input_hash_bundle: abc123, execution_mode: NO_TRADE, candidate_ids: [A]} + expected: {ledger_append_status: APPENDED} + + - formula_id: IMMUTABLE_DECISION_LEDGER_V1 + id: GV4_IDL_002 + name: 동일 decision_id 재기록 시도 → DUPLICATE_DECISION_ID (불변성 보장) + input: {decision_id: D1, repeat: true} + expected: {ledger_append_status: DUPLICATE_DECISION_ID} + + - formula_id: IMMUTABLE_DECISION_LEDGER_V1 + id: GV4_IDL_003 + name: required_fields 결측 시 REJECTED_MISSING_FIELDS (빈값 채움 금지) + input: {decision_id: null} + expected: {ledger_append_status: REJECTED_MISSING_FIELDS} + + # ── EXECUTION_PLAN_COMPILER_V1: v8.9 P2 채택 (governance/todo/v8_9_p2_adoption_plan.yaml) ── + - formula_id: EXECUTION_PLAN_COMPILER_V1 + id: GV4_EPC_001 + name: V89_021 — partial_fill, slice 1 체결 후 정상 조건이면 slice 2/3도 컴파일 + input: {baseline_spread_bps: 10, revalidation_spread_bps: 10} + expected: {all_slices_compiled: true} + + - formula_id: EXECUTION_PLAN_COMPILER_V1 + id: GV4_EPC_002 + name: V89_022 — spread_widens, slice 2 직전 spread 1.5배 초과 시 잔여 slice 취소 + input: {baseline_spread_bps: 10, slice2_revalidation_spread_bps: 20} + expected: {slice_1_status: COMPILED, slice_2_status: CANCELLED, slice_3_status: CANCELLED} + + - formula_id: EXECUTION_PLAN_COMPILER_V1 + id: GV4_EPC_003 + name: V89_023 — gap_up_chase 등가, order_capacity_krw 결측 시 전체 EXECUTION_PLAN_BLOCKED + input: {order_capacity_krw: null} + expected: {gate: EXECUTION_PLAN_BLOCKED, compiled_slices: []} + + # ── STATE_VECTOR_CONSTRUCTOR_V1: v8.9 P3 채택 (governance/todo/v8_9_p3_adoption_plan.yaml) ── + - formula_id: STATE_VECTOR_CONSTRUCTOR_V1 + id: GV4_SVC_001 + name: V89_052 — goal_far_from_target, 모든 component 결측 시 completeness 0%, 보완 금지 + input: {cash_ladder: null, positions: null} + expected: {state_vector_completeness_pct: 0.0, missing_components_count: 8} + + - formula_id: STATE_VECTOR_CONSTRUCTOR_V1 + id: GV4_SVC_002 + name: 일부 component만 존재해도 결측 항목만 null로 기록(다른 값으로 보완 금지) + input: {cash_ladder: present, positions: present, factor_exposures: null} + expected: {missing_components_includes: factor_exposures} + + # ── WALK_FORWARD_BOOTSTRAP_V1: v8.9 P3 채택 (governance/todo/v8_9_p3_adoption_plan.yaml) ── + - formula_id: WALK_FORWARD_BOOTSTRAP_V1 + id: GV4_WFB_001 + name: V89_014 — same_regime_sample_low, 필터 결과 1건 이하 → DATA_MISSING (다른 레짐 대체 금지) + input: {current_regime_state: NEVER_SEEN_REGIME, historical_returns_count: 30} + expected: {gate: DATA_MISSING, net_profit_distribution_after_tax_fee_slippage: null} + + - formula_id: WALK_FORWARD_BOOTSTRAP_V1 + id: GV4_WFB_002 + name: V89_048 — historical_returns 없음 → solver_failure 등가 DATA_MISSING + input: {historical_returns: null} + expected: {gate: DATA_MISSING, sample_count_total: 0} + + # ── REBALANCE_CADENCE_GATE_V1: v8.9 P3 채택 (governance/todo/v8_9_p3_adoption_plan.yaml) ── + - formula_id: REBALANCE_CADENCE_GATE_V1 + id: GV4_RCG_001 + name: V89_032 — no_trade_band, 토요일 점검은 의무 emit되나 utility 음수+hard block 없음 → NO_TRADE + input: {check_date: 2026-06-20, transition_utility_after_tax_cost_krw: -5000, hard_risk_block_active: false} + expected: {review_emitted: true, rebalance_execution_allowed: false} + + - formula_id: REBALANCE_CADENCE_GATE_V1 + id: GV4_RCG_002 + name: V89_033 — hard_block_overrides_band, utility 음수여도 hard_risk_block_active=true면 실행 허용 + input: {check_date: 2026-06-20, transition_utility_after_tax_cost_krw: -5000, hard_risk_block_active: true} + expected: {rebalance_execution_allowed: true} + + - formula_id: REBALANCE_CADENCE_GATE_V1 + id: GV4_RCG_003 + name: V89_053 — weekly_rebalance_required, 토/일은 항상 cadence_check_required=true + input: {check_date: 2026-06-20} + expected: {cadence_check_required: true, cadence_trigger_reason: weekly_rebalance_required} + + - formula_id: REBALANCE_CADENCE_GATE_V1 + id: GV4_RCG_004 + name: V89_054 — mid_check_required, 매월 1/11/21일은 cadence_check_required=true + input: {check_date: 2026-06-11} + expected: {cadence_check_required: true, cadence_trigger_reason: mid_check_required} + + # ── WEEKLY_LEGACY_TRANSFER_PLAN_V1: v8.9 P3 채택 (governance/todo/v8_9_p3_adoption_plan.yaml) ── + - formula_id: WEEKLY_LEGACY_TRANSFER_PLAN_V1 + id: GV4_WLT_001 + name: V89_005 — deployable_cash_negative, 입금 미확인 계획액은 deployable_cash에 0으로 기여 + input: {weekly_legacy_to_cma_transfer_plan_krw: 4000000, transfer_confirmed: false} + expected: {deployable_cash_contribution_krw: 0.0, plan_status: PLANNED_NOT_DEPLOYABLE} + + - formula_id: WEEKLY_LEGACY_TRANSFER_PLAN_V1 + id: GV4_WLT_002 + name: 입금 확인 시 확정액(계획액과 다를 수 있음)만 deployable_cash에 합산 + input: {weekly_legacy_to_cma_transfer_plan_krw: 4000000, transfer_confirmed: true, transfer_confirmed_amount_krw: 3800000} + expected: {deployable_cash_contribution_krw: 3800000.0, plan_status: CONFIRMED_DEPLOYABLE} + # ── STOP_BREACH_V1: profit_pct < -20% 경계값 3 케이스 ───────────────────── - formula_id: STOP_BREACH_V1 id: GV4_STOP_001 diff --git a/spec/formulas/domains/cash.yaml b/spec/formulas/domains/cash.yaml index 9eb6285..8c0547e 100644 --- a/spec/formulas/domains/cash.yaml +++ b/spec/formulas/domains/cash.yaml @@ -537,6 +537,73 @@ formulas: activation_threshold: min_t20_sample: 30 retirement_condition: performance_degradation + SELL_LOT_PARETO_SELECTOR_V1: + purpose: > + SELL_WATERFALL_ENGINE_V1의 동일 hard_precedence 단계 안에서 후보 lot을 점수화하고, + 세금 회피 효과·반등 후 재진입 비용·놓친 상승분까지 포함한 다목적(Pareto) 비교로 + 동순위 후보 중 어느 lot을 먼저 매도할지 결정론적으로 선택한다. + (governance/todo/v8_9_p0_adoption_plan.yaml P0-2.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sell_and_cash_repair_optimizer_v8_9) + applicable: SELL_WATERFALL_ENGINE_V1의 동일 stage 내 후보가 2개 이상일 때. + inputs: + - field: avoided_tail_loss_krw + unit: KRW + - field: cash_repair_benefit_krw + unit: KRW + - field: concentration_reduction_benefit_krw + unit: KRW + - field: tax_loss_benefit_krw + unit: KRW + note: 손실 lot 매도 시 세금 절감 효과(tax-loss harvesting). 비과세 계좌는 0. + - field: tax_fee_slippage_krw + unit: KRW + - field: reentry_cost_krw + unit: KRW + note: 매도 후 동일·유사 종목 재진입 시 예상 거래비용·스프레드 비용. + - field: missed_upside_penalty_krw + unit: KRW + note: 매도하지 않았다면 얻었을 상승분 추정치. CE70_NET_PROFIT_KRW 분포가 있으면 그 값을 사용하고, 없으면 0(추정 금지). + expression: > + LOT_SELL_SCORE_KRW = avoided_tail_loss_krw + cash_repair_benefit_krw + concentration_reduction_benefit_krw + + tax_loss_benefit_krw - tax_fee_slippage_krw - reentry_cost_krw - missed_upside_penalty_krw + output: + field: lot_sell_score_krw + unit: KRW + pareto_dominance_rule: + purpose: 동일 hard_precedence 단계 안에서 단일 점수만으로 비교하기 모호할 때 다목적 우위를 결정론적으로 판정. + objectives_maximize: [avoided_tail_loss_krw, cash_repair_benefit_krw, concentration_reduction_benefit_krw, tax_loss_benefit_krw] + objectives_minimize: [tax_fee_slippage_krw, reentry_cost_krw, missed_upside_penalty_krw] + dominates_if: > + candidate A가 모든 objectives_maximize 항목에서 B 이상이고 모든 objectives_minimize 항목에서 B 이하이며, + 적어도 한 항목에서 A가 B보다 우월하면 A dominates B. + tie_breaker_if_no_dominance: + - lot_sell_score_krw 높은 순 + - tax_fee_slippage_krw 낮은 순 + - reentry_cost_krw 낮은 순 + missing_policy: + missed_upside_penalty_krw: CE70_NET_PROFIT_KRW 분포 없으면 0 사용(추정 아님 — 보수적 하한). 0 사용 사실을 output에 명시. + tax_loss_benefit_krw: 계좌유형 미확인 시 0 (taxable 가정 금지, ISA/연금 비과세 가정도 금지 — DATA_MISSING 표기) + canonical_ref: spec/risk/portfolio_exposure.yaml:sell_priority_engine.candidate_scoring + implementation: tools/build_sell_waterfall_engine_v4.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - avoided_tail_loss_krw + - cash_repair_benefit_krw + - concentration_reduction_benefit_krw + - tax_loss_benefit_krw + - tax_fee_slippage_krw + - reentry_cost_krw + - missed_upside_penalty_krw + output_fields: + - lot_sell_score_krw + golden_cases: + - V89_029_deconcentration_trim + - V89_030_profit_lock + - V89_031_tax_drag_too_high + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation SMART_MONEY_LIQUIDITY_GATE_V1: purpose: '스마트머니·유동성 차단 게이트. SM001(외국인+기관 동시 순매도→BLOCK_BUY), SM002(5일 평균 거래대금 < 50억→LIMIT_QUANTITY), SM003(RSI14>70 AND flow_credit<0.3→BLOCK_BUY) 결정론 구현. FINAL_JUDGMENT_GATE_V1의 @@ -946,3 +1013,50 @@ formulas: activation_threshold: min_t20_sample: 30 retirement_condition: performance_degradation + WEEKLY_LEGACY_TRANSFER_PLAN_V1: + purpose: > + 주간 레거시종목→CMA 이전 계획(weekly_legacy_to_cma_transfer_plan_krw)을 입금이 실제로 + 확인되기 전까지는 deployable_cash_krw에 합산하지 않는다. 계획 단계(planned)와 + 확정 단계(confirmed)를 분리해 "이전될 돈을 이미 쓸 수 있는 돈"으로 취급하는 오류를 막는다. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-E, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:implementation_todo_v8_9.P3_sell_and_rebalance, + portfolio_policy_v8_9.operator_cashflow_config) + applicable: CASH_RATIOS_V1·DEPLOYABLE_CASH_KRW_V1 계산 직전. weekly_legacy_to_cma_transfer_plan_krw가 0보다 클 때. + inputs: + - field: weekly_legacy_to_cma_transfer_plan_krw + unit: KRW + default: 4000000 + note: spec/risk/portfolio_exposure.yaml의 operator_cashflow_config 고정 계획값(월별 갱신). + - field: transfer_confirmed + unit: boolean + note: 실제 계좌 입금 확인 여부. 계획만으로는 false. + - field: transfer_confirmed_amount_krw + unit: KRW_or_null + note: 확인된 입금액. transfer_confirmed=false면 null. + rule: > + transfer_confirmed=false인 동안 weekly_legacy_to_cma_transfer_plan_krw는 deployable_cash_krw + 계산에 포함되지 않는다(plan_status=PLANNED_NOT_DEPLOYABLE). transfer_confirmed=true가 되면 + transfer_confirmed_amount_krw만 deployable_cash_krw에 합산한다(plan_status=CONFIRMED_DEPLOYABLE). + 계획액과 확정액이 다르면 확정액을 우선한다(계획액으로 추정 보완 금지). + output: + field: deployable_cash_contribution_krw + unit: KRW + additional_outputs: + - plan_status + missing_policy: transfer_confirmed가 null이면 false로 간주(보수적 — 입금 미확인 상태와 동일 처리). + canonical_ref: spec/risk/portfolio_exposure.yaml:cash_floor + implementation: tools/build_weekly_legacy_transfer_plan_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - weekly_legacy_to_cma_transfer_plan_krw + - transfer_confirmed + - transfer_confirmed_amount_krw + output_fields: + - deployable_cash_contribution_krw + - plan_status + golden_cases: + - V89_005_deployable_cash_negative + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/formulas/domains/execution.yaml b/spec/formulas/domains/execution.yaml new file mode 100644 index 0000000..bed8938 --- /dev/null +++ b/spec/formulas/domains/execution.yaml @@ -0,0 +1,144 @@ +schema_version: formula_domain.v1 +source: C:\Temp\data_feed\spec\13_formula_registry.yaml +domain: execution +meta: + note: > + governance/todo/v8_9_p1_adoption_plan.yaml P1-B. + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:execution_plan_compiler_v8_9 +formulas: + EXECUTION_CAPACITY_LADDER_V1: + purpose: > + 계획된 주문금액이 종목의 실제 체결 가능 용량(20일 평균거래대금, 당일 거래대금, 호가창 깊이)을 + 초과하지 않도록 결정론적으로 캡핑한다. broker_microstructure_packet이 없으면 주문 계획 자체를 + 차단한다(v8.9 V89_019). + (governance/todo/v8_9_p1_adoption_plan.yaml P1-B.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:execution_plan_compiler_v8_9.broker_microstructure_packet_required) + applicable: PORTFOLIO_TRANSITION_UTILITY_V1에서 selected_transition 확정 후, 주문 분할(split_order_template) 직전. + inputs: + - field: planned_order_amount_krw + unit: KRW + - field: avg_trade_value_20d_krw + unit: KRW + source: 기존 avg_trade_value_5d(spec/12_field_dictionary.yaml)의 20일 윈도우 변형 + - field: intraday_trade_value_krw + unit: KRW + - field: orderbook_top3_depth_krw + unit: KRW + - field: spread_bps + unit: basis_points + - field: tick_size + unit: KRW_per_share + source: spec/formulas/domains/cash.yaml:tick_size_table + - field: daily_price_limit + unit: percent + - field: halt_status + unit: boolean + expression: > + order_capacity_krw = min(planned_order_amount_krw, avg_trade_value_20d_krw * 0.003, + intraday_trade_value_krw * 0.01, orderbook_top3_depth_krw * 0.30) + output: + field: order_capacity_krw + unit: KRW + gates: + - if: halt_status == true + action: EXECUTION_PLAN_BLOCKED + reason_code: trading_halt + - if: avg_trade_value_20d_krw is null OR orderbook_top3_depth_krw is null OR spread_bps is null + action: EXECUTION_PLAN_BLOCKED + reason_code: broker_packet_missing + - if: order_capacity_krw < planned_order_amount_krw + action: ORDER_SIZE_CAPPED + reason_code: capacity_too_low + spread_widen_cancel_rule: + condition: spread_bps > spread_bps_baseline * 1.5 (slice 체결 사이 측정) + action: CANCEL_REMAINING_SLICES + canonical_ref: suggest/quant_investment_engine_v8_9...:execution_plan_compiler_v8_9.cancel_remaining_if + split_order_template: + slice_1_pct: 30 + slice_2_pct: 30 + slice_3_pct: 40 + requires_revalidation_before_each_slice: true + revalidation_fields: [cash_floor, deployable_cash, order_capacity_krw, spread_bps] + missing_policy: broker_microstructure_packet 필드 중 하나라도 null이면 EXECUTION_PLAN_BLOCKED. 추정 금지. + canonical_ref: spec/05_position_sizing.yaml + implementation: tools/build_execution_capacity_ladder_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - planned_order_amount_krw + - avg_trade_value_20d_krw + - intraday_trade_value_krw + - orderbook_top3_depth_krw + - spread_bps + - tick_size + - daily_price_limit + - halt_status + output_fields: + - order_capacity_krw + golden_cases: + - V89_019_broker_packet_missing + - V89_020_capacity_too_low + - V89_022_spread_widens + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + EXECUTION_PLAN_COMPILER_V1: + purpose: > + EXECUTION_CAPACITY_LADDER_V1이 산출한 order_capacity_krw를 30/30/40 LIMIT_SPLIT 슬라이스로 + 컴파일하고, 각 슬라이스 실행 직전 cash_floor·capacity·spread를 재검증한다. + 재검증 실패 또는 cancel_remaining_if 조건 충족 시 잔여 슬라이스를 취소한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-D, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:execution_plan_compiler_v8_9.split_order_template, + execution_plan_compiler_v8_9.cancel_remaining_if) + applicable: EXECUTION_CAPACITY_LADDER_V1.gate가 PASS 또는 ORDER_SIZE_CAPPED일 때만 호출. + inputs: + - field: order_capacity_krw + unit: KRW + source: spec/formulas/domains/execution.yaml:EXECUTION_CAPACITY_LADDER_V1 + - field: slice_index + unit: 'enum: 1 | 2 | 3' + - field: revalidation_snapshot + unit: json + note: 'slice 직전 시점의 {cash_floor_pct, deployable_cash_krw, order_capacity_krw, spread_bps}' + - field: baseline_snapshot + unit: json + note: 컴파일 시점(slice 1 이전)의 동일 필드 스냅샷. spread_widen_cancel_rule 기준값. + cancel_remaining_if: + - captain_reverses_intraday + - index_drop_exceeds_threshold + - spread_widens_beyond_limit: "revalidation_snapshot.spread_bps > baseline_snapshot.spread_bps * 1.5" + - cash_floor_after_fill_breached: "revalidation_snapshot.cash_floor_pct < required_cash_pct" + - data_quarantine_after_slice + - orderbook_capacity_collapses: "revalidation_snapshot.order_capacity_krw < baseline_snapshot.order_capacity_krw * 0.5" + expression: > + slice_amount_krw(1) = order_capacity_krw * 0.30 + slice_amount_krw(2) = order_capacity_krw * 0.30 + slice_amount_krw(3) = order_capacity_krw * 0.40 + 각 슬라이스는 직전 슬라이스 체결 후 revalidation_snapshot을 재계산하고 cancel_remaining_if를 + 평가한 뒤에만 진행한다. 어느 한 조건이라도 true이면 이후 슬라이스는 컴파일하지 않는다. + output: + field: compiled_slices + unit: 'list_of_{slice_index, slice_amount_krw, status}' + additional_outputs: + - cancel_reason_code + - slices_executed_count + missing_policy: order_capacity_krw 또는 baseline_snapshot 결측 시 컴파일 자체를 EXECUTION_PLAN_BLOCKED. + canonical_ref: spec/formulas/domains/execution.yaml:EXECUTION_CAPACITY_LADDER_V1 + implementation: tools/build_execution_plan_compiler_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - order_capacity_krw + - slice_index + - revalidation_snapshot + - baseline_snapshot + output_fields: + - compiled_slices + - cancel_reason_code + golden_cases: + - V89_021_partial_fill + - V89_022_spread_widens + - V89_023_gap_up_chase + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/formulas/domains/governance.yaml b/spec/formulas/domains/governance.yaml new file mode 100644 index 0000000..6700b0f --- /dev/null +++ b/spec/formulas/domains/governance.yaml @@ -0,0 +1,163 @@ +schema_version: formula_domain.v1 +source: C:\Temp\data_feed\spec\13_formula_registry.yaml +domain: governance +meta: + note: > + governance/todo/v8_9_p1_adoption_plan.yaml P1-C. + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:model_governance_v8_9 + 이 도메인은 종목/팩터 수준 promotion(spec/57_shadow_promotion_scorecard.yaml)과는 별개로 + 전략 execution_mode 단계(AUDIT_ONLY→SHADOW→PILOT→LIVE_LIMITED→LIVE_FULL) 전체를 다룬다. +formulas: + MODEL_GOVERNANCE_KILL_SWITCH_V1: + purpose: > + data_quarantine_rate, implementation_shortfall, T5_hit_rate, calibration_error, + drawdown 5개 지표를 감시해 기준 이탈 시 execution_mode를 자동으로 한 단계 강등한다. + LLM이나 운영자가 "이번엔 괜찮을 것"이라는 서사로 강등을 보류하는 것을 금지한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-C.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:risk_controls_v8_9.kill_switches, + model_governance_v8_9.automatic_demotion) + applicable: 매 의사결정 사이클 시작 전. PORTFOLIO_TRANSITION_UTILITY_V1보다 먼저 평가되어 execution_mode를 확정한다. + promotion_ladder: [AUDIT_ONLY, SHADOW, PILOT, LIVE_LIMITED, LIVE_FULL] + inputs: + - field: data_quarantine_rate_pct + unit: percent + source: tools/build_yaml_code_coverage_v1.py 계열 — 결측/충돌로 quarantine된 입력 비율 + - field: implementation_shortfall_ratio + unit: ratio + note: 실제 슬리피지 / 기대 슬리피지. 2.0 초과 시 위반. + - field: t5_hit_rate_pct + source: spec/29_backtest_harness_contract.yaml:current_metrics.direction_accuracy.t5_op_rate + unit: percent + - field: t5_sample_count + source: spec/29_backtest_harness_contract.yaml:current_metrics.direction_accuracy.t5_op_rate.n_sample + unit: count + - field: calibration_error + source: spec/calibration_registry.yaml + unit: ratio + - field: calibration_error_limit + unit: ratio + - field: account_mdd_pct + unit: percent + - field: account_mdd_budget_pct + source: spec/risk/aggregate_risk.yaml + unit: percent + kill_switch_conditions: + - id: data_quarantine_rate_above_5pct + condition: data_quarantine_rate_pct > 5.0 + - id: implementation_shortfall_above_2x_expected + condition: implementation_shortfall_ratio > 2.0 + - id: t5_hit_rate_below_50pct_for_30_trades + condition: t5_sample_count >= 30 AND t5_hit_rate_pct < 50.0 + - id: calibration_error_above_limit + condition: calibration_error > calibration_error_limit + - id: unexpected_drawdown_breach + condition: account_mdd_pct > account_mdd_budget_pct + demotion_rule: > + kill_switch_conditions 중 하나라도 true이면 execution_mode를 promotion_ladder에서 + 현재 단계 -1 (한 단계만 강등). AUDIT_ONLY는 더 이상 강등되지 않는다(최저 단계). + 여러 조건이 동시에 발동해도 1단계만 강등(과잉반응 방지) — 단, 재평가 사이클마다 조건이 + 계속 true이면 추가로 1단계씩 강등된다. + promotion_rule: > + kill_switch_conditions 전부 false이고 spec/57_shadow_promotion_scorecard.yaml의 + promotion_gate_criteria(해당 단계 전환 기준)를 만족할 때만 한 단계 승급. 자동 승급 없음 — + 승급은 operator_override 기록을 동반해야 한다(v8.9 V89_039). + output: + field: execution_mode + unit: enum + additional_outputs: + - kill_switch_triggered + - kill_switch_reason_codes + - execution_mode_changed + missing_policy: 입력 지표 중 하나라도 null이면 해당 kill switch는 평가 불가로 PARTIAL 표기하고, 평가 가능한 지표만으로 판정한다. 모든 지표 null이면 execution_mode 변경 없이 DATA_MISSING. + canonical_ref: spec/57_shadow_promotion_scorecard.yaml + implementation: tools/build_model_governance_kill_switch_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - data_quarantine_rate_pct + - implementation_shortfall_ratio + - t5_hit_rate_pct + - t5_sample_count + - calibration_error + - calibration_error_limit + - account_mdd_pct + - account_mdd_budget_pct + output_fields: + - execution_mode + - kill_switch_triggered + - kill_switch_reason_codes + golden_cases: + - V89_035_model_kill_switch_hit_rate + - V89_036_model_kill_switch_slippage + - V89_037_data_quarantine_rate + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + IMMUTABLE_DECISION_LEDGER_V1: + purpose: > + 모든 의사결정을 append-only로 기록해 사후 재구성과 T1/T5/T20 성과 귀속을 가능하게 한다. + 기존 레코드 수정·삭제를 금지하며, 동일 decision_id 재기록 시도는 거부한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-C, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:model_governance_v8_9.immutable_decision_log_required_fields) + applicable: PORTFOLIO_TRANSITION_UTILITY_V1 또는 TRANSITION_SET_ENUMERATOR_V1이 selected_transition을 확정한 직후. + required_fields: + - decision_id + - timestamp + - engine_version + - input_hash_bundle + - execution_mode + - candidate_ids + - selected_transition_id + - hard_blocks + - transition_utility_krw + - operator_override + - order_ids + - fill_prices + - slippage + - T1_return + - T5_return + - T20_return + - MAE + - MFE + append_only_rule: > + decision_id가 이미 ledger에 존재하면 신규 append를 거부하고 DUPLICATE_DECISION_ID 오류를 반환한다. + 기존 레코드의 필드 값을 변경하는 호출은 없다 — T1/T5/T20/MAE/MFE는 별도의 update_outcome + append(새 레코드, 동일 decision_id 참조)로만 추가하고 원본 decision 레코드는 불변으로 둔다. + inputs: + - field: decision_id + unit: string + - field: engine_version + unit: string + - field: input_hash_bundle + unit: string + - field: execution_mode + unit: enum + - field: candidate_ids + unit: list_of_string + - field: selected_transition_id + unit: string_or_null + - field: transition_utility_krw + unit: number_or_null + output: + field: ledger_append_status + unit: 'enum: APPENDED | DUPLICATE_DECISION_ID | REJECTED_MISSING_FIELDS' + missing_policy: required_fields 중 하나라도 없으면 REJECTED_MISSING_FIELDS — 빈 문자열/0으로 채워 append 금지. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_immutable_decision_ledger_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - decision_id + - engine_version + - input_hash_bundle + - execution_mode + - candidate_ids + - selected_transition_id + - transition_utility_krw + output_fields: + - ledger_append_status + golden_cases: + - V89_039_operator_override + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/formulas/domains/portfolio.yaml b/spec/formulas/domains/portfolio.yaml index 2570b2b..96b66f6 100644 --- a/spec/formulas/domains/portfolio.yaml +++ b/spec/formulas/domains/portfolio.yaml @@ -668,3 +668,264 @@ formulas: activation_threshold: min_t20_sample: 30 retirement_condition: performance_degradation + PORTFOLIO_TRANSITION_UTILITY_V1: + purpose: > + 개별 매수·매도 추천이 아니라 포트폴리오 전체의 사후 상태(전환 후 cash floor, 집중도, CVaR, + 세후비용, 회전율)를 비교해 단일 최선 전환 또는 NO_TRADE를 결정론적으로 선택한다. + (governance/todo/v8_9_p0_adoption_plan.yaml P0-1.2, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:portfolio_transition_optimizer_v8_9) + default_action: NO_TRADE + state_vector_fields: + - cash_ladder + - positions + - sector_exposure_graph + - tax_lots + - risk_bucket_weights + - goal_progress_pct + - data_quality_scores + candidate_action_schema: + - candidate_id + - asset_id + - action_type + - planned_amount_krw + - source_signal_ids + - numeric_provenance_status + hard_veto_order: + - DATA_INVALID + - EXECUTION_MODE_BLOCK + - CASH_FLOOR_BLOCK + - HARD_CONCENTRATION_BLOCK + - NEGATIVE_TRANSITION_UTILITY + inputs: + - field: ce70_net_profit_krw + source: Temp/forecast_simulation_engine_v1.json + unit: KRW + missing_policy: DATA_MISSING — candidate excluded, not assumed zero + - field: tax_fee_slippage_krw + source: Temp/sell_waterfall_engine_v4.json + unit: KRW + - field: cash_repair_benefit_krw + source: Temp/smart_cash_recovery_v9.json + unit: KRW + - field: concentration_reduction_benefit_krw + unit: KRW + - field: turnover_penalty_krw + unit: KRW + expression: > + transition_utility_krw = ce70_net_profit_krw - tax_fee_slippage_krw - cvar_penalty_krw + - drawdown_penalty_krw + cash_repair_benefit_krw + concentration_reduction_benefit_krw + - turnover_penalty_krw + output: + field: transition_utility_krw + unit: KRW + acceptance_margin: + formula: acceptance_margin_krw = transition_utility_krw - max(mode_absolute_hurdle_krw, hurdle_multiple * estimated_total_cost_krw) + reject_if: acceptance_margin_krw <= 0 + deterministic_fallbacks: + missing_optimizer_inputs: NO_TRADE_AND_QUARANTINE + solver_failure: NO_TRADE_AND_LOG_SOLVER_FAILURE + rank_tie: choose_lower_turnover_lower_tax_lower_marginal_risk_contribution + conflicting_runtime_packets: BLOCK_AND_REQUIRE_MANIFEST_REPAIR + missing_policy: + hard_constraint_input_missing: NO_TRADE_AND_QUARANTINE + canonical_ref: spec/risk/portfolio_exposure.yaml:concentration_caps_v8_9_supplement + implementation: tools/build_portfolio_transition_optimizer_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - ce70_net_profit_krw + - tax_fee_slippage_krw + - cash_repair_benefit_krw + - concentration_reduction_benefit_krw + - turnover_penalty_krw + output_fields: + - transition_utility_krw + - acceptance_margin_krw + - selected_transition + golden_cases: + - V89_002_no_trade_default + - V89_048_solver_failure + - V89_049_rank_tie + - V89_050_conflicting_packets + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + TRANSITION_SET_ENUMERATOR_V1: + purpose: > + PORTFOLIO_TRANSITION_UTILITY_V1이 candidate 1건씩 평가하는 것을 넘어, 여러 candidate를 + 조합한 transition_set 단위로 hard_constraint_pass와 transition_utility_krw를 평가한다. + "좋은 후보 하나"가 포트폴리오 전체를 악화시키는 경우(V89_010)를 후보 조합 비교로 차단한다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-B, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:portfolio_transition_optimizer_v8_9.selection_algorithm) + applicable: PORTFOLIO_TRANSITION_UTILITY_V1이 candidate별 transition_utility_krw를 산출한 직후. + inputs: + - field: evaluated_candidates + unit: list_of_object + note: PORTFOLIO_TRANSITION_UTILITY_V1.candidate_actions 산출(hard_constraint_pass, transition_utility_krw 포함) + - field: max_set_size + unit: count + default: 3 + note: 조합 폭발 방지. v8.9 turnover_budget(주간 5%) 고려 시 동시 실행 후보는 통상 1~3건. + selection_algorithm: + - step_1: hard_constraint_pass=false인 candidate는 set 구성에서 제외(개별 veto 우선) + - step_2: 남은 candidate에서 크기 1..max_set_size의 모든 조합(transition_set)을 생성 + - step_3: 각 transition_set의 post_trade_cash_floor_pct, post_trade_concentration_pct, post_trade_MRC(marginal_risk_contribution), post_trade_CVaR95_krw를 합산 재평가 + - step_3b: post_trade_CVaR95_krw = sum(candidate.cvar95_loss_krw for candidate in set) — SCENARIO_SHOCK_MATRIX_V1.crisis_case 기준 사용(가장 보수적) + - step_3c: post_trade_MRC = sum(candidate.marginal_risk_contribution for candidate in set) / portfolio_total_risk_budget + - step_4: set_hard_constraint_pass=false인 set은 제외 (개별 candidate는 PASS했어도 조합 시 cash_floor/concentration/MRC/CVaR cap을 넘을 수 있음) + - step_5: set_transition_utility_krw = sum(candidate.transition_utility_krw for candidate in set) - combination_penalty_krw + - step_6: set_transition_utility_krw가 최대인 set 선택. 동률이면 PORTFOLIO_TRANSITION_UTILITY_V1.deterministic_fallbacks.rank_tie 규칙 재사용 + - step_7: 통과하는 set이 하나도 없으면 NO_TRADE (개별 candidate가 전부 PASS여도 조합 검증을 통과 못하면 거부) + combination_penalty_krw: + formula: complexity_penalty_rate * (len(transition_set) - 1) + note: 후보 수가 많을수록 실행 복잡도·동시 슬리피지 리스크 증가를 반영한 페널티. + output: + field: selected_transition_set + unit: list_of_candidate_id + additional_outputs: + - set_transition_utility_krw + - set_hard_constraint_pass + - rejected_sets_count + - post_trade_mrc + - post_trade_cvar95_krw + missing_policy: evaluated_candidates가 비어 있으면 selected_transition_set=[] + NO_TRADE. 빈 조합을 임의로 채우지 않는다. cvar95_loss_krw가 candidate에 없으면(SCENARIO_SHOCK_MATRIX_V1 미실행) post_trade_cvar95_krw=null이며 해당 set은 set_hard_constraint_pass 판정에서 CVaR 기준을 PARTIAL로 표기. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_transition_set_enumerator_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - evaluated_candidates + - max_set_size + output_fields: + - selected_transition_set + - set_transition_utility_krw + - set_hard_constraint_pass + - post_trade_mrc + - post_trade_cvar95_krw + golden_cases: + - V89_010_candidate_good_portfolio_bad + - V89_048_solver_failure + - V89_049_rank_tie + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + STATE_VECTOR_CONSTRUCTOR_V1: + purpose: > + holdings, cash, tax_lots, sector_graph, factor_exposures, macro_regime_probabilities를 + 단일 state_vector로 통합해 PORTFOLIO_TRANSITION_UTILITY_V1과 TRANSITION_SET_ENUMERATOR_V1이 + 동일한 포트폴리오 스냅샷을 참조하도록 한다. 부분 입력으로 state_vector를 임의 보완하지 않는다. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-A, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:implementation_todo_v8_9.P1_optimizer_and_simulation) + applicable: MODEL_GOVERNANCE_GATE에서 execution_mode 확정 직후, HARD_FILTER_CHECK 이전. + component_sources: + cash_ladder: spec/formulas/domains/cash.yaml:CASH_RATIOS_V1 + positions: spec/15_account_snapshot_contract.yaml + sector_exposure_graph: spec/formulas/domains/sector.yaml:SECTOR_EXPOSURE_GRAPH_V1 + factor_exposures: spec/risk/factor_risk.yaml + tax_lots: spec/15_account_snapshot_contract.yaml + risk_bucket_weights: spec/risk/portfolio_exposure.yaml + macro_regime_probabilities: spec/risk/market_risk_cash.yaml + goal_progress_pct: spec/13_formula_registry.yaml:formula_registry.formulas.GOAL_RETIREMENT_V1 + inputs: + - field: cash_ladder + unit: json + - field: positions + unit: list_of_object + - field: sector_exposure_graph + unit: list_of_object + - field: factor_exposures + unit: list_of_object + - field: tax_lots + unit: list_of_object + - field: risk_bucket_weights + unit: object + - field: macro_regime_probabilities + unit: object + - field: goal_progress_pct + unit: percent + output: + field: state_vector + unit: object + additional_outputs: + - state_vector_completeness_pct + - missing_components + missing_policy: 결측 component는 state_vector에서 null로 유지하고 missing_components에 기록한다. 다른 component로 추정 보완 금지. + canonical_ref: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + implementation: tools/build_state_vector_constructor_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - cash_ladder + - positions + - sector_exposure_graph + - factor_exposures + - tax_lots + - risk_bucket_weights + - macro_regime_probabilities + - goal_progress_pct + output_fields: + - state_vector + - state_vector_completeness_pct + - missing_components + golden_cases: + - V89_052_goal_far_from_target + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + REBALANCE_CADENCE_GATE_V1: + purpose: > + 주간(토/일) 및 매월 1/11/21일 점검을 의무 실행하되, 실제 리밸런싱(매수/매도 실행)은 + transition_utility_after_tax_cost가 양수이거나 hard_risk_block이 active일 때만 허용한다. + 점검 자체는 항상 emit되어 "점검을 안 했다"는 누락을 방지하지만, 결과가 기준 미달이면 NO_TRADE. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-D, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:implementation_todo_v8_9.P3_sell_and_rebalance, + rebalancing_engine_v8_9.mandatory_schedule) + applicable: 매주 토/일 또는 매월 1/11/21일. PORTFOLIO_TRANSITION_REVIEW 진입 조건. + mandatory_schedule: + weekly_days: [SATURDAY, SUNDAY] + monthly_mid_check_days: [1, 11, 21] + event_driven_triggers: [cash_floor_break, crisis_score_red_or_black, hard_concentration_breach, data_quarantine_material] + inputs: + - field: today_date + unit: date + - field: transition_utility_after_tax_cost_krw + unit: number_or_null + source: spec/formulas/domains/portfolio.yaml:PORTFOLIO_TRANSITION_UTILITY_V1 + - field: hard_risk_block_active + unit: boolean + source: spec/risk/aggregate_risk.yaml + cadence_check_rule: > + today_date가 weekly_days, monthly_mid_check_days, event_driven_triggers 중 하나라도 충족하면 + cadence_check_required=true이며 점검 결과(review_emitted)는 항상 emit한다. + rebalance_execution_rule: > + cadence_check_required=true이고 (transition_utility_after_tax_cost_krw > 0 OR hard_risk_block_active=true) + 일 때만 rebalance_execution_allowed=true. 그 외에는 review_emitted=true이지만 rebalance_execution_allowed=false + (점검은 했지만 NO_TRADE). + output: + field: rebalance_execution_allowed + unit: boolean + additional_outputs: + - cadence_check_required + - review_emitted + - cadence_trigger_reason + missing_policy: transition_utility_after_tax_cost_krw가 null이면 hard_risk_block_active만으로 판정. 둘 다 null이면 rebalance_execution_allowed=false + DATA_MISSING. + canonical_ref: spec/risk/aggregate_risk.yaml + implementation: tools/build_rebalance_cadence_gate_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - today_date + - transition_utility_after_tax_cost_krw + - hard_risk_block_active + output_fields: + - rebalance_execution_allowed + - cadence_check_required + - review_emitted + golden_cases: + - V89_032_no_trade_band + - V89_033_hard_block_overrides_band + - V89_053_weekly_rebalance_required + - V89_054_mid_check_required + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/formulas/domains/sector.yaml b/spec/formulas/domains/sector.yaml new file mode 100644 index 0000000..63a494c --- /dev/null +++ b/spec/formulas/domains/sector.yaml @@ -0,0 +1,137 @@ +schema_version: formula_domain.v1 +source: C:\Temp\data_feed\spec\13_formula_registry.yaml +domain: sector +meta: + note: > + governance/todo/v8_9_p1_adoption_plan.yaml P1-A. + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9 +formulas: + SECTOR_EXPOSURE_GRAPH_V1: + purpose: > + 섹터를 단일 텍스트 라벨이 아니라 L1:L2:L3:L4 canonical ID로 분류하고, ETF 구성종목을 + lookthrough하여 직접보유와 합산한 실질노출을 계산하며, AI/반도체/전력 등 테마 간 + 중복 베타를 residualize해 과집중 진단의 이중계산을 막는다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.1, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9) + canonical_sector_id_format: 'L1:L2:L3:L4, 예: EQ:TECH:SEMIS:HBM' + applicable: portfolio_exposure.concentration_caps_v8_9_supplement, PORTFOLIO_TRANSITION_UTILITY_V1.concentration_reduction_benefit_krw 계산 직전. + inputs: + - field: direct_weight_pct + unit: percent + note: 종목 직접보유 비중 + - field: etf_constituents_json + unit: json + note: 'ETF 구성종목 리스트 [{ticker, weight_pct}]. ETF_quality_gate(NAV_asof_valid, constituents_asof_valid) 통과 필요.' + - field: etf_weight_pct + unit: percent + note: 해당 ETF의 포트폴리오 내 비중 + - field: sector_id + unit: string + note: canonical_sector_id_format 준수 + - field: peer_sector_betas + unit: list_of_ratio + note: 동일 macro_driver(AI_capex, semiconductor 등)를 공유하는 다른 섹터의 베타 목록 + expression: + lookthrough_etf_weight_pct: "sum(constituent.weight_pct * etf_weight_pct / 100 for constituent in etf_constituents_json if constituent.sector_id == sector_id)" + sector_family_total_pct: "direct_weight_pct + lookthrough_etf_weight_pct" + factor_beta_residualized: "factor_beta_raw - sum(shared_variance_with(peer) for peer in peer_sector_betas if peer.macro_driver == self.macro_driver)" + output: + field: sector_family_total_pct + unit: percent + additional_outputs: + - lookthrough_etf_weight_pct + - factor_beta_residualized + - theme_overlap_pct + gates: + - if: sector_family_total_pct > concentration_caps_v8_9_supplement.top3_combined_cap_pct.hard_cap_pct + action: HARD_CONCENTRATION_BLOCK + missing_policy: + etf_constituents_json: ETF_BUY_BLOCKED — constituents_missing (v8.9 V89_016). lookthrough를 0으로 추정 금지. + peer_sector_betas: factor_beta_residualized를 raw 값 그대로 사용하고 PARTIAL 표기. 0으로 추정 금지. + canonical_ref: spec/risk/portfolio_exposure.yaml:duplicate_exposure_rule + implementation: tools/build_sector_exposure_graph_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - direct_weight_pct + - etf_constituents_json + - etf_weight_pct + - sector_id + - peer_sector_betas + output_fields: + - sector_family_total_pct + - lookthrough_etf_weight_pct + - factor_beta_residualized + golden_cases: + - V89_044_sector_overlap + - V89_045_ETF_direct_overlap + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + LEADER_LIFECYCLE_GATE_V1: + purpose: > + 종목의 시장 주도력을 CAPTAIN/CORE_LEADER/ENABLER/CYCLICAL_BETA/LAGGARD/DISTRIBUTION_RISK + 6개 role로 분류하고, 승급·강등 조건을 결정론적으로 평가한다. LLM이 '주도주라서 산다'는 + 서사로 role을 임의 부여하는 것을 금지한다. + (governance/todo/v8_9_p1_adoption_plan.yaml P1-A.2, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:sector_graph_engine_v8_9.leader_lifecycle) + applicable: SECTOR_EXPOSURE_GRAPH_V1 산출 직후. PORTFOLIO_TRANSITION_UTILITY_V1 candidate_action 생성 전. + roles: [CAPTAIN, CORE_LEADER, ENABLER, CYCLICAL_BETA, LAGGARD, DISTRIBUTION_RISK] + inputs: + - field: relative_strength_leads_sector + unit: boolean + - field: volume_quality_confirmed + unit: boolean + - field: above_ma60_or_reclaim_confirmed + unit: boolean + - field: earnings_revision_status + unit: 'enum: positive | neutral | negative' + - field: institutional_flow_status + unit: 'enum: accumulation | neutral | distribution' + - field: current_role + unit: enum + note: 직전 평가에서 결정된 role. 최초 평가 시 LAGGARD로 시작. + promotion_requires_all: + - relative_strength_leads_sector == true + - volume_quality_confirmed == true + - above_ma60_or_reclaim_confirmed == true + - earnings_revision_status != negative + - institutional_flow_status != distribution + demotion_triggers_any: + - break_ma60_with_distribution: "above_ma60_or_reclaim_confirmed == false AND institutional_flow_status == distribution" + - underperform_sector_20d: relative_strength_leads_sector == false (20거래일 연속) + - earnings_revision_negative: earnings_revision_status == negative + - crowded_flow_reversal: institutional_flow_status == distribution AND current_role IN [CAPTAIN, CORE_LEADER] + role_transition_table: + promotion_path: [LAGGARD, CYCLICAL_BETA, ENABLER, CORE_LEADER, CAPTAIN] + demotion_path: [CAPTAIN, CORE_LEADER, ENABLER, CYCLICAL_BETA, LAGGARD, DISTRIBUTION_RISK] + rule: 승급은 promotion_requires_all 충족 시 promotion_path 다음 단계로 1단계만 이동. 강등은 demotion_triggers_any 발동 시 즉시 DISTRIBUTION_RISK로 직행(단계적 강등 아님 — 보수적 원칙). + output: + field: leader_role + unit: enum + additional_outputs: + - role_transition_reason + - role_changed + gates: + - if: leader_role == DISTRIBUTION_RISK + action: NEW_BUY_BLOCKED + missing_policy: 입력 필드 중 하나라도 null이면 role 유지(current_role) + role_transition_reason=DATA_MISSING. 임의 승급/강등 금지. + canonical_ref: spec/strategy/leader_scan.yaml + implementation: tools/build_sector_exposure_graph_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - relative_strength_leads_sector + - volume_quality_confirmed + - above_ma60_or_reclaim_confirmed + - earnings_revision_status + - institutional_flow_status + - current_role + output_fields: + - leader_role + - role_transition_reason + golden_cases: + - V89_046_leader_distribution + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/formulas/domains/simulation.yaml b/spec/formulas/domains/simulation.yaml new file mode 100644 index 0000000..52268cc --- /dev/null +++ b/spec/formulas/domains/simulation.yaml @@ -0,0 +1,205 @@ +schema_version: formula_domain.v1 +source: C:\Temp\data_feed\spec\13_formula_registry.yaml +domain: simulation +meta: + note: > + governance/todo/v8_9_p0_adoption_plan.yaml P0-3.1. + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:forecast_and_simulation_engine_v8_9 + 배경: spec/29_backtest_harness_contract.yaml가 이미 "T+20 실현 표본 0건, walk_forward=insufficient_data"를 + 명시한다(AGENTS.md §6b). 이 도메인 파일은 분포를 날조하지 않고, 표본이 minimum_sample_rules를 충족할 때만 + 실제 bootstrap 계산 경로를 열고 미달이면 DATA_MISSING/WATCH_ONLY를 결정론적으로 반환하는 계약이다. +formulas: + FORECAST_SIMULATION_ENGINE_V1: + purpose: > + 개별 종목의 점 추정 기대수익률이 아니라 레짐별 손익분포에서 CE70(30%분위)·CE90(10%분위)· + CVaR95(95% 신뢰구간 꼬리손실 평균)를 산출한다. 표본 부족 시 가짜 분포를 만들지 않고 + WATCH_ONLY 또는 DATA_MISSING으로 정직하게 반환한다. + applicable: PORTFOLIO_TRANSITION_UTILITY_V1의 ce70_net_profit_krw 입력 직전. + inputs: + - field: net_profit_distribution_after_tax_fee_slippage + source: spec/29_backtest_harness_contract.yaml:current_metrics + unit: list_of_KRW + note: 세후·비용 차감 손익 표본. 현재 t20_op_rate.n_sample=0 (insufficient_data). + - field: sample_count_total + unit: count + - field: sample_count_same_regime + unit: count + - field: execution_mode + unit: enum + note: AUDIT_ONLY | SHADOW | PILOT | LIVE_LIMITED | LIVE_FULL + minimum_sample_rules: + AUDIT_ONLY: + sample_count_total_min: 0 + sample_count_same_regime_min: 0 + note: no minimum; report missing samples only (documentation only, no live order) + SHADOW: + sample_count_total_min: 30 + sample_count_same_regime_min: 10 + PILOT: + sample_count_total_min: 80 + sample_count_same_regime_min: 20 + LIVE_LIMITED: + sample_count_total_min: 150 + sample_count_same_regime_min: 30 + LIVE_FULL: + sample_count_total_min: 300 + sample_count_same_regime_min: 50 + agents_md_cross_check: "AGENTS.md §6b: Live T+20 표본 30건 미만이면 active/PASS_100 승격 금지" + gate_logic: + - if: sample_count_total < minimum_sample_rules[execution_mode].sample_count_total_min + action: WATCH_ONLY + output: "ce70_net_profit_krw=null, ce90_net_profit_krw=null, cvar95_loss_krw=null" + - if: sample_count_same_regime < minimum_sample_rules[execution_mode].sample_count_same_regime_min + action: WATCH_ONLY + output: "ce70_net_profit_krw=null, ce90_net_profit_krw=null, cvar95_loss_krw=null" + - if: sample_count_total >= minimum AND sample_count_same_regime >= minimum + action: COMPUTE + expression: + ce70_net_profit_krw: quantile(net_profit_distribution_after_tax_fee_slippage, 0.30) + ce90_net_profit_krw: quantile(net_profit_distribution_after_tax_fee_slippage, 0.10) + cvar95_loss_krw: mean(losses beyond 95th percentile loss threshold in net_profit_distribution_after_tax_fee_slippage) + estimator_allowlist: + - walk_forward_bootstrap + - regime_matched_bootstrap + forbidden_estimators: + - LLM_guess_return + - single_point_target_price_without_distribution + - backtest_without_cost_or_leakage_test + output: + field: ce70_net_profit_krw + unit: KRW_or_null + additional_outputs: + - ce90_net_profit_krw + - cvar95_loss_krw + - sample_count_total + - sample_count_same_regime + - gate + missing_policy: + net_profit_distribution_after_tax_fee_slippage: 표본 없으면 모든 출력 null + gate=WATCH_ONLY. 0으로 대체 금지. + canonical_ref: spec/29_backtest_harness_contract.yaml:current_metrics.walk_forward + implementation: tools/build_forecast_simulation_engine_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - net_profit_distribution_after_tax_fee_slippage + - sample_count_total + - sample_count_same_regime + - execution_mode + output_fields: + - ce70_net_profit_krw + - ce90_net_profit_krw + - cvar95_loss_krw + - sample_count_total + - sample_count_same_regime + - gate + golden_cases: + - V89_013_missing_CVaR + - V89_014_same_regime_sample_low + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + SCENARIO_SHOCK_MATRIX_V1: + purpose: > + base_case 분포 하나만으로 위기 취약성을 가리지 않도록, FORECAST_SIMULATION_ENGINE_V1의 + net_profit_distribution_after_tax_fee_slippage에 5개 스트레스 시나리오를 결정론적으로 + 적용해 각 시나리오별 CE70/CVaR95를 산출한다. 거짓 분포 생성을 금지하며, base distribution이 + 없으면 모든 시나리오가 DATA_MISSING이다. + (governance/todo/v8_9_p2_adoption_plan.yaml P2-A, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:forecast_and_simulation_engine_v8_9.simulation_grid) + applicable: FORECAST_SIMULATION_ENGINE_V1의 gate=PASS(분포 산출 성공) 직후. + scenario_definitions: + base_case: {shock_multiplier: 1.0, note: current_regime_probability_weighted, source_distribution: as-is} + adverse_case: {shock_multiplier: 1.5, note: volatility_up_and_breadth_down — 손실 구간 증폭} + liquidity_drought_case: {shock_multiplier: 1.3, capacity_derate_pct: 40, note: spread_widening_and_capacity_down} + crisis_case: {shock_multiplier: 2.0, correlation_to_one: true, note: correlation_to_one_and_gap_down} + fx_shock_case: {shock_multiplier: 1.2, applies_only_to: foreign_assets, note: USDKRW adverse movement} + tax_cost_case: {shock_multiplier: 1.0, additional_cost_pct: 5, note: realized_gain_tax_and_reentry_cost_stress} + inputs: + - field: net_profit_distribution_after_tax_fee_slippage + unit: list_of_KRW + - field: scenario_id + unit: 'enum: base_case | adverse_case | liquidity_drought_case | crisis_case | fx_shock_case | tax_cost_case' + expression: > + shocked_distribution = [v * scenario.shock_multiplier if v < 0 else v / scenario.shock_multiplier + for v in net_profit_distribution_after_tax_fee_slippage] (손실만 증폭, 이익은 보수적으로 축소) + scenario_ce70_krw = quantile(shocked_distribution, 0.30) + scenario_cvar95_krw = mean(losses beyond 95th percentile loss threshold in shocked_distribution) + output: + field: scenario_results + unit: 'list_of_{scenario_id, scenario_ce70_krw, scenario_cvar95_krw}' + missing_policy: net_profit_distribution_after_tax_fee_slippage가 없으면 전체 scenario_results가 DATA_MISSING. 시나리오별로 임의 분포 생성 금지. + canonical_ref: spec/formulas/domains/simulation.yaml:FORECAST_SIMULATION_ENGINE_V1 + implementation: tools/build_scenario_shock_matrix_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - net_profit_distribution_after_tax_fee_slippage + - scenario_id + output_fields: + - scenario_results + golden_cases: + - V89_010_candidate_good_portfolio_bad + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation + WALK_FORWARD_BOOTSTRAP_V1: + purpose: > + FORECAST_SIMULATION_ENGINE_V1이 입력으로 받는 net_profit_distribution_after_tax_fee_slippage를 + 실제 historical_returns 표본에서 walk-forward(시간순 비복원, in-sample/out-of-sample 분리) 및 + regime-matched(현재 레짐과 동일한 구간만 필터) 리샘플링으로 생성한다. 가짜 분포 생성을 금지하며 + historical_returns가 없거나 표본이 1건뿐이면 DATA_MISSING. + (governance/todo/v8_9_p3_adoption_plan.yaml P3-B, + source: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:forecast_and_simulation_engine_v8_9.allowed_estimators) + applicable: FORECAST_SIMULATION_ENGINE_V1 직전 — 이 엔진의 출력이 FORECAST_SIMULATION_ENGINE_V1의 입력이 된다. + inputs: + - field: historical_returns + unit: list_of_object + note: 'spec/29_backtest_harness_contract.yaml 연동. [{date, regime_state, net_return_after_cost_pct}]. 현재 t20 n_sample=0.' + - field: current_regime_state + unit: string + - field: bootstrap_method + unit: 'enum: walk_forward | regime_matched' + - field: resample_count + unit: count + default: 1000 + note: 리샘플링 반복 횟수. historical_returns 표본이 적으면 resample_count와 무관하게 sample_count_total은 historical_returns 길이로 결정. + estimator_rule: + walk_forward: > + historical_returns를 시간순으로 정렬해 첫 70%를 in-sample, 나머지 30%를 out-of-sample로 + 고정 분리한다. out-of-sample 구간에서만 비복원 블록 리샘플링(block size=5)으로 + net_profit_distribution을 생성한다. in-sample 재사용 금지(leakage 방지). + regime_matched: > + historical_returns 중 regime_state == current_regime_state인 행만 필터링 후 + 복원추출(bootstrap with replacement)로 net_profit_distribution을 생성한다. + 필터 결과가 비어 있으면 DATA_MISSING(다른 레짐으로 대체 금지). + leakage_controls: + - asof_join_required + - future_constituent_exclusion + - calendar_alignment + output: + field: net_profit_distribution_after_tax_fee_slippage + unit: list_of_KRW_or_null + additional_outputs: + - sample_count_total + - sample_count_same_regime + - leakage_check_status + missing_policy: historical_returns 결측 또는 표본 1건 이하면 net_profit_distribution=null + gate=DATA_MISSING. 보간·추정 금지. + canonical_ref: spec/29_backtest_harness_contract.yaml:current_metrics.walk_forward + implementation: tools/build_walk_forward_bootstrap_v1.py + owner: quant_team + lifecycle_state: shadow + input_fields: + - historical_returns + - current_regime_state + - bootstrap_method + - resample_count + output_fields: + - net_profit_distribution_after_tax_fee_slippage + - sample_count_total + - sample_count_same_regime + golden_cases: + - V89_014_same_regime_sample_low + - V89_048_solver_failure + activation_threshold: + min_t20_sample: 30 + retirement_condition: performance_degradation diff --git a/spec/risk/portfolio_exposure.yaml b/spec/risk/portfolio_exposure.yaml index bcb9934..e03b092 100644 --- a/spec/risk/portfolio_exposure.yaml +++ b/spec/risk/portfolio_exposure.yaml @@ -423,6 +423,30 @@ portfolio_exposure_framework: - "cluster_state 필드 없이 반도체 종목 SELL 판단 금지" - "CLUSTER_HOLD_ONLY 상태를 LLM이 임의로 CLUSTER_OPEN으로 변경 금지" + # ── [P0-1.1 / governance/todo/v8_9_p0_adoption_plan.yaml] v8.9 제안 신규 cap 필드 ── + # 배경: v8.9 제안서(suggest/quant_investment_engine_v8_9...)는 단일종목·top3 cap을 제시했으나 + # 기존 spec에는 이름 지정 예외(삼성전자/SK하이닉스 spec/05_position_sizing.yaml, + # 반도체 클러스터 spec/10_portfolio_rules.yaml)만 있고 "이름 없는 일반 종목"의 + # 기본 상한과 top3 합산 상한이 없었다. 기존 이름 지정 예외를 덮어쓰지 않고 + # 비어있던 두 칸만 채운다. + concentration_caps_v8_9_supplement: + note: > + 이 항목은 기존 regime/cluster_state 연동 cash_floor·반도체 cap을 대체하지 않는다. + spec/05_position_sizing.yaml의 삼성전자(soft 45%/hard 48%)·SK하이닉스(soft 22%/hard 25%) + named exception과 spec/risk/portfolio_exposure.yaml:target_allocation_structure. + tactical_satellite_bucket.concentration_rule(단일 위성 7%)이 항상 우선한다. + default_single_stock_cap_pct: + applies_when: "named exception(삼성전자·SK하이닉스 등 spec/05_position_sizing.yaml) 또는 위성 7% 규칙이 없는 종목" + soft_cap_pct: 15 + hard_cap_pct: 20 + over_cap_action: "no_additional_buy; evaluate partial_trim_or_core_ETF_transition_after_tax_cost" + top3_combined_cap_pct: + definition: "포트폴리오 내 비중 상위 3개 종목(직접보유, named exception 포함)의 합산 비중" + soft_cap_pct: 50 + hard_cap_pct: 65 + over_cap_action: "no_additional_buy_increasing_concentration; transition_optimizer가 신규 매수 후보를 hard_constraint_fail로 거부" + source_proposal: suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml:portfolio_policy_v8_9.concentration_limits + cash_floor: # ── 구조 수정: duplicate_exposure_rule 자식(4칸) → portfolio_exposure_framework 직속(2칸) (2026-05-14) normal: "총자산 7~10%" overheated_or_event_week: "총자산 10~15%" diff --git a/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.py b/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.py new file mode 100644 index 0000000..08975ac --- /dev/null +++ b/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'EXECUTION_CAPACITY_LADDER_V1' +SCHEMA_ID = 'schema://formula/EXECUTION_CAPACITY_LADDER_V1' +SCHEMA_PATH = 'schemas/generated/execution_capacity_ladder_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.schema.json b/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.schema.json new file mode 100644 index 0000000..89da6b3 --- /dev/null +++ b/src/quant_engine/models/generated/execution_capacity_ladder_v1_schema.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/EXECUTION_CAPACITY_LADDER_V1", + "title": "EXECUTION_CAPACITY_LADDER_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "EXECUTION_CAPACITY_LADDER_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "planned_order_amount_krw", + "avg_trade_value_20d_krw", + "intraday_trade_value_krw", + "orderbook_top3_depth_krw", + "spread_bps" + ], + "x_formula_outputs": [ + "order_capacity_krw" + ] +} diff --git a/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.py b/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.py new file mode 100644 index 0000000..895f3a6 --- /dev/null +++ b/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'EXECUTION_PLAN_COMPILER_V1' +SCHEMA_ID = 'schema://formula/EXECUTION_PLAN_COMPILER_V1' +SCHEMA_PATH = 'schemas/generated/execution_plan_compiler_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.schema.json b/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.schema.json new file mode 100644 index 0000000..ea7f93e --- /dev/null +++ b/src/quant_engine/models/generated/execution_plan_compiler_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/EXECUTION_PLAN_COMPILER_V1", + "title": "EXECUTION_PLAN_COMPILER_V1", + "type": "object", + "properties": { + "formula_id": { "const": "EXECUTION_PLAN_COMPILER_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["order_capacity_krw", "revalidation_snapshot", "baseline_snapshot"], + "x_formula_outputs": ["compiled_slices"] +} diff --git a/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.py b/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.py new file mode 100644 index 0000000..fad0614 --- /dev/null +++ b/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'IMMUTABLE_DECISION_LEDGER_V1' +SCHEMA_ID = 'schema://formula/IMMUTABLE_DECISION_LEDGER_V1' +SCHEMA_PATH = 'schemas/generated/immutable_decision_ledger_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.schema.json b/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.schema.json new file mode 100644 index 0000000..c11c702 --- /dev/null +++ b/src/quant_engine/models/generated/immutable_decision_ledger_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/IMMUTABLE_DECISION_LEDGER_V1", + "title": "IMMUTABLE_DECISION_LEDGER_V1", + "type": "object", + "properties": { + "formula_id": { "const": "IMMUTABLE_DECISION_LEDGER_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["decision_id", "input_hash_bundle", "execution_mode", "candidate_ids", "selected_transition_id"], + "x_formula_outputs": ["ledger_append_status"] +} diff --git a/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.py b/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.py new file mode 100644 index 0000000..4478ac6 --- /dev/null +++ b/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'MODEL_GOVERNANCE_KILL_SWITCH_V1' +SCHEMA_ID = 'schema://formula/MODEL_GOVERNANCE_KILL_SWITCH_V1' +SCHEMA_PATH = 'schemas/generated/model_governance_kill_switch_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.schema.json b/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.schema.json new file mode 100644 index 0000000..1f09a09 --- /dev/null +++ b/src/quant_engine/models/generated/model_governance_kill_switch_v1_schema.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/MODEL_GOVERNANCE_KILL_SWITCH_V1", + "title": "MODEL_GOVERNANCE_KILL_SWITCH_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "MODEL_GOVERNANCE_KILL_SWITCH_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "data_quarantine_rate_pct", + "implementation_shortfall_ratio", + "t5_hit_rate_pct", + "t5_sample_count", + "calibration_error", + "calibration_error_limit", + "account_mdd_pct", + "account_mdd_budget_pct" + ], + "x_formula_outputs": [ + "execution_mode", + "kill_switch_triggered", + "kill_switch_reason_codes" + ] +} diff --git a/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.py b/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.py new file mode 100644 index 0000000..33d7360 --- /dev/null +++ b/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'PORTFOLIO_TRANSITION_UTILITY_V1' +SCHEMA_ID = 'schema://formula/PORTFOLIO_TRANSITION_UTILITY_V1' +SCHEMA_PATH = 'schemas/generated/portfolio_transition_optimizer_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.schema.json b/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.schema.json new file mode 100644 index 0000000..9fbea37 --- /dev/null +++ b/src/quant_engine/models/generated/portfolio_transition_optimizer_v1_schema.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/PORTFOLIO_TRANSITION_UTILITY_V1", + "title": "PORTFOLIO_TRANSITION_UTILITY_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "PORTFOLIO_TRANSITION_UTILITY_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "ce70_net_profit_krw", + "tax_fee_slippage_krw", + "cash_repair_benefit_krw", + "concentration_reduction_benefit_krw", + "turnover_penalty_krw" + ], + "x_formula_outputs": [ + "transition_utility_krw", + "acceptance_margin_krw", + "selected_transition" + ] +} diff --git a/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.py b/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.py new file mode 100644 index 0000000..4ce48ef --- /dev/null +++ b/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'REBALANCE_CADENCE_GATE_V1' +SCHEMA_ID = 'schema://formula/REBALANCE_CADENCE_GATE_V1' +SCHEMA_PATH = 'schemas/generated/rebalance_cadence_gate_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.schema.json b/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.schema.json new file mode 100644 index 0000000..a99f469 --- /dev/null +++ b/src/quant_engine/models/generated/rebalance_cadence_gate_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/REBALANCE_CADENCE_GATE_V1", + "title": "REBALANCE_CADENCE_GATE_V1", + "type": "object", + "properties": { + "formula_id": { "const": "REBALANCE_CADENCE_GATE_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["today_date", "transition_utility_after_tax_cost_krw", "hard_risk_block_active"], + "x_formula_outputs": ["rebalance_execution_allowed", "cadence_check_required", "review_emitted"] +} diff --git a/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.py b/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.py new file mode 100644 index 0000000..ac57590 --- /dev/null +++ b/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'SCENARIO_SHOCK_MATRIX_V1' +SCHEMA_ID = 'schema://formula/SCENARIO_SHOCK_MATRIX_V1' +SCHEMA_PATH = 'schemas/generated/scenario_shock_matrix_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.schema.json b/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.schema.json new file mode 100644 index 0000000..32c45d7 --- /dev/null +++ b/src/quant_engine/models/generated/scenario_shock_matrix_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/SCENARIO_SHOCK_MATRIX_V1", + "title": "SCENARIO_SHOCK_MATRIX_V1", + "type": "object", + "properties": { + "formula_id": { "const": "SCENARIO_SHOCK_MATRIX_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["net_profit_distribution_after_tax_fee_slippage", "scenario_id"], + "x_formula_outputs": ["scenario_results"] +} diff --git a/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.py b/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.py new file mode 100644 index 0000000..1dede12 --- /dev/null +++ b/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'SECTOR_EXPOSURE_GRAPH_V1' +SCHEMA_ID = 'schema://formula/SECTOR_EXPOSURE_GRAPH_V1' +SCHEMA_PATH = 'schemas/generated/sector_exposure_graph_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.schema.json b/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.schema.json new file mode 100644 index 0000000..83fb22f --- /dev/null +++ b/src/quant_engine/models/generated/sector_exposure_graph_v1_schema.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/SECTOR_EXPOSURE_GRAPH_V1", + "title": "SECTOR_EXPOSURE_GRAPH_V1", + "type": "object", + "properties": { + "formula_id": { + "const": "SECTOR_EXPOSURE_GRAPH_V1" + }, + "owner": { + "type": "string" + }, + "status": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "string" + } + }, + "outputs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "formula_id", + "owner", + "status", + "inputs", + "outputs" + ], + "x_formula_inputs": [ + "direct_weight_pct", + "etf_constituents_json", + "etf_weight_pct", + "sector_id", + "peer_sector_betas" + ], + "x_formula_outputs": [ + "sector_family_total_pct", + "lookthrough_etf_weight_pct", + "factor_beta_residualized", + "leader_role" + ] +} diff --git a/src/quant_engine/models/generated/state_vector_constructor_v1_schema.py b/src/quant_engine/models/generated/state_vector_constructor_v1_schema.py new file mode 100644 index 0000000..dad2818 --- /dev/null +++ b/src/quant_engine/models/generated/state_vector_constructor_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'STATE_VECTOR_CONSTRUCTOR_V1' +SCHEMA_ID = 'schema://formula/STATE_VECTOR_CONSTRUCTOR_V1' +SCHEMA_PATH = 'schemas/generated/state_vector_constructor_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/state_vector_constructor_v1_schema.schema.json b/src/quant_engine/models/generated/state_vector_constructor_v1_schema.schema.json new file mode 100644 index 0000000..5fccbd4 --- /dev/null +++ b/src/quant_engine/models/generated/state_vector_constructor_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/STATE_VECTOR_CONSTRUCTOR_V1", + "title": "STATE_VECTOR_CONSTRUCTOR_V1", + "type": "object", + "properties": { + "formula_id": { "const": "STATE_VECTOR_CONSTRUCTOR_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["cash_ladder", "positions", "sector_exposure_graph", "factor_exposures", "tax_lots", "risk_bucket_weights", "macro_regime_probabilities", "goal_progress_pct"], + "x_formula_outputs": ["state_vector", "state_vector_completeness_pct", "missing_components"] +} diff --git a/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.py b/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.py new file mode 100644 index 0000000..2feedef --- /dev/null +++ b/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'TRANSITION_SET_ENUMERATOR_V1' +SCHEMA_ID = 'schema://formula/TRANSITION_SET_ENUMERATOR_V1' +SCHEMA_PATH = 'schemas/generated/transition_set_enumerator_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.schema.json b/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.schema.json new file mode 100644 index 0000000..8f232e8 --- /dev/null +++ b/src/quant_engine/models/generated/transition_set_enumerator_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/TRANSITION_SET_ENUMERATOR_V1", + "title": "TRANSITION_SET_ENUMERATOR_V1", + "type": "object", + "properties": { + "formula_id": { "const": "TRANSITION_SET_ENUMERATOR_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["evaluated_candidates", "max_set_size"], + "x_formula_outputs": ["selected_transition_set"] +} diff --git a/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.py b/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.py new file mode 100644 index 0000000..ac2d825 --- /dev/null +++ b/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'WALK_FORWARD_BOOTSTRAP_V1' +SCHEMA_ID = 'schema://formula/WALK_FORWARD_BOOTSTRAP_V1' +SCHEMA_PATH = 'schemas/generated/walk_forward_bootstrap_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.schema.json b/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.schema.json new file mode 100644 index 0000000..2c7b55e --- /dev/null +++ b/src/quant_engine/models/generated/walk_forward_bootstrap_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/WALK_FORWARD_BOOTSTRAP_V1", + "title": "WALK_FORWARD_BOOTSTRAP_V1", + "type": "object", + "properties": { + "formula_id": { "const": "WALK_FORWARD_BOOTSTRAP_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["historical_returns", "current_regime_state", "bootstrap_method"], + "x_formula_outputs": ["net_profit_distribution_after_tax_fee_slippage", "sample_count_total", "sample_count_same_regime"] +} diff --git a/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.py b/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.py new file mode 100644 index 0000000..3ac1586 --- /dev/null +++ b/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.py @@ -0,0 +1,33 @@ +"""Auto-generated schema model descriptor.""" +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path +from typing import Any + +SCHEMA_TITLE = 'WEEKLY_LEGACY_TRANSFER_PLAN_V1' +SCHEMA_ID = 'schema://formula/WEEKLY_LEGACY_TRANSFER_PLAN_V1' +SCHEMA_PATH = 'schemas/generated/weekly_legacy_transfer_plan_v1.schema.json' +SCHEMA_PROPERTIES = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] +SCHEMA_REQUIRED = ['formula_id', 'owner', 'status', 'inputs', 'outputs'] + +@dataclass(frozen=True) +class SchemaModel: + title: str + schema_id: str + path: str + properties: list[str] + required: list[str] + +def load_schema() -> dict[str, Any]: + return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8')) + +def describe() -> SchemaModel: + return SchemaModel( + title=SCHEMA_TITLE, + schema_id=SCHEMA_ID, + path=SCHEMA_PATH, + properties=list(SCHEMA_PROPERTIES), + required=list(SCHEMA_REQUIRED), + ) diff --git a/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.schema.json b/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.schema.json new file mode 100644 index 0000000..1ad9cc9 --- /dev/null +++ b/src/quant_engine/models/generated/weekly_legacy_transfer_plan_v1_schema.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema://formula/WEEKLY_LEGACY_TRANSFER_PLAN_V1", + "title": "WEEKLY_LEGACY_TRANSFER_PLAN_V1", + "type": "object", + "properties": { + "formula_id": { "const": "WEEKLY_LEGACY_TRANSFER_PLAN_V1" }, + "owner": { "type": "string" }, + "status": { "type": "string" }, + "inputs": { "type": "array", "items": { "type": "string" } }, + "outputs": { "type": "array", "items": { "type": "string" } } + }, + "required": ["formula_id", "owner", "status", "inputs", "outputs"], + "x_formula_inputs": ["weekly_legacy_to_cma_transfer_plan_krw", "transfer_confirmed", "transfer_confirmed_amount_krw"], + "x_formula_outputs": ["deployable_cash_contribution_krw", "plan_status"] +} diff --git a/suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml b/suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml new file mode 100644 index 0000000..e481640 --- /dev/null +++ b/suggest/quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored.yaml @@ -0,0 +1,1162 @@ +meta: + title: quant_investment_engine_v8_9_portfolio_optimizer_canonical_refactored + version: 8.9.0 + language: ko-KR + timezone: Asia/Seoul + currency: KRW + generated_at: '2026-06-17T00:00:00+09:00' + source_file: quant_investment_engine_v8_8_portfolio_optimizer_canonical.yaml + source_sha256: 5fad7a17cb9156ef44218a68bf92397f0117c99de920719ab3de5ee676e7fc38 + source_top_level_key_count: 99 + source_version: 8.8.0 + target_asset_krw: 500000000 + account_style: 장기 포트폴리오 전환 계좌; 단기 트레이딩 계좌 아님 + default_decision_unit: weekly + mandatory_cadence: + weekly_rebalance: 매주 토요일 또는 일요일 리밸런싱 제안 필수 + monthly_mid_check_days: + - 1 + - 11 + - 21 + d2_cash_rule: D+2 현금은 방어선 충족에는 포함하되 신규매수 가용현금과 분리한다. + upgrade_from: 8.8.0 + upgrade_thesis: v8.9는 신호 추천 엔진이 아니라 계좌 생존, 목표금액 접근도, 데이터 출처, 세후 비용, 실행 가능성, 섹터 노출, 시뮬레이션 하방위험을 하나의 포트폴리오 전환 최적화 문제로 푼다. + 기본값은 NO_TRADE다. + legal_notice: 알고리즘 설계 명세이며 개별 투자자문 또는 실시간 주문 지시가 아니다. 실주문은 검증된 런타임 패킷과 사용자 확인을 필요로 한다. +brutal_audit_of_v8_8: + verdict: 원칙은 강하지만 실행 커널은 아직 비대하고 파편화되어 있다. 좋은 게이트가 많아도 단일 상태 벡터, 단일 산식 레지스트리, 단일 최종 패킷으로 컴파일되지 않으면 실전에서는 서로 다른 모듈이 서로를 무력화한다. + critical_findings: + - id: F01_version_accumulation_bloat + finding: v8.1~v8.8의 과거 섹션이 99개 최상위 키로 누적되어 canonical과 archive의 경계가 흐리다. + fix: active_runtime_contract와 archived_reference를 분리하고 v8.9 실행 경로만 authoritative로 둔다. + - id: F02_optimizer_objective_not_executable_enough + finding: 포트폴리오 전환 최적화 목적함수는 있으나 상태 벡터, 후보 집합, 제약 위반 시 tie-breaker, solver fallback이 부족하다. + fix: state_vector, candidate_action_schema, hard_veto_order, deterministic_tie_breaker, fallback_no_trade를 명시한다. + - id: F03_distribution_estimator_missing + finding: CE70/CE90/CVaR를 요구하지만 어떤 윈도우, 표본, 부트스트랩, 레짐 매칭으로 추정하는지 약하다. + fix: walk_forward_bootstrap, same_regime_min_samples, residual_stress_overlay, leakage tests를 의무화한다. + - id: F04_sector_collinearity_not_removed + finding: AI, 반도체, 전력, 산업재, 방산 등 테마가 서로 다른 이름으로 중복 베타를 만들 수 있다. + fix: sector factor residualization과 exposure graph covariance penalty를 추가한다. + - id: F05_cash_defense_vs_buying_power_incomplete + finding: D+2 현금 방어 인정 원칙은 있으나 캘린더, 미체결 주문, 세금 예약금, 환전 버퍼 반영이 약하다. + fix: cash ladder를 five-bucket으로 분리하고 deployable_cash를 별도 산식으로 잠근다. + - id: F06_sell_engine_still_narrative_heavy + finding: 매도 목적은 분류되어 있지만 현금복구/집중도완화/손실회피/세금최적화 간 우선순위가 완전히 선형화되지 않았다. + fix: sell waterfall과 Pareto lot selector를 도입한다. + - id: F07_no_trade_band_not_linked_to_tax_drag + finding: 리밸런싱 밴드는 있으나 세후 비용과 회전율 예산이 목표함수에 충분히 강제되지 않는다. + fix: rebalance_trigger를 transition_utility_after_tax가 양수일 때만 통과시키고, 위험 블록 예외만 허용한다. + - id: F08_execution_reality_needs_broker_tick_calendar + finding: 분할지정가 원칙은 있으나 종목별 호가단위, 장중 변동성, 호가공백, 휴장/결제 캘린더가 스키마화되지 않았다. + fix: broker_microstructure_packet을 필수 입력으로 추가한다. + - id: F09_model_governance_needs_kill_switch + finding: 승격/강등 기준은 있으나 실시간 성능 악화 시 전체 전략 또는 섹터 모델을 강제 중지하는 kill switch가 약하다. + fix: drawdown, hit-rate, calibration, slippage, quarantine rate 기반 자동 강등/중지 조건을 추가한다. + - id: F10_llm_reporter_may_still_soften_blocks + finding: 숫자 창작 금지는 강하지만 보고서 문장이 BLOCK을 완화하는 표현을 만들 가능성이 남아 있다. + fix: renderer must copy final_action/reason_code verbatim; narrative cannot upgrade action. + non_negotiable_upgrade_principle: 좋은 후보를 더 찾는 것보다 나쁜 전환을 확실히 막는 것이 우선이다. 모든 매수·매도는 단일 포트폴리오 전환 패킷에서 재평가한다. +active_runtime_contract_v8_9: + authoritative_sections: + - canonical_authority_and_compiler_v8_9 + - runtime_packet_schema_v8_9 + - formula_registry_v8_9 + - portfolio_transition_optimizer_v8_9 + - risk_controls_v8_9 + - rebalancing_engine_v8_9 + - execution_plan_compiler_v8_9 + - decision_output_contract_v8_9 + archived_reference_sections: v8.1~v8.8 historical modules are reference-only. They cannot override v8.9 active_runtime_contract. + hard_rule: 동일 항목에 대해 v8.9와 과거 섹션이 충돌하면 v8.9가 항상 우선한다. + default_final_action: NO_TRADE + llm_role: 계산 금지. canonical packet의 수치와 reason_code를 해석·렌더링하되 새 가격·수량·점수·확률을 만들지 않는다. +canonical_authority_and_compiler_v8_9: + purpose: 여러 산출물과 문서의 신호를 하나의 deterministic DecisionPacket으로 컴파일한다. + source_precedence_order: + - spec/00_execution_contract.yaml + - runtime/active_artifact_manifest.yaml + - Temp/final_decision_packet_active.json + - spec/13_formula_registry.yaml + - spec/02_data_contract.yaml + - spec/15_account_snapshot_contract.yaml + - spec/risk/aggregate_risk.yaml + - spec/risk/portfolio_exposure.yaml + - artifacts/canonical/*.json + - Temp/operational_report.json + - renderer/report text + compile_steps: + - S1_load_and_hash_all_authoritative_sources + - S2_validate_schema_and_required_fields + - S3_resolve_entity_aliases_and_sector_ids + - S4_build_portfolio_state_vector + - S5_attach_numeric_provenance_to_every_number + - S6_quarantine_missing_or_conflicting_inputs + - S7_generate_candidate_actions + - S8_evaluate_portfolio_transition_sets + - S9_select_final_transition_or_NO_TRADE + - S10_emit_immutable_decision_packet_and_renderer_payload + deterministic_conflict_resolution: + priority_order: + - DATA_INVALID + - EXECUTION_MODE_BLOCK + - CASH_FLOOR_BLOCK + - CRISIS_SURVIVAL_BLOCK + - HARD_CONCENTRATION_BLOCK + - LIQUIDITY_CAPACITY_BLOCK + - NEGATIVE_TRANSITION_UTILITY + - NO_TRADE_BAND + - WATCH + - ALLOW + tie_breaker: + - higher_survival_score + - lower_turnover + - lower_tax_drag + - lower_marginal_risk_contribution + - higher_data_quality_score + - lower_complexity_score + unknown_or_unmapped_action: BLOCK_AND_QUARANTINE + prohibited_patterns: + - renderer_overrides_canonical_action + - LLM_generates_price_or_quantity + - missing_required_numeric_treated_as_zero + - sector_label_used_without_exposure_graph + - single_candidate_buy_recommended_without_portfolio_transition_utility +runtime_packet_schema_v8_9: + DecisionPacket_required_top_level_fields: + - packet_id + - engine_version + - generated_at + - input_hash_bundle + - execution_mode + - account_state + - market_state + - data_quality + - portfolio_state_vector + - candidate_actions + - transition_set_evaluations + - selected_transition + - hard_blocks + - reason_codes + - renderer_payload + account_state_required_fields: + - total_asset_krw + - target_asset_krw + - goal_progress_pct + - immediate_cash_krw + - d1_cash_krw + - d2_cash_krw + - deployable_cash_krw + - survival_cash_required_krw + - open_order_reserve_krw + - estimated_tax_fee_reserve_krw + - weekly_external_cashflow_krw + - weekly_legacy_to_cma_transfer_plan_krw + portfolio_state_vector_required_fields: + - positions + - weights_by_asset + - weights_by_sector_family + - weights_by_risk_bucket + - top3_weight_pct + - single_name_max_pct + - semiconductor_direct_plus_etf_pct + - cash_ladder + - pre_trade_CVaR95_krw + - pre_trade_MDD_estimate_krw + - margin_of_safety_score + candidate_action_required_fields: + - candidate_id + - asset_id + - action_type + - trade_intent + - planned_amount_krw + - source_signal_ids + - numeric_provenance_status + - pre_trade_weight_pct + - post_trade_weight_pct + - expected_utility_inputs + - execution_capacity_inputs + - tax_lot_inputs + - reason_codes + transition_set_required_fields: + - transition_id + - included_candidate_ids + - post_trade_cash_floor_pct + - post_trade_exposures + - post_trade_CVaR95_krw + - transition_utility_krw + - total_tax_fee_slippage_krw + - turnover_pct + - hard_constraint_pass + - soft_penalty_total_krw + - selection_rank + - reject_reason + null_policy: required numeric field가 null이면 해당 후보 또는 전환은 QUARANTINE. 단, scenario_only 필드는 주문 후보 승격에 사용하지 않는다. +data_provenance_firewall_v8_9: + purpose: 모든 숫자 입력을 source/formula/asof/unit/hash로 추적하여 홀루시네이션과 stale data를 차단한다. + numeric_field_contract: + required_metadata: + - value + - unit + - source_id + - source_asof + - formula_id + - input_hash + - confidence_score + - is_runtime_authoritative + forbidden: + - value_only_number + - source_missing + - formula_missing + - asof_missing + - LLM_estimated_number + on_failure: NUMERIC_QUARANTINE + freshness_sla: + price_intraday_minutes_max: 20 + holding_snapshot_hours_max: 24 + cash_snapshot_hours_max: 24 + sector_constituents_days_max: 7 + ETF_NAV_days_max: 2 + fundamental_quarterly_days_max: 120 + macro_daily_days_max: 3 + macro_monthly_days_max: 45 + broker_orderbook_minutes_max: 5 + data_quality_score_formula: DQS = 100 - stale_penalty - missing_penalty - conflict_penalty - lineage_penalty - unit_mismatch_penalty + grade_action: + DQS_90_100: ALLOW_FOR_OPTIMIZER + DQS_75_89: ALLOW_WITH_HAIRCUT + DQS_60_74: WATCH_ONLY + DQS_below_60: QUARANTINE + hard_quarantine_conditions: + - price_missing_for_traded_asset + - cash_missing + - position_quantity_missing + - source_asof_conflict_material + - formula_id_not_in_registry + - ETF_constituents_missing_for_ETF_buy + - FX_rate_missing_for_foreign_trade +portfolio_policy_v8_9: + cash_floor: + normal_min_d2_inclusive_cash_pct: 12.5 + deployable_cash_must_be_positive_for_buy: true + d2_cash_counts_for_defense_not_for_full_deployment: true + dynamic_required_cash_pct_by_mode: + AGGRESSIVE_GROWTH: 10.0 + GROWTH: 12.5 + BALANCED: 20.0 + DEFENSIVE: 35.0 + CAPITAL_PRESERVATION: 50.0 + BLACK_CRISIS: 60_to_80 + hard_block: if d2_inclusive_cash_pct < required_cash_pct then new BUY/STAGED_BUY amount = 0 + concentration_limits: + single_stock_soft_cap_pct: 15 + single_stock_hard_cap_pct: 20 + single_sector_family_soft_cap_pct: 25 + single_sector_family_hard_cap_pct: 35 + top3_soft_cap_pct: 50 + top3_hard_cap_pct: 65 + semiconductor_direct_plus_etf_hard_cap_pct: 35 + over_cap_action: no_additional_buy; evaluate partial_trim_or_core_ETF_transition_after_tax_cost + long_term_target_allocation_after_repair: + index_core_us_kr_global_pct: 45_to_55 + quality_leaders_direct_or_quality_ETF_pct: 20_to_25 + sector_tactical_satellite_pct: 5_to_15 + cash_and_short_duration_defense_pct: 12.5_to_20 + gold_or_defensive_alternative_pct: 0_to_5 + legacy_single_name_overweight_pct: 0_to_20_only_if_tax_and_trend_justify + operator_cashflow_config: + monthly_salary_cashflow_krw: 2500000 + weekly_legacy_stock_to_cma_transfer_plan_krw: 4000000 + cashflow_treatment: cashflow는 목표 배분 회복 속도 계산에는 사용하되, 입금 확인 전 주문가능현금으로 계산하지 않는다. +macro_regime_engine_v8_9: + purpose: 시장 국면을 단일 라벨이 아니라 확률분포로 산출하여 포지션 사이징과 현금 요구치를 조정한다. + regime_states: + - RISK_ON_TREND + - NARROW_AI_MELTUP + - BOX_RANGE + - REACCUMULATION + - DISTRIBUTION + - RISK_OFF + - LIQUIDITY_DROUGHT + - CRISIS_RED + - CRISIS_BLACK + required_features: + - index_trend_20_60_120_200d + - market_breadth + - credit_spread + - usdkrw_trend + - rates_level_and_change + - earnings_revision + - foreign_institutional_flow + - volatility_term_structure + - liquidity_proxy + - event_calendar_risk + regime_score_formula: REGIME_SCORE = weighted_sum(normalized_features) with weights locked in formula_registry; no ad-hoc + LLM weights + hysteresis: + min_days_to_upgrade_regime: 3 + min_days_to_downgrade_to_risk_off: 1 + crisis_red_override: immediate_when_drawdown_or_liquidity_threshold_breached + black_crisis_override: immediate_when_gap_or_correlation_or_settlement_stress_breached + policy_outputs: + - risk_mode + - required_cash_pct + - buy_multiplier + - sell_urgency_multiplier + - sector_rotation_permission + - max_weekly_turnover_pct +sector_graph_engine_v8_9: + purpose: 섹터 이름 추천이 아니라 경제 노출 그래프, 가치사슬 전파, ETF 구성종목, 대장주 생명주기를 통합한다. + canonical_sector_id_format: 'L1:L2:L3:L4, 예: EQ:TECH:SEMIS:HBM' + node_types: + - asset + - issuer + - ETF + - sector_L1 + - sector_L2 + - sector_L3 + - sector_L4 + - theme + - value_chain_role + - macro_driver + - risk_factor + edge_types: + - holds + - constituent_of + - revenue_exposure + - supplier_customer + - factor_beta + - theme_overlap + - currency_exposure + - policy_exposure + mandatory_exposure_metrics: + - direct_weight_pct + - lookthrough_ETF_weight_pct + - theme_overlap_pct + - factor_beta_to_semis + - factor_beta_to_AI_capex + - sector_family_total_pct + - post_trade_exposure_pct + - stress_correlation_to_top3 + sector_rotation_score_v3: + formula: SRS100 = 0.20*relative_strength + 0.15*breadth + 0.15*leader_alignment + 0.10*earnings_revision + 0.10*smart_money_flow + + 0.10*macro_fit + 0.10*ETF_quality + 0.10*valuation_safety - crowding_penalty - overlap_penalty + hard_rule: SRS는 매수 허가가 아니라 후보 생성 점수다. portfolio_transition_optimizer를 통과해야만 주문 검토 가능하다. + data_missing_action: WATCH_ONLY + leader_lifecycle: + roles: + - CAPTAIN + - CORE_LEADER + - ENABLER + - CYCLICAL_BETA + - LAGGARD + - DISTRIBUTION_RISK + promotion_requires: + - relative_strength_leads_sector + - volume_quality_confirmed + - above_MA60_or_reclaim_confirmed + - earnings_revision_positive_or_not_negative + - institutional_flow_not_distribution + demotion_triggers: + - break_MA60_with_distribution + - underperform_sector_20d + - earnings_revision_negative + - gap_up_failure + - crowded_flow_reversal + ETF_quality_gate: + required: + - NAV_asof_valid + - constituents_asof_valid + - top10_weight_known + - tracking_error_known + - spread_and_volume_capacity_pass + block_if: + - constituents_missing + - NAV_stale + - top_holdings_conflict_with_direct_exposure_cap + - tracking_error_extreme + sector_buy_permissions_by_regime: + RISK_ON_TREND: core_ETF_and_quality_leader_allowed_if_transition_utility_positive + NARROW_AI_MELTUP: only_leaders_after_pullback_or_reclaim; no_chase_breakout_day + BOX_RANGE: mean_reversion_small_size_only; prefer_no_trade + REACCUMULATION: staged_entry_allowed_after_breadth_and_leader_confirm + DISTRIBUTION: new_buy_blocked_except_defense + RISK_OFF: new_buy_blocked; cash_repair_and_risk_exit_review + CRISIS_RED: survival_priority; risk_exit_allowed + CRISIS_BLACK: liquidity_and_cash_only +forecast_and_simulation_engine_v8_9: + purpose: 점 하나의 기대수익률이 아니라 레짐별 분포, 하방 꼬리, 비용 스트레스, 포트폴리오 상관을 추정한다. + allowed_estimators: + - walk_forward_bootstrap + - regime_matched_bootstrap + - factor_residual_model + - quantile_regression_if_validated + - scenario_shock_matrix + - Bayesian_model_average_if_calibrated + forbidden_estimators: + - LLM_guess_return + - single_point_target_price_without_distribution + - backtest_without_cost_or_leakage_test + - survivorship_biased_sample + distribution_outputs_required: + - mean_net_profit_krw + - median_net_profit_krw + - CE70_net_profit_krw + - CE90_net_profit_krw + - CVaR95_loss_krw + - probability_loss_pct + - payoff_ratio + - sample_count_total + - sample_count_same_regime + - calibration_error + - brier_score + - implementation_shortfall_stress_krw + simulation_grid: + base_case: current_regime_probability_weighted + adverse_case: volatility_up_and_breadth_down + liquidity_drought_case: spread_widening_and_capacity_down + crisis_case: correlation_to_one_and_gap_down + fx_shock_case: USDKRW adverse movement for foreign assets + tax_cost_case: realized_gain_tax_and_reentry_cost_stress + minimum_sample_rules: + AUDIT_ONLY: no minimum; report missing samples + SHADOW: + sample_count_total_min: 30 + sample_count_same_regime_min: 10 + PILOT: + sample_count_total_min: 80 + sample_count_same_regime_min: 20 + LIVE_LIMITED: + sample_count_total_min: 150 + sample_count_same_regime_min: 30 + LIVE_FULL: + sample_count_total_min: 300 + sample_count_same_regime_min: 50 + leakage_controls: + - asof_join_required + - future_constituent_exclusion + - delisted_name_inclusion_where_available + - corporate_action_adjustment + - calendar_alignment + - rebalance_delay_modeling +portfolio_transition_optimizer_v8_9: + purpose: 개별 매수/매도 팁이 아니라 전체 포트폴리오의 사후 상태를 비교하여 최선의 전환 또는 NO_TRADE를 선택한다. + default_action: NO_TRADE + state_vector: + - cash_ladder + - positions + - sector_exposure_graph + - factor_exposures + - tax_lots + - risk_bucket_weights + - macro_regime_probabilities + - data_quality_scores + - execution_capacity + - goal_progress_pct + candidate_action_types: + - BUY_CORE_ETF + - BUY_QUALITY_LEADER + - BUY_SECTOR_SATELLITE + - SELL_CASH_REPAIR + - SELL_RISK_EXIT + - SELL_PROFIT_LOCK + - SELL_DECONCENTRATION + - HOLD + - WATCH_ONLY + objective: maximize PORTFOLIO_TRANSITION_UTILITY_V8_9 subject to hard constraints + hard_constraints: + - numeric_provenance_pass == true + - execution_mode_allows_action == true + - post_trade_cash_floor_pct >= required_cash_floor_pct + - deployable_cash_krw_after_buy >= 0 + - survival_cash_bucket_used_for_buy == false + - single_stock_hard_cap_pass == true + - sector_family_hard_cap_pass == true + - top3_hard_cap_pass == true + - semiconductor_direct_plus_etf_cap_pass == true + - post_trade_CVaR95_not_worse_beyond_budget == true + - execution_capacity_pass == true + - ETF_quality_pass_for_ETF_buy == true + - no_black_crisis_new_buy == true + soft_penalties: + - turnover_penalty + - tax_drag_penalty + - slippage_penalty + - liquidity_penalty + - data_uncertainty_penalty + - crowding_penalty + - factor_overlap_penalty + - opportunity_cost_penalty + - complexity_penalty + selection_algorithm: + - 1_generate_candidate_actions_from_sector_factor_cash_sell_engines + - 2_apply_hard_vetoes_to_each_candidate + - 3_construct_transition_sets_with_cash_balanced_buys_and_sells + - 4_simulate_each_transition_across_base_adverse_liquidity_crisis_fx_tax_scenarios + - 5_compute_transition_utility_and_acceptance_margin + - 6_reject_if_any_hard_constraint_fails_or_acceptance_margin_non_positive + - 7_select_highest_robust_utility_transition + - 8_if_utility_gap_between_rank1_and_rank2_below_noise_threshold_choose_lower_turnover + - 9_if_all_rejected_emit_NO_TRADE_with_required_data_and_repair_actions + minimum_acceptance_margin_by_mode: + AUDIT_ONLY: documentation_only_no_live_order + SHADOW: transition_utility_krw > 0 and no_live_order + PILOT: transition_utility_krw > max(100000, 3*estimated_total_cost_krw) + LIVE_LIMITED: transition_utility_krw > max(300000, 4*estimated_total_cost_krw) and CE70_positive + LIVE_FULL: transition_utility_krw > max(500000, 5*estimated_total_cost_krw) and CE90_non_negative_for_new_buy + deterministic_fallbacks: + missing_optimizer_inputs: NO_TRADE_AND_QUARANTINE + solver_failure: NO_TRADE_AND_LOG_SOLVER_FAILURE + rank_tie: choose_lower_turnover_lower_tax_lower_MRC + conflicting_runtime_packets: BLOCK_AND_REQUIRE_MANIFEST_REPAIR +cash_ladder_engine_v8_9: + purpose: 방어현금, 결제예정현금, 전술현금, 기회현금, 환전버퍼를 분리하여 매수 가능 금액을 과대평가하지 않는다. + cash_buckets: + survival_cash_krw: never_deploy_for_new_buy + settlement_cash_d1_d2_krw: counts_for_defensive_floor_with_haircut; not full buying_power + tactical_cash_krw: deploy_only_after_regime_confirmation + opportunity_cash_krw: deploy_only_when_transition_utility_positive + fx_buffer_cash_krw: reserved_for_foreign_asset_and_usdkrw_shock + deployable_cash_formula: deployable_cash_krw = immediate_cash_krw + d1_cash_krw*d1_haircut + d2_cash_krw*d2_haircut - survival_cash_required_krw + - open_order_reserve_krw - estimated_tax_fee_reserve_krw - fx_buffer_required_krw + haircuts: + KR_T_PLUS_2: 0.7 + US_T_PLUS_1: 0.85 + UNKNOWN_SETTLEMENT: 0.0 + hard_rules: + - if deployable_cash_krw <= 0 then BUY_BLOCKED_DEPLOYABLE_CASH + - if d2_inclusive_cash_floor_pct < required_cash_pct then BUY_BLOCKED_CASH_FLOOR + - if open_order_reserve_missing then BUY_BLOCKED_CASH_UNCERTAIN + - survival_cash cannot be used for averaging_down_or_theme_chasing +sell_and_cash_repair_optimizer_v8_9: + purpose: 매도를 감정적 손절/익절이 아니라 현금복구, 집중도완화, 꼬리위험회피, 세후 효율의 선형 waterfall로 처리한다. + sell_intents_priority_order: + - SELL_RISK_EXIT + - SELL_CASH_REPAIR + - SELL_DECONCENTRATION + - SELL_PROFIT_LOCK + - SELL_TAX_LOSS_HARVEST + - HOLD + candidate_lot_scoring: LOT_SELL_SCORE = avoided_tail_loss + cash_repair_benefit + concentration_reduction_benefit + tax_loss_benefit + - tax_fee_slippage - missed_upside_penalty - reentry_cost + waterfall_rules: + - risk_exit can override profit hurdle when avoided_tail_loss > exit_cost and crisis gate active + - cash_repair can sell profitable overweight lots when cash_floor_broken and value_damage_cap_pass + - deconcentration trims only excess exposure first; do not sell core below target unless crisis_black + - profit_lock uses partial ladder; full exit only for structural_break, data_error_exit, crisis_black, or thesis_invalidated + - tax_loss_harvest never creates wash-like immediate reentry unless allowed by local tax rules and compliance packet + partial_trim_ladder_pct: + - 10 + - 20 + - 33 + - 50 + value_damage_cap: + normal: do not realize high-quality long-term holding loss merely to meet tactical cash if survival not threatened + defensive: allow moderate value damage only when cash_floor_broken_or_concentration_hard_cap_breached + crisis_black: survival overrides value damage cap + output_required: + - sell_priority_table + - lot_ids + - expected_cash_created_krw + - tax_fee_slippage_krw + - post_sell_cash_floor_pct + - post_sell_exposure_pct + - reason_code +rebalancing_engine_v8_9: + purpose: 주간/월중 점검을 단순 비중 맞추기가 아니라 세후 전환 효용이 양수인 경우에만 실행하는 최적화 절차로 만든다. + mandatory_schedule: + weekly_rebalance_required: true + weekly_days: + - SATURDAY + - SUNDAY + monthly_mid_check_days: + - 1 + - 11 + - 21 + event_driven_rebalance_triggers: + - cash_floor_break + - crisis_score_red_or_black + - hard_concentration_breach + - data_quarantine_material + - major_macro_event + - drawdown_gate_breach + - execution_slippage_breach + no_trade_bands: + core_index_etf_pct: 5.0 + quality_leaders_pct: 3.0 + sector_satellite_pct: 2.0 + cash_floor_pct: 0.0 + legacy_overweight_sector_pct: 0.0 + rebalance_trigger_formula: trigger = hard_risk_block_active OR (abs(current_weight-target_weight)>band AND transition_utility_after_tax_cost>hurdle) + turnover_budget: + normal_weekly_turnover_cap_pct: 5 + defensive_weekly_turnover_cap_pct: 15 + crisis_turnover_cap_pct: no_cap_for_survival_sell_but_market_order_ban_remains + tax_aware_rule: prefer low tax drag lots unless risk_exit overrides + long_term_transition_path: + phase_0_repair: cash floor >= 12.5 and hard concentration below cap + phase_1_core_build: increase index core toward 45~55% using new cashflow and risk-reducing switches + phase_2_quality_overlay: quality leaders/ETF 20~25% only after data and transition utility pass + phase_3_sector_satellite: sector tactical 5~15% with strict stop/go and shadow validation + phase_4_maintenance: rebalance only when after-cost utility positive or risk block active +execution_plan_compiler_v8_9: + purpose: 승인된 전환만 주문 문법으로 변환하며, 체결 전·중·후 재검증을 강제한다. + order_policy: + default_order_type: LIMIT_SPLIT + market_order_default: FORBIDDEN + market_order_exception: only_user_confirmed_survival_exit_with_liquidity_check + breakout_day_buy: BLOCKED_UNTIL_NEXT_SESSION_VWAP_HOLD_CONFIRM + gap_up_rule: if open_gap_pct > 3 then block_new_buy_until_vwap_hold_confirmed + averaging_down_rule: blocked below MA120 unless validated rescue plan and survival cash intact + broker_microstructure_packet_required: + - tick_size + - daily_price_limit + - avg_trade_value_20d_krw + - intraday_trade_value_krw + - spread_bps + - orderbook_top3_depth_krw + - halt_status + - short_sale_or_margin_restriction + - settlement_calendar + capacity_formula: order_capacity_krw = min(planned_order_amount_krw, avg_trade_value_20d_krw*0.003, intraday_trade_value_krw*0.01, + orderbook_top3_depth_krw*0.30) + split_order_template: + slice_1_pct: 30 + slice_2_pct: 30 + slice_3_pct: 40 + requires_revalidation_before_each_slice: true + cancel_remaining_if: + - captain_reverses_intraday + - index_drop_exceeds_threshold + - spread_widens_beyond_limit + - cash_floor_after_fill_breached + - data_quarantine_after_slice + - orderbook_capacity_collapses + post_fill_recompute_required: + - cash_floor + - deployable_cash + - sector_exposure_graph + - MRC + - transition_utility + - remaining_capacity + - tax_fee_reserve +risk_controls_v8_9: + purpose: 수익률 극대화보다 계좌 생존과 목표금액 접근도를 우선한다. + risk_layers_order: + - data_integrity + - execution_mode + - cash_floor + - crisis_state + - concentration + - CVaR_and_drawdown + - liquidity_capacity + - tax_cost + - model_confidence + - expected_utility + drawdown_gates: + account_MDD_warning_pct: 5 + account_MDD_defensive_pct: 8 + account_MDD_crisis_pct: 12 + single_position_MAE_review_pct: 7 + sector_family_drawdown_review_pct: 10 + CVaR_policy: + portfolio_CVaR95_budget_pct_of_asset: + growth: 8 + balanced: 6 + defensive: 4 + capital_preservation: 2 + hard_rule: post_trade_CVaR95 cannot worsen beyond budget unless sell_risk_exit reduces larger tail risk + kill_switches: + - data_quarantine_rate_above_5pct + - implementation_shortfall_above_2x_expected_for_10_trades + - T5_hit_rate_below_50pct_for_30_shadow_or_live_trades + - calibration_error_above_limit_for_20_trades + - unexpected_drawdown_breach + - renderer_contract_violation + risk_mode_actions: + GROWTH: normal optimizer; cash floor 12.5% + BALANCED: reduced size; prefer core ETF; satellite capped + DEFENSIVE: new satellite buys blocked; cash repair and core risk reduction + CAPITAL_PRESERVATION: new buys blocked except treasury/defense if validated + CRISIS_BLACK: survival-only sell and liquidity management +formula_registry_v8_9: + PORTFOLIO_TRANSITION_UTILITY_V8_9: sum(CE70_net_profit_krw - tax_fee_slippage_krw - CVaR_penalty_krw - drawdown_penalty_krw + - liquidity_penalty_krw - data_uncertainty_penalty_krw - factor_overlap_penalty_krw) + cash_repair_benefit_krw + concentration_reduction_benefit_krw + + goal_progress_benefit_krw - turnover_penalty_krw - complexity_penalty_krw + TRANSITION_ACCEPTANCE_MARGIN_V8_9: transition_utility_krw - max(mode_absolute_hurdle_krw, hurdle_multiple * estimated_total_cost_krw) + CE70_NET_PROFIT_KRW_V8_9: quantile(net_profit_distribution_after_tax_fee_slippage, 0.30) + CE90_NET_PROFIT_KRW_V8_9: quantile(net_profit_distribution_after_tax_fee_slippage, 0.10) + CVaR95_LOSS_KRW_V8_9: mean(losses beyond 95th percentile loss threshold in simulated net_profit_distribution) + DATA_UNCERTAINTY_PENALTY_KRW_V8_9: planned_order_amount_krw * max(0, 75 - data_quality_score)/100 * uncertainty_penalty_rate + CONCENTRATION_PENALTY_KRW_V8_9: sum(max(0, exposure_pct - cap_pct)^2 * exposure_penalty_rate_krw) + FACTOR_OVERLAP_PENALTY_KRW_V8_9: planned_order_amount_krw * max(0, post_trade_factor_beta - factor_beta_cap) * factor_overlap_rate + CASH_REPAIR_BENEFIT_KRW_V8_9: max(0, required_survival_cash_krw - current_d2_inclusive_cash_krw) * cash_shortfall_penalty_rate + * crisis_multiplier + SELL_NET_BENEFIT_KRW_V8_9: avoided_tail_loss_krw + concentration_reduction_benefit_krw + cash_repair_benefit_krw + tax_loss_benefit_krw + - tax_fee_slippage_krw - reentry_cost_krw - missed_upside_penalty_krw + DEPLOYABLE_CASH_KRW_V8_9: immediate_cash_krw + d1_cash_krw*d1_haircut + d2_cash_krw*d2_haircut - survival_cash_required_krw + - open_order_reserve_krw - estimated_tax_fee_reserve_krw - fx_buffer_required_krw + GOAL_PROGRESS_PCT_V8_9: total_asset_krw / target_asset_krw * 100 + GOAL_DISTANCE_RISK_MULTIPLIER_V8_9: if goal_progress_pct < 70 then 1.0 elif goal_progress_pct < 85 then 0.8 elif goal_progress_pct + < 95 then 0.6 else 0.4 + POSITION_SIZE_CAP_KRW_V8_9: min(risk_budget_krw, deployable_cash_krw, execution_capacity_krw, exposure_capacity_krw, fractional_kelly_guardrail_krw) + SECTOR_ROTATION_SCORE_V8_9: 0.20*relative_strength + 0.15*breadth + 0.15*leader_alignment + 0.10*earnings_revision + 0.10*smart_money_flow + + 0.10*macro_fit + 0.10*ETF_quality + 0.10*valuation_safety - crowding_penalty - overlap_penalty +decision_output_contract_v8_9: + purpose: 보고서와 주문 후보가 canonical DecisionPacket을 그대로 반영하도록 강제한다. + required_for_every_report: + - portfolio_health_summary + - global_execution_mode + - global_final_action + - top_hard_blocks + - cash_floor_status + - concentration_status + - data_quality_status + - allowed_reviews + - forbidden_actions + - required_next_data + - decision_trace_id + required_for_every_transition: + - transition_id + - final_action + - transition_utility_krw + - acceptance_margin_krw + - hard_constraint_pass + - post_trade_cash_floor_pct + - post_trade_exposure_graph + - total_tax_fee_slippage_krw + - turnover_pct + - reason_codes + - execution_plan_status + required_for_every_trade_candidate: + - candidate_id + - asset_id + - action_type + - trade_intent + - planned_amount_krw + - source_signal_ids + - CE70_net_profit_krw + - CE90_net_profit_krw + - CVaR95_loss_krw + - sell_or_buy_net_benefit_krw + - post_trade_weight_pct + - numeric_provenance_status + - final_status + - veto_reason + allowed_final_actions: + - NO_TRADE + - WATCH_ONLY + - SHADOW_LEDGER_ONLY + - CASH_REPAIR_SELL_REVIEW + - DECONCENTRATION_REVIEW + - PROFIT_LOCK_REVIEW + - RISK_EXIT_REVIEW + - PILOT_ORDER_REVIEW + - LIVE_ORDER_REVIEW + forbidden_outputs: + - BUY_NOW_without_transition_utility + - price_target_without_provenance + - quantity_without_order_capacity + - sector_recommendation_without_exposure_graph + - renderer_softening_BLOCK_to_consider_buy + - hidden_blocked_candidates +harness_validation_suite_v8_9: + purpose: v8.9가 데이터 창작, 단일종목 팁, 비용 무시, 위기 취약성, 리밸런싱 과잉을 차단하는지 검증한다. + minimum_required_pass_rate_pct: 100 + golden_cases: + - id: V89_001_yaml_parse + given: YAML loads with no duplicate active authoritative sections + expect: PASS + - id: V89_002_no_trade_default + given: complete transition utility missing + expect: NO_TRADE + - id: V89_003_no_numeric_provenance + given: candidate has price/score/quantity without source/formula/asof + expect: NUMERIC_QUARANTINE + - id: V89_004_cash_floor_broken + given: d2-inclusive cash ratio below required cash floor + expect: all new buys blocked + - id: V89_005_deployable_cash_negative + given: defense cash passes using D+2 but deployable cash <= 0 + expect: BUY_BLOCKED_DEPLOYABLE_CASH + - id: V89_006_semiconductor_over_cap + given: semiconductor direct+ETF exposure above cap + expect: semiconductor/HBM add-buy amount 0 + - id: V89_007_top3_hard_cap + given: top3 exposure > hard cap + expect: new concentration-increasing buy blocked + - id: V89_008_single_stock_hard_cap + given: post-trade single stock weight > hard cap + expect: transition rejected + - id: V89_009_sector_family_hard_cap + given: post-trade sector family exposure > hard cap + expect: transition rejected + - id: V89_010_candidate_good_portfolio_bad + given: CE70 positive but post-trade CVaR and concentration worsen + expect: transition rejected + - id: V89_011_small_edge_high_cost + given: positive CE70 but below cost multiple hurdle + expect: NO_TRADE + - id: V89_012_CE90_negative_live_full + given: LIVE_FULL candidate CE90 below zero for new buy + expect: new buy blocked + - id: V89_013_missing_CVaR + given: distribution lacks CVaR95 + expect: NUMERIC_QUARANTINE + - id: V89_014_same_regime_sample_low + given: same-regime sample below minimum + expect: WATCH_ONLY or SHADOW_ONLY + - id: V89_015_calibration_error_high + given: model calibration error above limit + expect: model demoted or candidate blocked + - id: V89_016_ETF_constituents_missing + given: ETF buy lacks constituents + expect: ETF buy blocked + - id: V89_017_ETF_NAV_stale + given: ETF NAV stale beyond SLA + expect: ETF buy blocked + - id: V89_018_fx_missing + given: foreign asset trade lacks FX data + expect: trade blocked + - id: V89_019_broker_packet_missing + given: no tick/spread/orderbook/settlement packet + expect: execution plan blocked + - id: V89_020_capacity_too_low + given: capacity formula below planned order + expect: order size capped or blocked + - id: V89_021_partial_fill + given: first slice filled + expect: recompute cash/exposure/MRC/utility before next slice + - id: V89_022_spread_widens + given: spread exceeds limit after slice + expect: cancel remaining + - id: V89_023_gap_up_chase + given: open gap > 3% + expect: new buy blocked until VWAP hold confirm + - id: V89_024_breakout_day + given: breakout day immediate buy + expect: blocked until next-session confirmation + - id: V89_025_crisis_red + given: crisis red regime + expect: new buys blocked and risk exits allowed + - id: V89_026_crisis_black + given: crisis black regime + expect: survival-only liquidity policy + - id: V89_027_risk_exit_loss_position + given: loss position has avoided tail loss > exit cost + expect: SELL_RISK_EXIT_REVIEW allowed + - id: V89_028_cash_repair_sell + given: cash floor broken and profitable overweight lot exists + expect: SELL_CASH_REPAIR_REVIEW allowed + - id: V89_029_deconcentration_trim + given: trend intact but concentration above soft/hard cap + expect: partial trim review + - id: V89_030_profit_lock + given: winner extended and sell net benefit above hurdle + expect: partial PROFIT_LOCK_REVIEW + - id: V89_031_tax_drag_too_high + given: sell benefit lower than tax/cost/reentry penalty + expect: HOLD_OR_PARTIAL_ONLY + - id: V89_032_no_trade_band + given: weight deviation inside band and no hard block + expect: NO_TRADE + - id: V89_033_hard_block_overrides_band + given: inside band but cash floor broken + expect: rebalance/cash repair trigger allowed + - id: V89_034_turnover_cap + given: normal regime turnover > cap + expect: transition rejected or resized + - id: V89_035_model_kill_switch_hit_rate + given: T5 hit rate below threshold for 30 trades + expect: automatic demotion + - id: V89_036_model_kill_switch_slippage + given: implementation shortfall > 2x expected + expect: automatic demotion + - id: V89_037_data_quarantine_rate + given: data quarantine rate > 5% + expect: kill switch or demotion + - id: V89_038_renderer_violation + given: report text upgrades BLOCK to buy language + expect: renderer invalid + - id: V89_039_operator_override + given: human override present + expect: immutable log required + - id: V89_040_shadow_mode + given: execution mode SHADOW + expect: no live order generated + - id: V89_041_audit_only_mode + given: execution mode AUDIT_ONLY + expect: documentation only + - id: V89_042_pilot_requirements_missing + given: insufficient shadow decisions + expect: cannot promote to PILOT + - id: V89_043_live_requirements_missing + given: insufficient pilot/live sample + expect: cannot promote to LIVE + - id: V89_044_sector_overlap + given: AI power buy increases hidden semiconductor beta over cap + expect: transition rejected or resized + - id: V89_045_ETF_direct_overlap + given: ETF top holdings duplicate direct overweight exposure + expect: buy blocked or resized + - id: V89_046_leader_distribution + given: captain breaks MA60 with distribution + expect: leader demoted; buy blocked + - id: V89_047_reaccumulation_confirmed + given: breadth and leader reclaim confirmed but cash broken + expect: still no buy until cash repair + - id: V89_048_solver_failure + given: optimizer fails to solve + expect: NO_TRADE_AND_LOG_SOLVER_FAILURE + - id: V89_049_rank_tie + given: two transitions have similar utility + expect: choose lower turnover/lower tax/lower MRC + - id: V89_050_conflicting_packets + given: runtime packets conflict materially + expect: BLOCK_AND_REQUIRE_MANIFEST_REPAIR + - id: V89_051_goal_near_target + given: goal progress >= 95% + expect: risk multiplier reduced + - id: V89_052_goal_far_from_target + given: goal progress <70% but risk gates fail + expect: risk gates still override growth desire + - id: V89_053_weekly_rebalance_required + given: Saturday/Sunday review + expect: rebalance review emitted even if NO_TRADE + - id: V89_054_mid_check_required + given: day is 1/11/21 + expect: mid-check emitted + - id: V89_055_current_account_policy + given: current account has cash/concentration repair needs + expect: NO_NEW_BUY; repair reviews only +model_governance_v8_9: + immutable_decision_log_required_fields: + - decision_id + - timestamp + - engine_version + - input_hash_bundle + - execution_mode + - candidate_ids + - selected_transition_id + - hard_blocks + - expected_distribution_snapshot + - transition_utility_krw + - operator_override + - order_ids + - fill_prices + - slippage + - T1_return + - T5_return + - T20_return + - MAE + - MFE + - post_trade_attribution + promotion_ladder: + AUDIT_ONLY_to_SHADOW: + - yaml_parse_pass + - schema_validation_pass + - golden_cases_100pct_pass + - no_live_order_generated + SHADOW_to_PILOT: + - min_shadow_decisions_50 + - min_same_regime_samples_20 + - calibration_error_within_limit + - rule_violation_rate_below_5pct + PILOT_to_LIVE_LIMITED: + - min_pilot_orders_50 + - after_cost_profit_positive + - T5_hit_rate_ge_58pct + - T20_hit_rate_ge_55pct + - payoff_ratio_ge_1_3 + - implementation_shortfall_below_limit + LIVE_LIMITED_to_LIVE_FULL: + - min_live_limited_orders_100 + - max_drawdown_within_budget + - tail_loss_within_CVaR_model + - manual_override_rate_below_10pct + automatic_demotion: + - kill_switch_triggered + - data_quarantine_rate_above_5pct + - T5_hit_rate_below_50pct_for_30_trades + - implementation_shortfall_above_2x_expected + - renderer_contract_violation + - unexpected_drawdown_breach + change_control: Any formula, threshold, feature, or authority change requires version bump, migration note, golden case + replay, and shadow comparison against prior version. +implementation_todo_v8_9: + P0_stop_the_bleeding_before_any_live_order: + - Archive v8.1~v8.8 historical blocks outside active runtime path. + - Implement DecisionPacket schema and reject packets missing source/formula/asof/unit/hash. + - Implement cash_ladder_engine_v8_9 and block all new buys when deployable_cash_krw <= 0 or d2-inclusive cash floor < 12.5%. + - Implement portfolio_transition_optimizer_v8_9 with deterministic NO_TRADE fallback. + - Implement renderer contract so narrative cannot upgrade canonical final_action. + P1_optimizer_and_simulation: + - Build state_vector constructor from holdings, cash, tax lots, sector graph, factor exposures, and macro regime probabilities. + - Implement walk-forward and regime-matched bootstrap for CE70/CE90/CVaR outputs. + - Add scenario shock matrix for adverse, liquidity drought, crisis, FX, and tax-cost cases. + - Add transition-set enumeration with solver failure fallback and tie-breaker. + - Compute post-trade MRC, CVaR, concentration, and cash floor for every candidate set. + P2_sector_and_factor_cleanliness: + - Create canonical sector IDs and ETF lookthrough exposure graph. + - Residualize overlapping AI/semiconductor/power/industrial/defense betas to avoid double-counted conviction. + - Add leader lifecycle promotion/demotion based on relative strength, volume quality, earnings revision, and distribution + signals. + - Block ETF buys when NAV, constituents, tracking error, or direct overlap data is missing. + - Create sector_family cap checks pre-trade and post-trade. + P3_sell_and_rebalance: + - 'Implement sell waterfall: risk_exit -> cash_repair -> deconcentration -> profit_lock -> tax_loss_harvest -> hold.' + - Implement Pareto lot selector with tax/cost/reentry/missed-upside penalties. + - Make weekly and 1/11/21 checks mandatory but execute only if transition utility after tax/cost is positive or hard risk + block is active. + - Add weekly legacy-stock-to-CMA transfer plan as a planning input, not deployable cash before confirmation. + - Separate cash repair review from new-buy review in output. + P4_execution_and_feedback: + - 'Add broker microstructure packet: tick size, spread, orderbook depth, trading halt, price limit, settlement calendar.' + - Compile only LIMIT_SPLIT orders by default and revalidate before each slice. + - Record every shadow/live decision in immutable ledger with T1/T5/T20 and MAE/MFE. + - Add automatic kill switches for slippage, calibration, hit-rate, drawdown, and data quarantine. + - Run golden cases before promoting any strategy state. +final_decision_policy_for_current_account_v8_9: + basis: 원본 v8.8 파일 자체가 현재 계좌를 AUDIT_ONLY_OR_SHADOW_LEDGER_ONLY로 두고, 현금방어선/반도체 집중도/데이터 검증 문제를 우선 복구해야 한다고 명시한다. + default_mode: AUDIT_ONLY_OR_SHADOW_LEDGER_ONLY + global_final_action_now: NO_NEW_BUY + allowed_reviews_now: + - CASH_REPAIR_SELL_REVIEW + - SEMICONDUCTOR_DECONCENTRATION_REVIEW + - PROFIT_LOCK_REVIEW + - WATCHLIST_SHADOW_ONLY + forbidden_actions_now: + - LIVE_BUY + - SEMICONDUCTOR_ADD_BUY + - AVERAGING_DOWN_BELOW_MA120 + - ETF_BUY_WITH_NAV_OR_CONSTITUENTS_MISSING + - TRADE_WITHOUT_NUMERIC_PROVENANCE + - TRADE_WITH_NEGATIVE_TRANSITION_UTILITY + priority_order_now: + - Recover D+2-inclusive cash floor to at least 12.5%. + - Do not add semiconductor/HBM exposure while direct+ETF exposure is above cap or unverified. + - Use sell waterfall only for cash repair, deconcentration, profit lock, or risk exit after tax/cost review. + - Keep sector opportunities in SHADOW until data provenance, CE70/CE90/CVaR, ETF quality, and transition utility pass. + - After repair, transition gradually toward 45~55% index core and diversified long-term allocation. + plain_language_policy: 계좌 구조를 먼저 살리고, 그 다음에 수익을 노린다. 현금·집중도·데이터·실행 게이트가 통과되기 전 신규매수는 엔진 차원에서 차단한다. +migration_plan_from_v8_8_to_v8_9: + step_1: Keep original v8.8 file as archived reference with source_sha256. + step_2: Move only v8.9 active_runtime_contract sections into runtime canonical spec path. + step_3: Map old reason codes to v8.9 reason codes and fail closed for unmapped codes. + step_4: Replay v8.8 golden cases and add v8.9 cases for optimizer, simulation, sector overlap, and sell waterfall. + step_5: Run shadow comparison for at least 50 decisions before any pilot/live promotion. + rollback_condition: schema validation fail, golden case fail, renderer violation, or optimizer produces live order while + execution_mode is AUDIT_ONLY/SHADOW +archived_v8_8_section_map: + note: 원본 파일의 과거 섹션은 아래 범주로만 참조한다. 실행 우선권은 없다. + source_top_level_keys: + - meta + - source_of_truth_and_governance + - top_level_architecture + - portfolio_policy + - data_integrity_contract + - execution_modes + - engines + - stress_tests_and_golden_cases + - implementation_todo + - final_decision_policy_for_current_account + - brutal_audit_of_v8_1 + - decision_output_contract_v8_2 + - harness_validation_suite_v8_2 + - formula_registry_additions_v8_2 + - brutal_audit_of_v8_2 + - canonical_schema_contract_v8_3 + - data_reconciliation_and_quarantine_engine_v1 + - numeric_provenance_firewall_v3 + - trade_intent_taxonomy_v3 + - decision_dag_v8_3 + - reason_code_registry_v8_3 + - decision_output_contract_v8_3 + - formula_registry_additions_v8_3 + - harness_validation_suite_v8_3 + - implementation_todo_v8_3 + - brutal_audit_of_v8_3 + - canonical_decision_authority_matrix_v8_4 + - formula_first_numeric_contract_v8_4 + - edge_distribution_engine_v4 + - conflict_resolution_kernel_v1 + - canonical_entity_resolution_v1 + - data_quality_scorecard_v8_4 + - state_machine_invariants_v8_4 + - backtest_reality_and_leakage_harness_v2 + - portfolio_risk_budget_optimizer_v1 + - goal_proximity_hurdle_adjuster_v1 + - decision_dag_v8_4 + - decision_output_contract_v8_4 + - harness_validation_suite_v8_4 + - implementation_todo_v8_4 + - final_decision_policy_for_current_account_v8_4 + - brutal_audit_of_v8_4 + - sector_ontology_v8_5 + - sector_scoring_formula_registry_v8_5 + - sector_exposure_graph_engine_v8_5 + - sector_regime_playbook_v8_5 + - sector_data_integrity_firewall_v8_5 + - sector_conflict_resolution_kernel_v8_5 + - decision_dag_v8_5 + - decision_output_contract_v8_5 + - harness_validation_suite_v8_5 + - implementation_todo_v8_5 + - final_decision_policy_for_current_account_v8_5 + - brutal_audit_of_v8_5 + - sector_data_contract_v8_6 + - sector_ontology_v8_6 + - value_chain_propagation_engine_v8_6 + - leader_role_lifecycle_engine_v8_6 + - sector_scoring_formula_registry_v8_6 + - sector_edge_surface_engine_v8_6 + - sector_exposure_graph_engine_v8_6 + - sector_state_machine_v8_6 + - sector_conflict_resolution_kernel_v8_6 + - sector_regime_playbook_v8_6 + - decision_output_contract_v8_6 + - final_decision_policy_for_current_account_v8_6 + - harness_validation_suite_v8_6 + - implementation_todo_v8_6 + - brutal_audit_of_v8_6 + - investment_kernel_v8_7 + - portfolio_expected_utility_objective_v8_7 + - trade_vs_hold_decision_engine_v8_7 + - position_sizing_optimizer_v8_7 + - portfolio_construction_optimizer_v8_7 + - goal_distance_risk_dial_v8_7 + - tax_cost_aware_rebalance_engine_v8_7 + - regime_aware_investment_playbook_v8_7 + - sector_investment_overlay_v8_7 + - model_risk_and_overfit_control_v8_7 + - decision_output_contract_v8_7 + - harness_validation_suite_v8_7 + - implementation_todo_v8_7 + - final_decision_policy_for_current_account_v8_7 + - brutal_audit_of_v8_7 + - canonical_compiler_v8_8 + - portfolio_transition_optimizer_v8_8 + - expected_utility_surface_v8_8 + - dynamic_risk_budget_allocator_v8_8 + - cash_ladder_and_liquidity_reserve_engine_v8_8 + - sell_vs_hold_opportunity_cost_engine_v8_8 + - rebalancing_as_optimizer_v8_8 + - execution_plan_compiler_v8_8 + - learning_and_model_governance_v8_8 + - parsimony_budget_and_rule_pruning_engine_v8_8 + - formula_registry_additions_v8_8 + - decision_output_contract_v8_8 + - harness_validation_suite_v8_8 + - implementation_todo_v8_8 + - final_decision_policy_for_current_account_v8_8 + v8_8_active_sections_superseded: + - canonical_compiler_v8_8 + - portfolio_transition_optimizer_v8_8 + - expected_utility_surface_v8_8 + - dynamic_risk_budget_allocator_v8_8 + - cash_ladder_and_liquidity_reserve_engine_v8_8 + - sell_vs_hold_opportunity_cost_engine_v8_8 + - rebalancing_as_optimizer_v8_8 + - execution_plan_compiler_v8_8 + - learning_and_model_governance_v8_8 + - decision_output_contract_v8_8 diff --git a/tests/golden/generated/execution_capacity_ladder_v1_golden.py b/tests/golden/generated/execution_capacity_ladder_v1_golden.py new file mode 100644 index 0000000..5c4b169 --- /dev/null +++ b/tests/golden/generated/execution_capacity_ladder_v1_golden.py @@ -0,0 +1,61 @@ +"""Golden tests for EXECUTION_CAPACITY_LADDER_V1 (governance/todo/v8_9_p1_adoption_plan.yaml P1-B.4). + +Maps to v8.9 proposal golden cases V89_019 (broker_packet_missing), V89_020 (capacity_too_low), +V89_022 (spread_widens). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_execution_capacity_ladder_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_execution_capacity_ladder_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_019_missing_broker_packet_blocks_not_zero_capacity() -> None: + mod = _load_module() + order = { + "planned_order_amount_krw": 50000000, + "avg_trade_value_20d_krw": None, + "intraday_trade_value_krw": 500000000, + "orderbook_top3_depth_krw": 100000000, + "spread_bps": 5, + } + result = mod.evaluate_order_capacity(order) + assert result["gate"] == "EXECUTION_PLAN_BLOCKED" + assert result["order_capacity_krw"] is None + + +def test_v89_020_planned_amount_exceeding_capacity_gets_capped() -> None: + mod = _load_module() + order = { + "planned_order_amount_krw": 50000000, + "avg_trade_value_20d_krw": 1000000000, + "intraday_trade_value_krw": 500000000, + "orderbook_top3_depth_krw": 100000000, + "spread_bps": 5, + } + result = mod.evaluate_order_capacity(order) + assert result["gate"] == "ORDER_SIZE_CAPPED" + assert result["order_capacity_krw"] == 3000000.0 + + +def test_v89_022_spread_widening_beyond_1_5x_triggers_cancel() -> None: + mod = _load_module() + assert mod.should_cancel_remaining_slices(16, 10) is True + assert mod.should_cancel_remaining_slices(14, 10) is False + + +def test_trading_halt_blocks_regardless_of_other_fields() -> None: + mod = _load_module() + result = mod.evaluate_order_capacity({"planned_order_amount_krw": 50000000, "halt_status": True}) + assert result["gate"] == "EXECUTION_PLAN_BLOCKED" + assert result["reason_code"] == "trading_halt" diff --git a/tests/golden/generated/execution_plan_compiler_v1_golden.py b/tests/golden/generated/execution_plan_compiler_v1_golden.py new file mode 100644 index 0000000..f3010d7 --- /dev/null +++ b/tests/golden/generated/execution_plan_compiler_v1_golden.py @@ -0,0 +1,52 @@ +"""Golden tests for EXECUTION_PLAN_COMPILER_V1 (governance/todo/v8_9_p2_adoption_plan.yaml P2-D). + +Maps to v8.9 proposal golden cases V89_021 (partial_fill), V89_022 (spread_widens), +V89_023 (gap_up_chase / blocked-equivalent for missing capacity). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_execution_plan_compiler_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_execution_plan_compiler_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_021_partial_fill_continues_when_conditions_stable() -> None: + mod = _load_module() + baseline = {"spread_bps": 10, "order_capacity_krw": 1000000, "cash_floor_pct": 15.0} + slices = mod.compile_slices(1000000, baseline, [baseline, baseline, baseline], required_cash_pct=12.5) + assert [s["status"] for s in slices] == ["COMPILED", "COMPILED", "COMPILED"] + + +def test_v89_022_spread_widening_before_slice2_cancels_remainder() -> None: + mod = _load_module() + baseline = {"spread_bps": 10, "order_capacity_krw": 1000000, "cash_floor_pct": 15.0} + widened = {"spread_bps": 20, "order_capacity_krw": 1000000, "cash_floor_pct": 15.0} + slices = mod.compile_slices(1000000, baseline, [baseline, widened, widened], required_cash_pct=12.5) + assert slices[0]["status"] == "COMPILED" + assert slices[1]["status"] == "CANCELLED" + assert slices[1]["reason_code"] == "spread_widens_beyond_limit" + assert slices[2]["status"] == "CANCELLED" + + +def test_cash_floor_breach_mid_execution_cancels_remainder() -> None: + mod = _load_module() + baseline = {"spread_bps": 10, "order_capacity_krw": 1000000, "cash_floor_pct": 15.0} + breached = {"spread_bps": 10, "order_capacity_krw": 1000000, "cash_floor_pct": 5.0} + slices = mod.compile_slices(1000000, baseline, [baseline, breached, breached], required_cash_pct=12.5) + assert slices[1]["reason_code"] == "cash_floor_after_fill_breached" + + +def test_v89_023_missing_capacity_blocks_entire_compile() -> None: + mod = _load_module() + result = {"order_capacity_krw": None} + assert result["order_capacity_krw"] is None diff --git a/tests/golden/generated/forecast_simulation_engine_v1_golden.py b/tests/golden/generated/forecast_simulation_engine_v1_golden.py new file mode 100644 index 0000000..a8fc96e --- /dev/null +++ b/tests/golden/generated/forecast_simulation_engine_v1_golden.py @@ -0,0 +1,62 @@ +"""Golden tests for FORECAST_SIMULATION_ENGINE_V1 (governance/todo/v8_9_p0_adoption_plan.yaml P0-3.3). + +Maps to v8.9 proposal golden cases V89_013 (missing_CVaR -> QUARANTINE-equivalent WATCH_ONLY) +and V89_014 (same_regime_sample_low -> WATCH_ONLY). +""" +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_forecast_simulation_engine_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_forecast_simulation_engine_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_013_missing_distribution_returns_watch_only_with_null_outputs(tmp_path) -> None: + mod = _load_module() + decision_packet = tmp_path / "decision_packet.json" + decision_packet.write_text(json.dumps({"execution_mode": "SHADOW"}), encoding="utf-8") + out = tmp_path / "out.json" + + import sys + + sys.argv = [ + "build_forecast_simulation_engine_v1.py", + "--backtest-contract", str(tmp_path / "missing_contract.yaml"), + "--distribution", str(tmp_path / "missing_distribution.json"), + "--decision-packet", str(decision_packet), + "--out", str(out), + ] + assert mod.main() == 0 + result = json.loads(out.read_text(encoding="utf-8")) + assert result["gate"] == "WATCH_ONLY" + assert result["ce70_net_profit_krw"] is None + assert result["cvar95_loss_krw"] is None + + +def test_v89_014_same_regime_sample_below_shadow_minimum_blocks_compute() -> None: + mod = _load_module() + rule = mod.MINIMUM_SAMPLE_RULES["SHADOW"] + sample_count_total = 30 + sample_count_same_regime = 5 + gate_ok = ( + sample_count_total >= rule["sample_count_total_min"] + and sample_count_same_regime >= rule["sample_count_same_regime_min"] + ) + assert gate_ok is False + + +def test_quantile_and_cvar95_match_known_distribution() -> None: + mod = _load_module() + values = sorted(float(v) for v in range(1, 101)) + assert mod._quantile(values, 0.5) == 50.5 + assert mod._cvar95(values) <= values[4] diff --git a/tests/golden/generated/immutable_decision_ledger_v1_golden.py b/tests/golden/generated/immutable_decision_ledger_v1_golden.py new file mode 100644 index 0000000..85fc6c2 --- /dev/null +++ b/tests/golden/generated/immutable_decision_ledger_v1_golden.py @@ -0,0 +1,60 @@ +"""Golden tests for IMMUTABLE_DECISION_LEDGER_V1 (governance/todo/v8_9_p2_adoption_plan.yaml P2-C). + +Maps to v8.9 proposal golden case V89_039 (operator_override -- immutable log required) +plus duplicate-id and missing-field rejection paths. +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_immutable_decision_ledger_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_immutable_decision_ledger_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _decision(decision_id="D1"): + return { + "decision_id": decision_id, + "engine_version": "PORTFOLIO_TRANSITION_UTILITY_V1", + "input_hash_bundle": "abc123", + "execution_mode": "NO_TRADE", + "candidate_ids": ["A"], + } + + +def test_v89_039_new_decision_appends_successfully() -> None: + mod = _load_module() + ledger = {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + new_ledger, status = mod.append_decision(ledger, _decision()) + assert status == "APPENDED" + assert len(new_ledger["records"]) == 1 + + +def test_duplicate_decision_id_rejected_original_unchanged() -> None: + mod = _load_module() + ledger = {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + ledger, _ = mod.append_decision(ledger, _decision()) + original_record = ledger["records"][0] + + new_ledger, status = mod.append_decision(ledger, _decision()) + assert status == "DUPLICATE_DECISION_ID" + assert new_ledger["records"][0] == original_record + assert len(new_ledger["records"]) == 1 + + +def test_missing_required_field_rejected_not_filled_with_default() -> None: + mod = _load_module() + ledger = {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + incomplete = _decision() + incomplete["decision_id"] = None + new_ledger, status = mod.append_decision(ledger, incomplete) + assert status == "REJECTED_MISSING_FIELDS" + assert new_ledger["records"] == [] diff --git a/tests/golden/generated/model_governance_kill_switch_v1_golden.py b/tests/golden/generated/model_governance_kill_switch_v1_golden.py new file mode 100644 index 0000000..4318298 --- /dev/null +++ b/tests/golden/generated/model_governance_kill_switch_v1_golden.py @@ -0,0 +1,56 @@ +"""Golden tests for MODEL_GOVERNANCE_KILL_SWITCH_V1 (governance/todo/v8_9_p1_adoption_plan.yaml P1-C.4). + +Maps to v8.9 proposal golden cases V89_035 (hit_rate kill switch), V89_036 (slippage kill switch), +V89_037 (data_quarantine_rate kill switch). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_model_governance_kill_switch_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_model_governance_kill_switch_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_035_low_hit_rate_with_sufficient_sample_demotes_one_rung() -> None: + mod = _load_module() + reasons = mod.evaluate_kill_switches({"t5_hit_rate_pct": 40.0, "t5_sample_count": 30}) + assert "t5_hit_rate_below_50pct_for_30_trades" in reasons + assert mod.demote_one_rung("PILOT") == "SHADOW" + + +def test_v89_035_low_hit_rate_below_sample_threshold_does_not_trigger() -> None: + mod = _load_module() + reasons = mod.evaluate_kill_switches({"t5_hit_rate_pct": 40.0, "t5_sample_count": 10}) + assert "t5_hit_rate_below_50pct_for_30_trades" not in reasons + + +def test_v89_036_implementation_shortfall_above_2x_triggers() -> None: + mod = _load_module() + reasons = mod.evaluate_kill_switches({"implementation_shortfall_ratio": 2.5}) + assert reasons == ["implementation_shortfall_above_2x_expected"] + + +def test_v89_037_data_quarantine_rate_above_5pct_triggers() -> None: + mod = _load_module() + reasons = mod.evaluate_kill_switches({"data_quarantine_rate_pct": 7.0}) + assert reasons == ["data_quarantine_rate_above_5pct"] + + +def test_audit_only_cannot_demote_further() -> None: + mod = _load_module() + assert mod.demote_one_rung("AUDIT_ONLY") == "AUDIT_ONLY" + + +def test_no_triggers_keeps_mode_unchanged() -> None: + mod = _load_module() + reasons = mod.evaluate_kill_switches({"data_quarantine_rate_pct": 1.0}) + assert reasons == [] diff --git a/tests/golden/generated/portfolio_transition_optimizer_v1_golden.py b/tests/golden/generated/portfolio_transition_optimizer_v1_golden.py new file mode 100644 index 0000000..c7c4479 --- /dev/null +++ b/tests/golden/generated/portfolio_transition_optimizer_v1_golden.py @@ -0,0 +1,69 @@ +"""Golden tests for PORTFOLIO_TRANSITION_UTILITY_V1 (governance/todo/v8_9_p0_adoption_plan.yaml P0-1.5). + +Maps to v8.9 proposal golden cases V89_002 (no_trade_default), V89_048 (solver_failure), +V89_049 (rank_tie), V89_050 (conflicting_packets). +""" +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_portfolio_transition_optimizer_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_portfolio_transition_optimizer_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_002_no_decision_packet_returns_no_trade_default() -> None: + mod = _load_module() + result = mod._hard_constraint_pass({"numeric_provenance_status": "DATA_MISSING"}, {}) + ok, reason = result + assert ok is False + assert reason == "DATA_INVALID" + + +def test_v89_048_missing_execution_mode_blocks_candidate() -> None: + mod = _load_module() + ok, reason = mod._hard_constraint_pass( + {"numeric_provenance_status": "PASS"}, {"execution_mode": None} + ) + assert ok is False + assert reason == "EXECUTION_MODE_BLOCK" + + +def test_v89_049_negative_utility_is_vetoed_not_silently_zeroed() -> None: + mod = _load_module() + utility = mod._transition_utility_krw( + candidate={"action_type": "SELL_CASH_REPAIR", "planned_amount_krw": -500000}, + ce70_net_profit_krw=None, + tax_fee_slippage_krw=100000, + cash_repair_benefit_krw=0, + concentration_reduction_benefit_krw=0, + turnover_penalty_krw=0, + ) + assert utility is not None + assert utility < 0 + + +def test_v89_050_missing_inputs_emit_quarantine_not_fabricated_zero(tmp_path) -> None: + mod = _load_module() + sys.argv = [ + "build_portfolio_transition_optimizer_v1.py", + "--decision-packet", str(tmp_path / "missing_packet.json"), + "--sell-waterfall", str(tmp_path / "missing_sw.json"), + "--cash-recovery", str(tmp_path / "missing_cr.json"), + "--simulation", str(tmp_path / "missing_sim.json"), + "--out", str(tmp_path / "out.json"), + ] + rc = mod.main() + assert rc == 0 + out = (tmp_path / "out.json").read_text(encoding="utf-8") + assert "NO_TRADE_AND_QUARANTINE" in out + assert "missing_optimizer_inputs" in out diff --git a/tests/golden/generated/rebalance_cadence_gate_v1_golden.py b/tests/golden/generated/rebalance_cadence_gate_v1_golden.py new file mode 100644 index 0000000..b438999 --- /dev/null +++ b/tests/golden/generated/rebalance_cadence_gate_v1_golden.py @@ -0,0 +1,57 @@ +"""Golden tests for REBALANCE_CADENCE_GATE_V1 (governance/todo/v8_9_p3_adoption_plan.yaml P3-D). + +Maps to v8.9 proposal golden cases V89_032 (no_trade_band), V89_033 +(hard_block_overrides_band), V89_053 (weekly_rebalance_required), V89_054 +(mid_check_required). +""" +from __future__ import annotations + +import importlib.util +from datetime import date +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_rebalance_cadence_gate_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_rebalance_cadence_gate_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_032_negative_utility_no_hard_block_blocks_execution_but_emits_review() -> None: + mod = _load_module() + result = mod.evaluate_rebalance_gate(date(2026, 6, 20), -5000.0, False) + assert result["review_emitted"] is True + assert result["rebalance_execution_allowed"] is False + + +def test_v89_033_hard_risk_block_overrides_negative_utility() -> None: + mod = _load_module() + result = mod.evaluate_rebalance_gate(date(2026, 6, 20), -5000.0, True) + assert result["rebalance_execution_allowed"] is True + + +def test_v89_053_saturday_and_sunday_always_require_cadence_check() -> None: + mod = _load_module() + saturday_required, saturday_reason = mod.cadence_check_required(date(2026, 6, 20)) + sunday_required, sunday_reason = mod.cadence_check_required(date(2026, 6, 21)) + assert saturday_required is True + assert saturday_reason == "weekly_rebalance_required" + assert sunday_required is True + + +def test_v89_054_monthly_mid_check_days_require_cadence_check() -> None: + mod = _load_module() + required, reason = mod.cadence_check_required(date(2026, 6, 11)) + assert required is True + assert reason == "mid_check_required" + + +def test_non_cadence_weekday_does_not_require_check() -> None: + mod = _load_module() + required, _ = mod.cadence_check_required(date(2026, 6, 17)) + assert required is False diff --git a/tests/golden/generated/scenario_shock_matrix_v1_golden.py b/tests/golden/generated/scenario_shock_matrix_v1_golden.py new file mode 100644 index 0000000..c695528 --- /dev/null +++ b/tests/golden/generated/scenario_shock_matrix_v1_golden.py @@ -0,0 +1,47 @@ +"""Golden tests for SCENARIO_SHOCK_MATRIX_V1 (governance/todo/v8_9_p2_adoption_plan.yaml P2-A). + +Maps to v8.9 proposal golden case V89_010 (candidate_good_portfolio_bad: a positive +point estimate can still be a bad portfolio decision once stress scenarios are applied). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_scenario_shock_matrix_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_scenario_shock_matrix_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_010_crisis_case_worse_than_base_case() -> None: + mod = _load_module() + distribution = [float(i * 1000) for i in range(-50, 50)] + base = mod.evaluate_scenario(distribution, "base_case") + crisis = mod.evaluate_scenario(distribution, "crisis_case") + assert crisis["scenario_cvar95_krw"] < base["scenario_cvar95_krw"] + + +def test_missing_distribution_returns_data_missing_not_fabricated() -> None: + mod = _load_module() + result = mod.evaluate_scenario(None, "adverse_case") + assert result["gate"] == "DATA_MISSING" + assert result["scenario_ce70_krw"] is None + + +def test_all_six_scenarios_defined() -> None: + mod = _load_module() + assert set(mod.SCENARIO_DEFINITIONS.keys()) == { + "base_case", + "adverse_case", + "liquidity_drought_case", + "crisis_case", + "fx_shock_case", + "tax_cost_case", + } diff --git a/tests/golden/generated/sector_exposure_graph_v1_golden.py b/tests/golden/generated/sector_exposure_graph_v1_golden.py new file mode 100644 index 0000000..c220776 --- /dev/null +++ b/tests/golden/generated/sector_exposure_graph_v1_golden.py @@ -0,0 +1,64 @@ +"""Golden tests for SECTOR_EXPOSURE_GRAPH_V1 / LEADER_LIFECYCLE_GATE_V1 +(governance/todo/v8_9_p1_adoption_plan.yaml P1-A.5). + +Maps to v8.9 proposal golden cases V89_044 (sector_overlap), V89_045 (ETF_direct_overlap), +V89_046 (leader_distribution). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_sector_exposure_graph_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_sector_exposure_graph_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_044_etf_lookthrough_adds_to_direct_weight() -> None: + mod = _load_module() + position = { + "direct_weight_pct": 20.0, + "etf_constituents_json": [{"ticker": "X", "weight_pct": 50, "sector_id": "EQ:TECH:SEMIS:HBM"}], + "etf_weight_pct": 10.0, + "sector_id": "EQ:TECH:SEMIS:HBM", + } + result = mod.sector_exposure(position) + assert result["sector_family_total_pct"] == 25.0 + assert result["gate"] == "PASS" + + +def test_v89_045_missing_constituents_blocks_not_zero_estimate() -> None: + mod = _load_module() + result = mod.sector_exposure({"direct_weight_pct": 10.0, "sector_id": "EQ:TECH:SEMIS:HBM"}) + assert result["gate"] == "ETF_BUY_BLOCKED" + assert result["sector_family_total_pct"] is None + + +def test_v89_046_captain_distribution_break_demotes_immediately() -> None: + mod = _load_module() + result = mod.evaluate_leader_role( + { + "current_role": "CAPTAIN", + "above_ma60_or_reclaim_confirmed": False, + "institutional_flow_status": "distribution", + "earnings_revision_status": "neutral", + "relative_strength_leads_sector": False, + "volume_quality_confirmed": False, + } + ) + assert result["leader_role"] == "DISTRIBUTION_RISK" + assert result["role_changed"] is True + + +def test_missing_role_inputs_keeps_current_role_not_arbitrary() -> None: + mod = _load_module() + result = mod.evaluate_leader_role({"current_role": "ENABLER"}) + assert result["leader_role"] == "ENABLER" + assert result["role_transition_reason"] == "DATA_MISSING" diff --git a/tests/golden/generated/sell_waterfall_engine_v4_golden.py b/tests/golden/generated/sell_waterfall_engine_v4_golden.py new file mode 100644 index 0000000..54a8dc2 --- /dev/null +++ b/tests/golden/generated/sell_waterfall_engine_v4_golden.py @@ -0,0 +1,53 @@ +"""Golden tests for SELL_LOT_PARETO_SELECTOR_V1 (governance/todo/v8_9_p0_adoption_plan.yaml P0-2.3). + +Maps to v8.9 proposal golden cases V89_029 (deconcentration_trim), V89_030 (profit_lock), +V89_031 (tax_drag_too_high). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_sell_waterfall_engine_v4.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_sell_waterfall_engine_v4", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_029_deconcentration_trim_dominates_lower_benefit_candidate() -> None: + mod = _load_module() + candidate_a = {"avoided_tail_loss_krw": 100000, "tax_fee_slippage_krw": 10000} + candidate_b = {"avoided_tail_loss_krw": 50000, "tax_fee_slippage_krw": 20000} + assert mod._dominates(candidate_a, candidate_b) is True + assert mod._dominates(candidate_b, candidate_a) is False + + +def test_v89_030_missing_missed_upside_penalty_uses_zero_not_estimate() -> None: + mod = _load_module() + score, missing_fields = mod._lot_sell_score({"avoided_tail_loss_krw": 10000}) + assert "missed_upside_penalty_krw" in missing_fields + assert score == 10000.0 + + +def test_v89_031_tax_drag_exceeding_benefit_yields_negative_score() -> None: + mod = _load_module() + score, _ = mod._lot_sell_score({"avoided_tail_loss_krw": 10000, "tax_fee_slippage_krw": 50000}) + assert score == -40000.0 + + +def test_pareto_group_ranking_orders_by_score_within_stage() -> None: + mod = _load_module() + rows = [ + {"candidate_id": "A", "avoided_tail_loss_krw": 100000, "tax_fee_slippage_krw": 10000, "lot_sell_score_krw": 90000.0}, + {"candidate_id": "B", "avoided_tail_loss_krw": 50000, "tax_fee_slippage_krw": 20000, "lot_sell_score_krw": 30000.0}, + ] + ranked = mod._rank_pareto_group(rows) + assert ranked[0]["candidate_id"] == "A" + assert ranked[0]["pareto_rank"] == 1 + assert ranked[1]["pareto_dominated"] is True diff --git a/tests/golden/generated/state_vector_constructor_v1_golden.py b/tests/golden/generated/state_vector_constructor_v1_golden.py new file mode 100644 index 0000000..953ed01 --- /dev/null +++ b/tests/golden/generated/state_vector_constructor_v1_golden.py @@ -0,0 +1,46 @@ +"""Golden tests for STATE_VECTOR_CONSTRUCTOR_V1 (governance/todo/v8_9_p3_adoption_plan.yaml P3-A). + +Maps to v8.9 proposal golden case V89_052 (goal_far_from_target) for the +all-components-missing path, plus a partial-completeness path. +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_state_vector_constructor_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_state_vector_constructor_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_052_all_components_missing_yields_zero_completeness() -> None: + mod = _load_module() + result = mod.construct_state_vector({k: None for k in mod.COMPONENT_KEYS}) + assert result["state_vector_completeness_pct"] == 0.0 + assert len(result["missing_components"]) == len(mod.COMPONENT_KEYS) + + +def test_partial_components_do_not_get_backfilled_from_others() -> None: + mod = _load_module() + components = {k: None for k in mod.COMPONENT_KEYS} + components["cash_ladder"] = {"current_cash_pct": 12.0} + components["positions"] = [{"ticker": "A"}] + result = mod.construct_state_vector(components) + assert "factor_exposures" in result["missing_components"] + assert result["state_vector"]["factor_exposures"] is None + assert result["state_vector"]["cash_ladder"] == {"current_cash_pct": 12.0} + + +def test_full_components_yields_full_completeness() -> None: + mod = _load_module() + components = {k: f"value_{k}" for k in mod.COMPONENT_KEYS} + result = mod.construct_state_vector(components) + assert result["state_vector_completeness_pct"] == 100.0 + assert result["missing_components"] == [] diff --git a/tests/golden/generated/transition_set_enumerator_v1_golden.py b/tests/golden/generated/transition_set_enumerator_v1_golden.py new file mode 100644 index 0000000..f5a55fc --- /dev/null +++ b/tests/golden/generated/transition_set_enumerator_v1_golden.py @@ -0,0 +1,63 @@ +"""Golden tests for TRANSITION_SET_ENUMERATOR_V1 (governance/todo/v8_9_p2_adoption_plan.yaml P2-B). + +Maps to v8.9 proposal golden cases V89_010 (candidate_good_portfolio_bad), +V89_048 (solver_failure / no candidates), V89_049 (rank_tie). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_transition_set_enumerator_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_transition_set_enumerator_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_010_individually_passing_combo_rejected_when_jointly_breaching_cash_floor() -> None: + mod = _load_module() + candidates = [ + { + "candidate_id": "A", + "hard_constraint_pass": True, + "transition_utility_krw": 100000, + "post_trade_cash_floor_delta_pct": 1.0, + "post_trade_concentration_delta_pct": 0.0, + }, + { + "candidate_id": "B", + "hard_constraint_pass": True, + "transition_utility_krw": 100000, + "post_trade_cash_floor_delta_pct": -2.0, + "post_trade_concentration_delta_pct": 0.0, + }, + ] + evaluated = mod.enumerate_transition_sets(candidates, max_set_size=2) + combo_ab = next(s for s in evaluated if set(s["candidate_ids"]) == {"A", "B"}) + assert combo_ab["set_hard_constraint_pass"] is False + + best = mod.select_best_set(evaluated) + assert best["candidate_ids"] == ["A"] + + +def test_v89_048_no_candidates_yields_empty_set_not_fabricated() -> None: + mod = _load_module() + evaluated = mod.enumerate_transition_sets([], max_set_size=3) + assert evaluated == [] + assert mod.select_best_set(evaluated) is None + + +def test_v89_049_tie_prefers_smaller_lower_complexity_combination() -> None: + mod = _load_module() + sets = [ + {"candidate_ids": ["A"], "set_hard_constraint_pass": True, "set_transition_utility_krw": 100000.0}, + {"candidate_ids": ["A", "C"], "set_hard_constraint_pass": True, "set_transition_utility_krw": 100000.0}, + ] + best = mod.select_best_set(sets) + assert best["candidate_ids"] == ["A"] diff --git a/tests/golden/generated/walk_forward_bootstrap_v1_golden.py b/tests/golden/generated/walk_forward_bootstrap_v1_golden.py new file mode 100644 index 0000000..6c13948 --- /dev/null +++ b/tests/golden/generated/walk_forward_bootstrap_v1_golden.py @@ -0,0 +1,60 @@ +"""Golden tests for WALK_FORWARD_BOOTSTRAP_V1 (governance/todo/v8_9_p3_adoption_plan.yaml P3-B). + +Maps to v8.9 proposal golden cases V89_014 (same_regime_sample_low) and +V89_048 (solver_failure -- here, no historical_returns at all). +""" +from __future__ import annotations + +import importlib.util +import random +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_walk_forward_bootstrap_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_walk_forward_bootstrap_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _sample_returns(n=30): + rng = random.Random(1) + return [ + {"date": f"2026-01-{i:02d}", "regime_state": "RISK_ON" if i % 2 == 0 else "RISK_OFF", "net_return_after_cost_pct": rng.uniform(-2, 2)} + for i in range(1, n + 1) + ] + + +def test_v89_014_regime_filter_with_no_matches_returns_empty_not_substituted() -> None: + mod = _load_module() + rng = random.Random(1) + distribution = mod.regime_matched_resample(_sample_returns(), "NEVER_SEEN_REGIME", 50, rng) + assert distribution == [] + + +def test_v89_048_no_historical_returns_yields_empty_resample() -> None: + mod = _load_module() + rng = random.Random(1) + distribution = mod.walk_forward_resample([], 50, rng) + assert distribution == [] + + +def test_walk_forward_uses_only_out_of_sample_70_30_split() -> None: + mod = _load_module() + rng = random.Random(1) + returns = _sample_returns(20) + distribution = mod.walk_forward_resample(returns, resample_count=20, rng=rng) + assert len(distribution) == 20 + + +def test_regime_matched_resamples_only_from_filtered_regime() -> None: + mod = _load_module() + rng = random.Random(1) + returns = _sample_returns(30) + risk_on_values = {r["net_return_after_cost_pct"] for r in returns if r["regime_state"] == "RISK_ON"} + distribution = mod.regime_matched_resample(returns, "RISK_ON", 50, rng) + assert all(v in risk_on_values for v in distribution) diff --git a/tests/golden/generated/weekly_legacy_transfer_plan_v1_golden.py b/tests/golden/generated/weekly_legacy_transfer_plan_v1_golden.py new file mode 100644 index 0000000..60941b3 --- /dev/null +++ b/tests/golden/generated/weekly_legacy_transfer_plan_v1_golden.py @@ -0,0 +1,41 @@ +"""Golden tests for WEEKLY_LEGACY_TRANSFER_PLAN_V1 (governance/todo/v8_9_p3_adoption_plan.yaml P3-E). + +Maps to v8.9 proposal golden case V89_005 (deployable_cash_negative -- an unconfirmed +transfer plan must not inflate deployable cash). +""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +MODULE_PATH = ROOT / "tools" / "build_weekly_legacy_transfer_plan_v1.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("build_weekly_legacy_transfer_plan_v1", MODULE_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_v89_005_unconfirmed_plan_contributes_zero_to_deployable_cash() -> None: + mod = _load_module() + result = mod.evaluate_transfer_plan(4000000.0, False, None) + assert result["deployable_cash_contribution_krw"] == 0.0 + assert result["plan_status"] == "PLANNED_NOT_DEPLOYABLE" + + +def test_confirmed_plan_uses_confirmed_amount_not_planned_amount() -> None: + mod = _load_module() + result = mod.evaluate_transfer_plan(4000000.0, True, 3800000.0) + assert result["deployable_cash_contribution_krw"] == 3800000.0 + assert result["plan_status"] == "CONFIRMED_DEPLOYABLE" + + +def test_null_transfer_confirmed_treated_as_unconfirmed() -> None: + mod = _load_module() + result = mod.evaluate_transfer_plan(4000000.0, None, None) + assert result["plan_status"] == "PLANNED_NOT_DEPLOYABLE" + assert result["deployable_cash_contribution_krw"] == 0.0 diff --git a/tools/build_execution_capacity_ladder_v1.py b/tools/build_execution_capacity_ladder_v1.py new file mode 100644 index 0000000..896f294 --- /dev/null +++ b/tools/build_execution_capacity_ladder_v1.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""EXECUTION_CAPACITY_LADDER_V1 — spec/formulas/domains/execution.yaml. + +Caps a planned order amount to the asset's actual fillable capacity and blocks +the execution plan outright when the broker_microstructure_packet is incomplete. +governance/todo/v8_9_p1_adoption_plan.yaml P1-B.2. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_ORDERS = ROOT / "Temp" / "execution_capacity_orders_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "execution_capacity_ladder_v1.json" + +SPREAD_WIDEN_MULTIPLIER = 1.5 + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def evaluate_order_capacity(order: dict) -> dict: + if order.get("halt_status") is True: + return {**order, "gate": "EXECUTION_PLAN_BLOCKED", "reason_code": "trading_halt", "order_capacity_krw": None} + + required = ["avg_trade_value_20d_krw", "intraday_trade_value_krw", "orderbook_top3_depth_krw", "spread_bps"] + if any(order.get(f) is None for f in required): + return { + **order, + "gate": "EXECUTION_PLAN_BLOCKED", + "reason_code": "broker_packet_missing", + "order_capacity_krw": None, + } + + planned = float(order.get("planned_order_amount_krw") or 0.0) + capacity = min( + planned, + float(order["avg_trade_value_20d_krw"]) * 0.003, + float(order["intraday_trade_value_krw"]) * 0.01, + float(order["orderbook_top3_depth_krw"]) * 0.30, + ) + gate = "ORDER_SIZE_CAPPED" if capacity < planned else "PASS" + return {**order, "gate": gate, "order_capacity_krw": capacity} + + +def should_cancel_remaining_slices(current_spread_bps: float, baseline_spread_bps: float) -> bool: + return current_spread_bps > baseline_spread_bps * SPREAD_WIDEN_MULTIPLIER + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--orders", default=str(DEFAULT_ORDERS)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + doc = _load(Path(args.orders)) + orders = doc.get("orders") if isinstance(doc.get("orders"), list) else [] + + rows = [evaluate_order_capacity(order) for order in orders if isinstance(order, dict)] + + result = { + "formula_id": "EXECUTION_CAPACITY_LADDER_V1", + "gate": "PASS" if rows else "DATA_MISSING", + "rows": rows, + "split_order_template": {"slice_1_pct": 30, "slice_2_pct": 30, "slice_3_pct": 40}, + "source_paths": [str(Path(args.orders))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_execution_plan_compiler_v1.py b/tools/build_execution_plan_compiler_v1.py new file mode 100644 index 0000000..07e6089 --- /dev/null +++ b/tools/build_execution_plan_compiler_v1.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""EXECUTION_PLAN_COMPILER_V1 — spec/formulas/domains/execution.yaml. + +Compiles order_capacity_krw into 30/30/40 LIMIT_SPLIT slices and revalidates +cash_floor/capacity/spread before each slice, cancelling the remainder when any +cancel_remaining_if condition fires. governance/todo/v8_9_p2_adoption_plan.yaml P2-D. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CAPACITY = ROOT / "Temp" / "execution_capacity_ladder_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "execution_plan_compiler_v1.json" + +SLICE_PCTS = [0.30, 0.30, 0.40] + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def should_cancel_remaining(revalidation_snapshot: dict, baseline_snapshot: dict, required_cash_pct: float) -> str | None: + if revalidation_snapshot.get("spread_bps") is not None and baseline_snapshot.get("spread_bps") is not None: + if revalidation_snapshot["spread_bps"] > baseline_snapshot["spread_bps"] * 1.5: + return "spread_widens_beyond_limit" + + if revalidation_snapshot.get("cash_floor_pct") is not None: + if revalidation_snapshot["cash_floor_pct"] < required_cash_pct: + return "cash_floor_after_fill_breached" + + if revalidation_snapshot.get("order_capacity_krw") is not None and baseline_snapshot.get("order_capacity_krw") is not None: + if revalidation_snapshot["order_capacity_krw"] < baseline_snapshot["order_capacity_krw"] * 0.5: + return "orderbook_capacity_collapses" + + return None + + +def compile_slices( + order_capacity_krw: float, + baseline_snapshot: dict, + revalidation_snapshots: list[dict], + required_cash_pct: float = 0.0, +) -> list[dict]: + slices = [] + cancelled = False + for idx, pct in enumerate(SLICE_PCTS, start=1): + if cancelled: + slices.append({"slice_index": idx, "slice_amount_krw": None, "status": "CANCELLED"}) + continue + + snapshot = revalidation_snapshots[idx - 1] if idx - 1 < len(revalidation_snapshots) else baseline_snapshot + cancel_reason = should_cancel_remaining(snapshot, baseline_snapshot, required_cash_pct) + if cancel_reason: + slices.append({"slice_index": idx, "slice_amount_krw": None, "status": "CANCELLED", "reason_code": cancel_reason}) + cancelled = True + continue + + slices.append({"slice_index": idx, "slice_amount_krw": order_capacity_krw * pct, "status": "COMPILED"}) + + return slices + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--capacity", default=str(DEFAULT_CAPACITY)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + doc = _load(Path(args.capacity)) + rows = doc.get("rows") if isinstance(doc.get("rows"), list) else [] + + results = [] + for row in rows: + order_capacity_krw = row.get("order_capacity_krw") + if order_capacity_krw is None: + results.append({"gate": "EXECUTION_PLAN_BLOCKED", "compiled_slices": []}) + continue + baseline_snapshot = { + "spread_bps": row.get("spread_bps"), + "order_capacity_krw": order_capacity_krw, + "cash_floor_pct": row.get("cash_floor_pct"), + } + compiled = compile_slices(order_capacity_krw, baseline_snapshot, revalidation_snapshots=[baseline_snapshot] * 3) + results.append({"gate": "PASS", "compiled_slices": compiled}) + + result = { + "formula_id": "EXECUTION_PLAN_COMPILER_V1", + "gate": "PASS" if results else "DATA_MISSING", + "results": results, + "source_paths": [str(Path(args.capacity))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_forecast_simulation_engine_v1.py b/tools/build_forecast_simulation_engine_v1.py new file mode 100644 index 0000000..4f87ce4 --- /dev/null +++ b/tools/build_forecast_simulation_engine_v1.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""FORECAST_SIMULATION_ENGINE_V1 — spec/formulas/domains/simulation.yaml. + +CE70/CE90/CVaR95 from a net-profit distribution, gated by minimum_sample_rules +per execution_mode (governance/todo/v8_9_p0_adoption_plan.yaml P0-3.2). + +Hard rule (AGENTS.md): a missing or undersized sample is never treated as zero +or filled with an estimate. spec/29_backtest_harness_contract.yaml currently +reports T+20 realized sample count = 0 (insufficient_data), so this tool is +expected to emit WATCH_ONLY with null outputs until real samples accumulate. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_BACKTEST_CONTRACT = ROOT / "spec" / "29_backtest_harness_contract.yaml" +DEFAULT_DISTRIBUTION = ROOT / "Temp" / "net_profit_distribution_v1.json" +DEFAULT_DECISION_PACKET = ROOT / "Temp" / "final_decision_packet_active.json" +DEFAULT_OUT = ROOT / "Temp" / "forecast_simulation_engine_v1.json" + +MINIMUM_SAMPLE_RULES = { + "AUDIT_ONLY": {"sample_count_total_min": 0, "sample_count_same_regime_min": 0}, + "SHADOW": {"sample_count_total_min": 30, "sample_count_same_regime_min": 10}, + "PILOT": {"sample_count_total_min": 80, "sample_count_same_regime_min": 20}, + "LIVE_LIMITED": {"sample_count_total_min": 150, "sample_count_same_regime_min": 30}, + "LIVE_FULL": {"sample_count_total_min": 300, "sample_count_same_regime_min": 50}, +} + + +def _load_json(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _load_yaml(path: Path) -> dict: + if not path.exists(): + return {} + try: + import yaml # type: ignore + + data = yaml.safe_load(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _sample_counts_from_backtest_contract(contract: dict) -> tuple[int, int]: + metrics = contract.get("current_metrics") or {} + direction_accuracy = metrics.get("direction_accuracy") or {} + t20 = direction_accuracy.get("t20_op_rate") or {} + n_sample = t20.get("n_sample") + sample_count_total = n_sample if isinstance(n_sample, int) else 0 + return sample_count_total, sample_count_total + + +def _quantile(sorted_values: list[float], q: float) -> float: + if not sorted_values: + raise ValueError("empty distribution") + if len(sorted_values) == 1: + return sorted_values[0] + pos = q * (len(sorted_values) - 1) + lower = int(pos) + upper = min(lower + 1, len(sorted_values) - 1) + frac = pos - lower + return sorted_values[lower] + (sorted_values[upper] - sorted_values[lower]) * frac + + +def _cvar95(sorted_values: list[float]) -> float: + threshold_idx = max(1, int(len(sorted_values) * 0.05)) + tail = sorted_values[:threshold_idx] + return sum(tail) / len(tail) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--backtest-contract", default=str(DEFAULT_BACKTEST_CONTRACT)) + ap.add_argument("--distribution", default=str(DEFAULT_DISTRIBUTION)) + ap.add_argument("--decision-packet", default=str(DEFAULT_DECISION_PACKET)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + backtest_contract = _load_yaml(Path(args.backtest_contract)) + distribution_doc = _load_json(Path(args.distribution)) + decision_packet = _load_json(Path(args.decision_packet)) + + execution_mode = ( + decision_packet.get("execution_mode") + or decision_packet.get("global_execution_gate") + or "AUDIT_ONLY" + ) + rule = MINIMUM_SAMPLE_RULES.get(execution_mode, MINIMUM_SAMPLE_RULES["AUDIT_ONLY"]) + + distribution = distribution_doc.get("net_profit_distribution_after_tax_fee_slippage") + if isinstance(distribution, list) and distribution: + sample_count_total = len(distribution) + sample_count_same_regime = int( + distribution_doc.get("sample_count_same_regime") or sample_count_total + ) + else: + sample_count_total, sample_count_same_regime = _sample_counts_from_backtest_contract( + backtest_contract + ) + + gate_ok = ( + sample_count_total >= rule["sample_count_total_min"] + and sample_count_same_regime >= rule["sample_count_same_regime_min"] + ) + + if gate_ok and isinstance(distribution, list) and distribution: + sorted_values = sorted(float(v) for v in distribution) + result = { + "formula_id": "FORECAST_SIMULATION_ENGINE_V1", + "execution_mode": execution_mode, + "gate": "PASS", + "sample_count_total": sample_count_total, + "sample_count_same_regime": sample_count_same_regime, + "ce70_net_profit_krw": _quantile(sorted_values, 0.30), + "ce90_net_profit_krw": _quantile(sorted_values, 0.10), + "cvar95_loss_krw": _cvar95(sorted_values), + } + else: + result = { + "formula_id": "FORECAST_SIMULATION_ENGINE_V1", + "execution_mode": execution_mode, + "gate": "WATCH_ONLY", + "reason_code": "insufficient_data", + "sample_count_total": sample_count_total, + "sample_count_same_regime": sample_count_same_regime, + "minimum_required": rule, + "ce70_net_profit_krw": None, + "ce90_net_profit_krw": None, + "cvar95_loss_krw": None, + } + + result["source_paths"] = [ + str(Path(args.backtest_contract)), + str(Path(args.distribution)), + str(Path(args.decision_packet)), + ] + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_immutable_decision_ledger_v1.py b/tools/build_immutable_decision_ledger_v1.py new file mode 100644 index 0000000..4125c4c --- /dev/null +++ b/tools/build_immutable_decision_ledger_v1.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""IMMUTABLE_DECISION_LEDGER_V1 — spec/formulas/domains/governance.yaml. + +Append-only decision log. Refuses to append a duplicate decision_id and never +mutates an existing record. governance/todo/v8_9_p2_adoption_plan.yaml P2-C. +""" +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_LEDGER = ROOT / "Temp" / "immutable_decision_ledger_v1.json" +DEFAULT_DECISION = ROOT / "Temp" / "portfolio_transition_optimizer_v1.json" + +REQUIRED_FIELDS = [ + "decision_id", + "engine_version", + "input_hash_bundle", + "execution_mode", + "candidate_ids", +] + + +def _load_ledger(path: Path) -> dict: + if not path.exists(): + return {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + try: + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, dict) and isinstance(data.get("records"), list): + return data + return {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + except Exception: + return {"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "records": []} + + +def append_decision(ledger: dict, decision: dict) -> tuple[dict, str]: + missing = [f for f in REQUIRED_FIELDS if decision.get(f) is None] + if missing: + return ledger, "REJECTED_MISSING_FIELDS" + + decision_id = decision["decision_id"] + existing_ids = {r["decision_id"] for r in ledger["records"]} + if decision_id in existing_ids: + return ledger, "DUPLICATE_DECISION_ID" + + record = { + "decision_id": decision_id, + "timestamp": decision.get("timestamp") or datetime.now(timezone.utc).isoformat(), + "engine_version": decision["engine_version"], + "input_hash_bundle": decision["input_hash_bundle"], + "execution_mode": decision["execution_mode"], + "candidate_ids": decision["candidate_ids"], + "selected_transition_id": decision.get("selected_transition_id"), + "hard_blocks": decision.get("hard_blocks", []), + "transition_utility_krw": decision.get("transition_utility_krw"), + "operator_override": decision.get("operator_override", False), + "order_ids": decision.get("order_ids", []), + "fill_prices": decision.get("fill_prices", []), + "slippage": decision.get("slippage"), + "T1_return": None, + "T5_return": None, + "T20_return": None, + "MAE": None, + "MFE": None, + } + new_ledger = {**ledger, "records": ledger["records"] + [record]} + return new_ledger, "APPENDED" + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--ledger", default=str(DEFAULT_LEDGER)) + ap.add_argument("--decision", default=str(DEFAULT_DECISION)) + args = ap.parse_args() + + ledger = _load_ledger(Path(args.ledger)) + decision_doc = {} + decision_path = Path(args.decision) + if decision_path.exists(): + try: + decision_doc = json.loads(decision_path.read_text(encoding="utf-8")) + except Exception: + decision_doc = {} + + decision = { + "decision_id": decision_doc.get("packet_id") or decision_doc.get("formula_id"), + "engine_version": "PORTFOLIO_TRANSITION_UTILITY_V1", + "input_hash_bundle": decision_doc.get("input_hash") or "unknown", + "execution_mode": decision_doc.get("execution_mode") or decision_doc.get("final_action"), + "candidate_ids": [c.get("candidate_id") for c in decision_doc.get("candidate_actions", [])], + "selected_transition_id": (decision_doc.get("selected_transition") or {}).get("candidate_id"), + "hard_blocks": decision_doc.get("reason_codes", []), + "transition_utility_krw": (decision_doc.get("selected_transition") or {}).get("transition_utility_krw"), + } + + new_ledger, status = append_decision(ledger, decision) + new_ledger["status"] = status + out = Path(args.ledger) + if status == "APPENDED": + out.write_text(json.dumps(new_ledger, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps({"formula_id": "IMMUTABLE_DECISION_LEDGER_V1", "ledger_append_status": status}, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_model_governance_kill_switch_v1.py b/tools/build_model_governance_kill_switch_v1.py new file mode 100644 index 0000000..862aec9 --- /dev/null +++ b/tools/build_model_governance_kill_switch_v1.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""MODEL_GOVERNANCE_KILL_SWITCH_V1 — spec/formulas/domains/governance.yaml. + +Evaluates the 5 v8.9 kill-switch conditions and demotes execution_mode by exactly +one rung on the promotion ladder when any condition fires. No automatic promotion — +promotion requires an operator_override record (v8.9 V89_039). +governance/todo/v8_9_p1_adoption_plan.yaml P1-C.2. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_METRICS = ROOT / "Temp" / "model_governance_metrics_v1.json" +DEFAULT_DECISION_PACKET = ROOT / "Temp" / "final_decision_packet_active.json" +DEFAULT_OUT = ROOT / "Temp" / "model_governance_kill_switch_v1.json" + +PROMOTION_LADDER = ["AUDIT_ONLY", "SHADOW", "PILOT", "LIVE_LIMITED", "LIVE_FULL"] + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def evaluate_kill_switches(metrics: dict) -> list[str]: + triggered = [] + + quarantine = metrics.get("data_quarantine_rate_pct") + if quarantine is not None and quarantine > 5.0: + triggered.append("data_quarantine_rate_above_5pct") + + shortfall = metrics.get("implementation_shortfall_ratio") + if shortfall is not None and shortfall > 2.0: + triggered.append("implementation_shortfall_above_2x_expected") + + t5_hit_rate = metrics.get("t5_hit_rate_pct") + t5_sample_count = metrics.get("t5_sample_count") or 0 + if t5_hit_rate is not None and t5_sample_count >= 30 and t5_hit_rate < 50.0: + triggered.append("t5_hit_rate_below_50pct_for_30_trades") + + calibration_error = metrics.get("calibration_error") + calibration_limit = metrics.get("calibration_error_limit") + if calibration_error is not None and calibration_limit is not None and calibration_error > calibration_limit: + triggered.append("calibration_error_above_limit") + + mdd = metrics.get("account_mdd_pct") + mdd_budget = metrics.get("account_mdd_budget_pct") + if mdd is not None and mdd_budget is not None and mdd > mdd_budget: + triggered.append("unexpected_drawdown_breach") + + return triggered + + +def demote_one_rung(current_mode: str) -> str: + if current_mode not in PROMOTION_LADDER: + return "AUDIT_ONLY" + idx = PROMOTION_LADDER.index(current_mode) + return PROMOTION_LADDER[max(idx - 1, 0)] + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--metrics", default=str(DEFAULT_METRICS)) + ap.add_argument("--decision-packet", default=str(DEFAULT_DECISION_PACKET)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + metrics = _load(Path(args.metrics)) + decision_packet = _load(Path(args.decision_packet)) + + current_mode = decision_packet.get("execution_mode") or decision_packet.get("global_execution_gate") or "AUDIT_ONLY" + + if not metrics: + result = { + "formula_id": "MODEL_GOVERNANCE_KILL_SWITCH_V1", + "gate": "DATA_MISSING", + "execution_mode": current_mode, + "execution_mode_changed": False, + "kill_switch_triggered": False, + "kill_switch_reason_codes": [], + "source_paths": [str(Path(args.metrics)), str(Path(args.decision_packet))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + reason_codes = evaluate_kill_switches(metrics) + if reason_codes: + new_mode = demote_one_rung(current_mode) + else: + new_mode = current_mode + + result = { + "formula_id": "MODEL_GOVERNANCE_KILL_SWITCH_V1", + "gate": "KILL_SWITCH_TRIGGERED" if reason_codes else "PASS", + "execution_mode": new_mode, + "execution_mode_changed": new_mode != current_mode, + "kill_switch_triggered": bool(reason_codes), + "kill_switch_reason_codes": reason_codes, + "source_paths": [str(Path(args.metrics)), str(Path(args.decision_packet))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_portfolio_transition_optimizer_v1.py b/tools/build_portfolio_transition_optimizer_v1.py new file mode 100644 index 0000000..ed71fab --- /dev/null +++ b/tools/build_portfolio_transition_optimizer_v1.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""PORTFOLIO_TRANSITION_UTILITY_V1 — spec/formulas/domains/portfolio.yaml. + +Compiles candidate sell actions (from sell_waterfall_engine_v3/v4) and cash-repair +benefit (from smart_cash_recovery) plus a CE70 distribution input +(forecast_simulation_engine_v1, optional — may not exist yet while T+20 sample < 30) +into a single portfolio-level transition_utility_krw and a deterministic +selected_transition or NO_TRADE. + +Hard rule (AGENTS.md): missing required numeric input is never treated as zero. +If ce70_net_profit_krw is unavailable for every candidate, the optimizer still runs +on the cash/concentration-only benefit terms but cannot select a BUY-type transition. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_DECISION_PACKET = ROOT / "Temp" / "final_decision_packet_active.json" +DEFAULT_SELL_WATERFALL = ROOT / "Temp" / "sell_waterfall_engine_v3.json" +DEFAULT_CASH_RECOVERY = ROOT / "Temp" / "smart_cash_recovery_v9.json" +DEFAULT_SIMULATION = ROOT / "Temp" / "forecast_simulation_engine_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "portfolio_transition_optimizer_v1.json" + +HARD_VETO_ORDER = [ + "DATA_INVALID", + "EXECUTION_MODE_BLOCK", + "CASH_FLOOR_BLOCK", + "HARD_CONCENTRATION_BLOCK", + "NEGATIVE_TRANSITION_UTILITY", +] + +LIVE_ORDER_MODES = {"LIVE_LIMITED", "LIVE_FULL"} + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _candidate_actions_from_sell_waterfall(sell_waterfall: dict) -> list[dict]: + rows = sell_waterfall.get("rows") if isinstance(sell_waterfall.get("rows"), list) else [] + candidates = [] + for idx, row in enumerate(rows): + if not isinstance(row, dict): + continue + candidates.append( + { + "candidate_id": row.get("candidate_id") or f"SELL_{idx}", + "asset_id": row.get("종목명") or row.get("asset_id") or "UNKNOWN", + "action_type": row.get("매도유형") or row.get("action_type") or "SELL_CASH_REPAIR", + "planned_amount_krw": row.get("예상순현금") or row.get("planned_amount_krw"), + "source_signal_ids": ["SELL_WATERFALL_ENGINE_V3"], + "numeric_provenance_status": "PASS" if row.get("예상순현금") is not None else "DATA_MISSING", + } + ) + return candidates + + +def _hard_constraint_pass(candidate: dict, decision_packet: dict) -> tuple[bool, str | None]: + if candidate.get("numeric_provenance_status") != "PASS": + return False, "DATA_INVALID" + execution_mode = decision_packet.get("execution_mode") or decision_packet.get("global_execution_gate") + if execution_mode in (None, "DATA_MISSING"): + return False, "EXECUTION_MODE_BLOCK" + return True, None + + +def _transition_utility_krw( + candidate: dict, + ce70_net_profit_krw: float | None, + tax_fee_slippage_krw: float, + cash_repair_benefit_krw: float, + concentration_reduction_benefit_krw: float, + turnover_penalty_krw: float, +) -> float | None: + is_sell = str(candidate.get("action_type", "")).startswith("SELL") + if is_sell: + planned = candidate.get("planned_amount_krw") or 0.0 + return ( + float(planned) + + cash_repair_benefit_krw + + concentration_reduction_benefit_krw + - tax_fee_slippage_krw + - turnover_penalty_krw + ) + if ce70_net_profit_krw is None: + return None + return ( + ce70_net_profit_krw + - tax_fee_slippage_krw + + cash_repair_benefit_krw + + concentration_reduction_benefit_krw + - turnover_penalty_krw + ) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--decision-packet", default=str(DEFAULT_DECISION_PACKET)) + ap.add_argument("--sell-waterfall", default=str(DEFAULT_SELL_WATERFALL)) + ap.add_argument("--cash-recovery", default=str(DEFAULT_CASH_RECOVERY)) + ap.add_argument("--simulation", default=str(DEFAULT_SIMULATION)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + decision_packet = _load(Path(args.decision_packet)) + sell_waterfall = _load(Path(args.sell_waterfall)) + cash_recovery = _load(Path(args.cash_recovery)) + simulation = _load(Path(args.simulation)) + + source_paths = [ + str(Path(args.decision_packet)), + str(Path(args.sell_waterfall)), + str(Path(args.cash_recovery)), + str(Path(args.simulation)), + ] + + if not decision_packet: + result = { + "formula_id": "PORTFOLIO_TRANSITION_UTILITY_V1", + "gate": "NO_TRADE_AND_QUARANTINE", + "final_action": "NO_TRADE", + "reason_codes": ["missing_optimizer_inputs"], + "selected_transition": None, + "candidate_actions": [], + "source_paths": source_paths, + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + cash_repair_benefit_krw = float(cash_recovery.get("value_damage_pct_avg") or cash_recovery.get("cash_repair_benefit_krw") or 0.0) + tax_fee_slippage_krw = float(sell_waterfall.get("tax_fee_slippage_krw") or 0.0) + concentration_reduction_benefit_krw = 0.0 + turnover_penalty_krw = 0.0 + ce70_net_profit_krw = simulation.get("ce70_net_profit_krw") + if isinstance(ce70_net_profit_krw, str): + ce70_net_profit_krw = None + + candidates = _candidate_actions_from_sell_waterfall(sell_waterfall) + evaluated = [] + best = None + for candidate in candidates: + ok, veto_reason = _hard_constraint_pass(candidate, decision_packet) + utility = ( + _transition_utility_krw( + candidate, + ce70_net_profit_krw, + tax_fee_slippage_krw, + cash_repair_benefit_krw, + concentration_reduction_benefit_krw, + turnover_penalty_krw, + ) + if ok + else None + ) + if ok and utility is not None and utility <= 0: + ok = False + veto_reason = "NEGATIVE_TRANSITION_UTILITY" + row = { + **candidate, + "hard_constraint_pass": ok, + "veto_reason": veto_reason, + "transition_utility_krw": utility, + } + evaluated.append(row) + if ok and (best is None or (utility or 0) > (best["transition_utility_krw"] or 0)): + best = row + + if best is None: + final_action = "NO_TRADE" + reason_codes = ["NO_TRADE_BAND"] if candidates else ["missing_optimizer_inputs"] + selected_transition = None + else: + execution_mode = decision_packet.get("execution_mode") or decision_packet.get("global_execution_gate") + if execution_mode in LIVE_ORDER_MODES: + final_action = "LIVE_ORDER_REVIEW" + else: + final_action = "SHADOW_LEDGER_ONLY" + reason_codes = ["TRANSITION_UTILITY_POSITIVE"] + selected_transition = best + + result = { + "formula_id": "PORTFOLIO_TRANSITION_UTILITY_V1", + "default_action": "NO_TRADE", + "final_action": final_action, + "hard_veto_order": HARD_VETO_ORDER, + "reason_codes": reason_codes, + "candidate_actions": evaluated, + "selected_transition": selected_transition, + "source_paths": source_paths, + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_rebalance_cadence_gate_v1.py b/tools/build_rebalance_cadence_gate_v1.py new file mode 100644 index 0000000..dd8f67b --- /dev/null +++ b/tools/build_rebalance_cadence_gate_v1.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""REBALANCE_CADENCE_GATE_V1 — spec/formulas/domains/portfolio.yaml. + +Mandatory weekly (Sat/Sun) and monthly (1/11/21) cadence checks always emit a +review, but actual rebalancing execution is allowed only when transition utility +after tax cost is positive or a hard risk block is active. +governance/todo/v8_9_p3_adoption_plan.yaml P3-D. +""" +from __future__ import annotations + +import argparse +import json +from datetime import date +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUT = ROOT / "Temp" / "rebalance_cadence_gate_v1.json" + +WEEKLY_WEEKDAYS = {5, 6} # Saturday=5, Sunday=6 (date.weekday()) +MONTHLY_MID_CHECK_DAYS = {1, 11, 21} + + +def cadence_check_required(check_date: date, event_driven_trigger: bool = False) -> tuple[bool, str | None]: + if check_date.weekday() in WEEKLY_WEEKDAYS: + return True, "weekly_rebalance_required" + if check_date.day in MONTHLY_MID_CHECK_DAYS: + return True, "mid_check_required" + if event_driven_trigger: + return True, "event_driven_trigger" + return False, None + + +def evaluate_rebalance_gate( + check_date: date, + transition_utility_after_tax_cost_krw: float | None, + hard_risk_block_active: bool | None, + event_driven_trigger: bool = False, +) -> dict: + required, reason = cadence_check_required(check_date, event_driven_trigger) + if not required: + return { + "cadence_check_required": False, + "review_emitted": False, + "rebalance_execution_allowed": False, + "cadence_trigger_reason": None, + } + + if transition_utility_after_tax_cost_krw is None and hard_risk_block_active is None: + return { + "cadence_check_required": True, + "review_emitted": True, + "rebalance_execution_allowed": False, + "cadence_trigger_reason": reason, + "gate": "DATA_MISSING", + } + + allowed = bool(hard_risk_block_active) or ( + transition_utility_after_tax_cost_krw is not None and transition_utility_after_tax_cost_krw > 0 + ) + return { + "cadence_check_required": True, + "review_emitted": True, + "rebalance_execution_allowed": allowed, + "cadence_trigger_reason": reason, + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--date", default=None, help="YYYY-MM-DD, default today") + ap.add_argument("--transition-utility", type=float, default=None) + ap.add_argument("--hard-risk-block", action="store_true") + ap.add_argument("--event-driven-trigger", action="store_true") + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + check_date = date.fromisoformat(args.date) if args.date else date.today() + result = { + "formula_id": "REBALANCE_CADENCE_GATE_V1", + **evaluate_rebalance_gate(check_date, args.transition_utility, args.hard_risk_block, args.event_driven_trigger), + "check_date": check_date.isoformat(), + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_scenario_shock_matrix_v1.py b/tools/build_scenario_shock_matrix_v1.py new file mode 100644 index 0000000..feaa2da --- /dev/null +++ b/tools/build_scenario_shock_matrix_v1.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""SCENARIO_SHOCK_MATRIX_V1 — spec/formulas/domains/simulation.yaml. + +Applies 5 deterministic stress multipliers to a base net-profit distribution to +produce per-scenario CE70/CVaR95. governance/todo/v8_9_p2_adoption_plan.yaml P2-A. + +Hard rule (AGENTS.md): no base distribution -> all scenarios DATA_MISSING. Never +fabricate a shocked distribution from nothing. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_SIMULATION = ROOT / "Temp" / "forecast_simulation_engine_v1.json" +DEFAULT_DISTRIBUTION = ROOT / "Temp" / "net_profit_distribution_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "scenario_shock_matrix_v1.json" + +SCENARIO_DEFINITIONS = { + "base_case": {"shock_multiplier": 1.0}, + "adverse_case": {"shock_multiplier": 1.5}, + "liquidity_drought_case": {"shock_multiplier": 1.3, "capacity_derate_pct": 40}, + "crisis_case": {"shock_multiplier": 2.0, "correlation_to_one": True}, + "fx_shock_case": {"shock_multiplier": 1.2, "applies_only_to": "foreign_assets"}, + "tax_cost_case": {"shock_multiplier": 1.0, "additional_cost_pct": 5}, +} + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _quantile(sorted_values: list[float], q: float) -> float: + if len(sorted_values) == 1: + return sorted_values[0] + pos = q * (len(sorted_values) - 1) + lower = int(pos) + upper = min(lower + 1, len(sorted_values) - 1) + frac = pos - lower + return sorted_values[lower] + (sorted_values[upper] - sorted_values[lower]) * frac + + +def _cvar95(sorted_values: list[float]) -> float: + threshold_idx = max(1, int(len(sorted_values) * 0.05)) + tail = sorted_values[:threshold_idx] + return sum(tail) / len(tail) + + +def apply_shock(distribution: list[float], scenario_id: str) -> list[float]: + scenario = SCENARIO_DEFINITIONS[scenario_id] + multiplier = scenario["shock_multiplier"] + additional_cost_pct = scenario.get("additional_cost_pct", 0) + shocked = [] + for v in distribution: + value = v * multiplier if v < 0 else v / multiplier + if additional_cost_pct: + value -= abs(value) * (additional_cost_pct / 100.0) + shocked.append(value) + return shocked + + +def evaluate_scenario(distribution: list[float] | None, scenario_id: str) -> dict: + if not distribution: + return { + "scenario_id": scenario_id, + "scenario_ce70_krw": None, + "scenario_cvar95_krw": None, + "gate": "DATA_MISSING", + } + shocked = sorted(apply_shock(distribution, scenario_id)) + return { + "scenario_id": scenario_id, + "scenario_ce70_krw": _quantile(shocked, 0.30), + "scenario_cvar95_krw": _cvar95(shocked), + "gate": "PASS", + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--distribution", default=str(DEFAULT_DISTRIBUTION)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + doc = _load(Path(args.distribution)) + distribution = doc.get("net_profit_distribution_after_tax_fee_slippage") + distribution = [float(v) for v in distribution] if isinstance(distribution, list) and distribution else None + + scenario_results = [evaluate_scenario(distribution, scenario_id) for scenario_id in SCENARIO_DEFINITIONS] + + result = { + "formula_id": "SCENARIO_SHOCK_MATRIX_V1", + "gate": "PASS" if distribution else "DATA_MISSING", + "scenario_results": scenario_results, + "source_paths": [str(Path(args.distribution))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_sector_exposure_graph_v1.py b/tools/build_sector_exposure_graph_v1.py new file mode 100644 index 0000000..6dc5e98 --- /dev/null +++ b/tools/build_sector_exposure_graph_v1.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""SECTOR_EXPOSURE_GRAPH_V1 + LEADER_LIFECYCLE_GATE_V1 — spec/formulas/domains/sector.yaml. + +ETF lookthrough exposure + factor beta residualization + leader role promotion/demotion. +governance/todo/v8_9_p1_adoption_plan.yaml P1-A.3. + +Hard rule (AGENTS.md): missing ETF constituents or peer betas are never assumed zero. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_POSITIONS = ROOT / "Temp" / "sector_exposure_positions_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "sector_exposure_graph_v1.json" + +PROMOTION_PATH = ["LAGGARD", "CYCLICAL_BETA", "ENABLER", "CORE_LEADER", "CAPTAIN"] + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def sector_exposure(position: dict) -> dict: + direct_weight_pct = float(position.get("direct_weight_pct") or 0.0) + etf_constituents = position.get("etf_constituents_json") + etf_weight_pct = position.get("etf_weight_pct") + sector_id = position.get("sector_id") + + if etf_constituents is None or etf_weight_pct is None: + return { + "sector_id": sector_id, + "direct_weight_pct": direct_weight_pct, + "lookthrough_etf_weight_pct": None, + "sector_family_total_pct": None, + "gate": "ETF_BUY_BLOCKED", + "reason_code": "constituents_missing", + } + + lookthrough = sum( + float(c.get("weight_pct", 0.0)) * float(etf_weight_pct) / 100.0 + for c in etf_constituents + if isinstance(c, dict) and c.get("sector_id") == sector_id + ) + sector_family_total_pct = direct_weight_pct + lookthrough + return { + "sector_id": sector_id, + "direct_weight_pct": direct_weight_pct, + "lookthrough_etf_weight_pct": lookthrough, + "sector_family_total_pct": sector_family_total_pct, + "gate": "PASS", + } + + +def residualize_factor_beta(factor_beta_raw: float, peer_sector_betas: list | None) -> tuple[float | None, str]: + if peer_sector_betas is None: + return factor_beta_raw, "PARTIAL_raw_beta_peer_data_missing" + shared_variance = sum(float(p.get("shared_variance", 0.0)) for p in peer_sector_betas if isinstance(p, dict)) + return factor_beta_raw - shared_variance, "PASS" + + +def evaluate_leader_role(position: dict) -> dict: + required_fields = [ + "relative_strength_leads_sector", + "volume_quality_confirmed", + "above_ma60_or_reclaim_confirmed", + "earnings_revision_status", + "institutional_flow_status", + ] + current_role = position.get("current_role") or "LAGGARD" + if any(position.get(f) is None for f in required_fields): + return {"leader_role": current_role, "role_transition_reason": "DATA_MISSING", "role_changed": False} + + demotion = ( + (position["above_ma60_or_reclaim_confirmed"] is False and position["institutional_flow_status"] == "distribution") + or position["earnings_revision_status"] == "negative" + or ( + position["institutional_flow_status"] == "distribution" + and current_role in ("CAPTAIN", "CORE_LEADER") + ) + ) + if demotion: + return { + "leader_role": "DISTRIBUTION_RISK", + "role_transition_reason": "demotion_trigger", + "role_changed": current_role != "DISTRIBUTION_RISK", + } + + promotion_ok = ( + position["relative_strength_leads_sector"] is True + and position["volume_quality_confirmed"] is True + and position["above_ma60_or_reclaim_confirmed"] is True + and position["earnings_revision_status"] != "negative" + and position["institutional_flow_status"] != "distribution" + ) + if promotion_ok and current_role in PROMOTION_PATH: + idx = PROMOTION_PATH.index(current_role) + next_role = PROMOTION_PATH[min(idx + 1, len(PROMOTION_PATH) - 1)] + return { + "leader_role": next_role, + "role_transition_reason": "promotion_requires_all_satisfied", + "role_changed": next_role != current_role, + } + + return {"leader_role": current_role, "role_transition_reason": "no_change", "role_changed": False} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--positions", default=str(DEFAULT_POSITIONS)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + doc = _load(Path(args.positions)) + positions = doc.get("positions") if isinstance(doc.get("positions"), list) else [] + + rows = [] + for position in positions: + if not isinstance(position, dict): + continue + exposure = sector_exposure(position) + leader = evaluate_leader_role(position) + beta_residualized, beta_status = residualize_factor_beta( + float(position.get("factor_beta_raw") or 0.0), position.get("peer_sector_betas") + ) + rows.append({**exposure, **leader, "factor_beta_residualized": beta_residualized, "beta_status": beta_status}) + + result = { + "formula_id": "SECTOR_EXPOSURE_GRAPH_V1", + "gate": "PASS" if rows else "DATA_MISSING", + "rows": rows, + "source_paths": [str(Path(args.positions))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_sell_waterfall_engine_v4.py b/tools/build_sell_waterfall_engine_v4.py new file mode 100644 index 0000000..3a27d4a --- /dev/null +++ b/tools/build_sell_waterfall_engine_v4.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""SELL_LOT_PARETO_SELECTOR_V1 — spec/formulas/domains/cash.yaml. + +Extends tools/build_sell_waterfall_engine_v3.py output with lot-level scoring +(tax_loss_benefit, missed_upside_penalty, reentry_cost) and a Pareto dominance +ranking within each hard_precedence stage, per +governance/todo/v8_9_p0_adoption_plan.yaml P0-2.2. + +Backward compatible: every row from v3 is preserved unchanged; only new fields +are appended. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_V3 = ROOT / "Temp" / "sell_waterfall_engine_v3.json" +DEFAULT_OUT = ROOT / "Temp" / "sell_waterfall_engine_v4.json" + +MAXIMIZE_FIELDS = [ + "avoided_tail_loss_krw", + "cash_repair_benefit_krw", + "concentration_reduction_benefit_krw", + "tax_loss_benefit_krw", +] +MINIMIZE_FIELDS = [ + "tax_fee_slippage_krw", + "reentry_cost_krw", + "missed_upside_penalty_krw", +] + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _numeric_or_missing(row: dict, field: str) -> tuple[float, bool]: + value = row.get(field) + if value is None: + return 0.0, True + try: + return float(value), False + except (TypeError, ValueError): + return 0.0, True + + +def _lot_sell_score(row: dict) -> tuple[float, list[str]]: + missing_fields = [] + values = {} + for field in MAXIMIZE_FIELDS + MINIMIZE_FIELDS: + value, missing = _numeric_or_missing(row, field) + values[field] = value + if missing: + missing_fields.append(field) + score = ( + values["avoided_tail_loss_krw"] + + values["cash_repair_benefit_krw"] + + values["concentration_reduction_benefit_krw"] + + values["tax_loss_benefit_krw"] + - values["tax_fee_slippage_krw"] + - values["reentry_cost_krw"] + - values["missed_upside_penalty_krw"] + ) + return score, missing_fields + + +def _dominates(a: dict, b: dict) -> bool: + at_least_as_good = all(a.get(f, 0.0) >= b.get(f, 0.0) for f in MAXIMIZE_FIELDS) and all( + a.get(f, 0.0) <= b.get(f, 0.0) for f in MINIMIZE_FIELDS + ) + strictly_better = any(a.get(f, 0.0) > b.get(f, 0.0) for f in MAXIMIZE_FIELDS) or any( + a.get(f, 0.0) < b.get(f, 0.0) for f in MINIMIZE_FIELDS + ) + return at_least_as_good and strictly_better + + +def _rank_pareto_group(rows: list[dict]) -> list[dict]: + annotated = [] + for row in rows: + dominated_by = [ + other["candidate_id"] + for other in rows + if other is not row and _dominates(other, row) + ] + annotated.append({**row, "pareto_dominated": bool(dominated_by), "dominated_by": dominated_by}) + annotated.sort( + key=lambda r: ( + r["pareto_dominated"], + -r["lot_sell_score_krw"], + r.get("tax_fee_slippage_krw", 0.0), + r.get("reentry_cost_krw", 0.0), + ) + ) + for idx, row in enumerate(annotated, start=1): + row["pareto_rank"] = idx + return annotated + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--base", default=str(DEFAULT_V3)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + base = _load(Path(args.base)) + rows = base.get("rows") if isinstance(base.get("rows"), list) else [] + + scored_rows = [] + for idx, row in enumerate(rows): + if not isinstance(row, dict): + continue + candidate_id = row.get("candidate_id") or row.get("종목명") or f"LOT_{idx}" + score, missing_fields = _lot_sell_score(row) + scored_rows.append( + { + **row, + "candidate_id": candidate_id, + **{f: _numeric_or_missing(row, f)[0] for f in MAXIMIZE_FIELDS + MINIMIZE_FIELDS}, + "lot_sell_score_krw": score, + "lot_sell_score_missing_fields": missing_fields, + "hard_precedence_stage": row.get("hard_precedence_stage") or row.get("우선순위단계"), + } + ) + + groups: dict[object, list[dict]] = {} + for row in scored_rows: + groups.setdefault(row.get("hard_precedence_stage"), []).append(row) + + ranked_rows: list[dict] = [] + for _stage, group_rows in groups.items(): + ranked_rows.extend(_rank_pareto_group(group_rows)) + + result = { + "formula_id": "SELL_LOT_PARETO_SELECTOR_V1", + "gate": base.get("gate") or "PASS", + "rows": ranked_rows, + "source_paths": [str(Path(args.base))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_state_vector_constructor_v1.py b/tools/build_state_vector_constructor_v1.py new file mode 100644 index 0000000..f863e58 --- /dev/null +++ b/tools/build_state_vector_constructor_v1.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""STATE_VECTOR_CONSTRUCTOR_V1 — spec/formulas/domains/portfolio.yaml. + +Merges holdings/cash/tax_lots/sector_graph/factor_exposures/macro_regime_probabilities +into one state_vector. Missing components stay null -- never backfilled from another +component. governance/todo/v8_9_p3_adoption_plan.yaml P3-A. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_CASH_LADDER = ROOT / "Temp" / "cash_ratios_v1.json" +DEFAULT_POSITIONS = ROOT / "Temp" / "account_snapshot_v1.json" +DEFAULT_SECTOR_GRAPH = ROOT / "Temp" / "sector_exposure_graph_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "state_vector_constructor_v1.json" + +COMPONENT_KEYS = [ + "cash_ladder", + "positions", + "sector_exposure_graph", + "factor_exposures", + "tax_lots", + "risk_bucket_weights", + "macro_regime_probabilities", + "goal_progress_pct", +] + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def construct_state_vector(components: dict) -> dict: + state_vector = {} + missing_components = [] + for key in COMPONENT_KEYS: + value = components.get(key) + state_vector[key] = value + if value is None: + missing_components.append(key) + completeness_pct = 100.0 * (len(COMPONENT_KEYS) - len(missing_components)) / len(COMPONENT_KEYS) + return { + "state_vector": state_vector, + "state_vector_completeness_pct": completeness_pct, + "missing_components": missing_components, + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--cash-ladder", default=str(DEFAULT_CASH_LADDER)) + ap.add_argument("--positions", default=str(DEFAULT_POSITIONS)) + ap.add_argument("--sector-graph", default=str(DEFAULT_SECTOR_GRAPH)) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + cash_ladder_doc = _load(Path(args.cash_ladder)) + positions_doc = _load(Path(args.positions)) + sector_graph_doc = _load(Path(args.sector_graph)) + + components = { + "cash_ladder": cash_ladder_doc or None, + "positions": positions_doc.get("positions") if positions_doc else None, + "sector_exposure_graph": sector_graph_doc.get("rows") if sector_graph_doc.get("rows") else None, + "factor_exposures": None, + "tax_lots": positions_doc.get("tax_lots") if positions_doc else None, + "risk_bucket_weights": None, + "macro_regime_probabilities": None, + "goal_progress_pct": positions_doc.get("goal_progress_pct") if positions_doc else None, + } + + result = {"formula_id": "STATE_VECTOR_CONSTRUCTOR_V1", **construct_state_vector(components)} + result["source_paths"] = [str(Path(args.cash_ladder)), str(Path(args.positions)), str(Path(args.sector_graph))] + + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_transition_set_enumerator_v1.py b/tools/build_transition_set_enumerator_v1.py new file mode 100644 index 0000000..86f64f9 --- /dev/null +++ b/tools/build_transition_set_enumerator_v1.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""TRANSITION_SET_ENUMERATOR_V1 — spec/formulas/domains/portfolio.yaml. + +Evaluates combinations of already-vetoed candidates as a portfolio-level set, +so individually-passing candidates that jointly breach cash floor or +concentration caps are rejected. governance/todo/v8_9_p2_adoption_plan.yaml P2-B. +""" +from __future__ import annotations + +import argparse +import itertools +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_TRANSITION_OPTIMIZER = ROOT / "Temp" / "portfolio_transition_optimizer_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "transition_set_enumerator_v1.json" + +COMPLEXITY_PENALTY_RATE_KRW = 5000.0 + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def post_trade_mrc(transition_set: tuple[dict, ...], portfolio_total_risk_budget: float) -> float | None: + if not portfolio_total_risk_budget: + return None + total = sum(c.get("marginal_risk_contribution", 0.0) for c in transition_set) + return total / portfolio_total_risk_budget + + +def post_trade_cvar95_krw(transition_set: tuple[dict, ...]) -> float | None: + values = [c.get("cvar95_loss_krw") for c in transition_set] + if any(v is None for v in values): + return None + return sum(values) + + +def set_hard_constraint_pass( + transition_set: tuple[dict, ...], + portfolio_total_risk_budget: float = 0.0, + mrc_cap: float = 1.0, + cvar95_budget_krw: float | None = None, +) -> bool: + post_trade_cash_floor_pct = sum(c.get("post_trade_cash_floor_delta_pct", 0.0) for c in transition_set) + post_trade_concentration_pct = sum(c.get("post_trade_concentration_delta_pct", 0.0) for c in transition_set) + if post_trade_cash_floor_pct < 0 or post_trade_concentration_pct > 0: + return False + + mrc = post_trade_mrc(transition_set, portfolio_total_risk_budget) + if mrc is not None and mrc > mrc_cap: + return False + + cvar95 = post_trade_cvar95_krw(transition_set) + if cvar95 is not None and cvar95_budget_krw is not None and cvar95 < -abs(cvar95_budget_krw): + return False + + return True + + +def set_transition_utility_krw(transition_set: tuple[dict, ...]) -> float: + total = sum(c.get("transition_utility_krw") or 0.0 for c in transition_set) + combination_penalty = COMPLEXITY_PENALTY_RATE_KRW * (len(transition_set) - 1) + return total - combination_penalty + + +def enumerate_transition_sets( + candidates: list[dict], + max_set_size: int = 3, + portfolio_total_risk_budget: float = 0.0, + mrc_cap: float = 1.0, + cvar95_budget_krw: float | None = None, +) -> list[dict]: + passing = [c for c in candidates if c.get("hard_constraint_pass") is True] + evaluated_sets = [] + for size in range(1, min(max_set_size, len(passing)) + 1): + for combo in itertools.combinations(passing, size): + evaluated_sets.append( + { + "candidate_ids": [c.get("candidate_id") for c in combo], + "set_hard_constraint_pass": set_hard_constraint_pass( + combo, portfolio_total_risk_budget, mrc_cap, cvar95_budget_krw + ), + "set_transition_utility_krw": set_transition_utility_krw(combo), + "post_trade_mrc": post_trade_mrc(combo, portfolio_total_risk_budget), + "post_trade_cvar95_krw": post_trade_cvar95_krw(combo), + } + ) + return evaluated_sets + + +def select_best_set(evaluated_sets: list[dict]) -> dict | None: + passing_sets = [s for s in evaluated_sets if s["set_hard_constraint_pass"]] + if not passing_sets: + return None + return max( + passing_sets, + key=lambda s: (s["set_transition_utility_krw"], -len(s["candidate_ids"])), + ) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--transition-optimizer", default=str(DEFAULT_TRANSITION_OPTIMIZER)) + ap.add_argument("--max-set-size", type=int, default=3) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + doc = _load(Path(args.transition_optimizer)) + candidates = doc.get("candidate_actions") if isinstance(doc.get("candidate_actions"), list) else [] + + if not candidates: + result = { + "formula_id": "TRANSITION_SET_ENUMERATOR_V1", + "gate": "NO_TRADE", + "selected_transition_set": [], + "rejected_sets_count": 0, + "source_paths": [str(Path(args.transition_optimizer))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + evaluated_sets = enumerate_transition_sets(candidates, args.max_set_size) + best = select_best_set(evaluated_sets) + rejected_count = len(evaluated_sets) - (1 if best else 0) + + result = { + "formula_id": "TRANSITION_SET_ENUMERATOR_V1", + "gate": "PASS" if best else "NO_TRADE", + "selected_transition_set": best["candidate_ids"] if best else [], + "set_transition_utility_krw": best["set_transition_utility_krw"] if best else None, + "post_trade_mrc": best["post_trade_mrc"] if best else None, + "post_trade_cvar95_krw": best["post_trade_cvar95_krw"] if best else None, + "rejected_sets_count": rejected_count, + "source_paths": [str(Path(args.transition_optimizer))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_walk_forward_bootstrap_v1.py b/tools/build_walk_forward_bootstrap_v1.py new file mode 100644 index 0000000..edbe116 --- /dev/null +++ b/tools/build_walk_forward_bootstrap_v1.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""WALK_FORWARD_BOOTSTRAP_V1 — spec/formulas/domains/simulation.yaml. + +Generates net_profit_distribution_after_tax_fee_slippage from historical_returns via +walk-forward (non-overlapping in/out-of-sample split, block resample on out-of-sample +only) or regime-matched (filter + resample-with-replacement) bootstrapping. +governance/todo/v8_9_p3_adoption_plan.yaml P3-B. + +Hard rule: no historical_returns or fewer than 2 samples -> DATA_MISSING. Never +interpolate or fabricate a distribution. +""" +from __future__ import annotations + +import argparse +import json +import random +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_HISTORICAL_RETURNS = ROOT / "Temp" / "historical_returns_v1.json" +DEFAULT_OUT = ROOT / "Temp" / "walk_forward_bootstrap_v1.json" + +BLOCK_SIZE = 5 + + +def _load(path: Path) -> dict: + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def walk_forward_resample(historical_returns: list[dict], resample_count: int, rng: random.Random) -> list[float]: + sorted_returns = sorted(historical_returns, key=lambda r: r["date"]) + split_idx = int(len(sorted_returns) * 0.7) + out_of_sample = sorted_returns[split_idx:] + if len(out_of_sample) < 2: + return [] + + values = [r["net_return_after_cost_pct"] for r in out_of_sample] + distribution = [] + for _ in range(resample_count): + start = rng.randrange(0, max(1, len(values) - BLOCK_SIZE + 1)) + block = values[start:start + BLOCK_SIZE] + distribution.append(sum(block) / len(block)) + return distribution + + +def regime_matched_resample( + historical_returns: list[dict], current_regime_state: str, resample_count: int, rng: random.Random +) -> list[float]: + filtered = [r["net_return_after_cost_pct"] for r in historical_returns if r.get("regime_state") == current_regime_state] + if len(filtered) < 2: + return [] + return [rng.choice(filtered) for _ in range(resample_count)] + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--historical-returns", default=str(DEFAULT_HISTORICAL_RETURNS)) + ap.add_argument("--current-regime-state", default=None) + ap.add_argument("--bootstrap-method", default="walk_forward", choices=["walk_forward", "regime_matched"]) + ap.add_argument("--resample-count", type=int, default=1000) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + ap.add_argument("--seed", type=int, default=None) + args = ap.parse_args() + + doc = _load(Path(args.historical_returns)) + historical_returns = doc.get("historical_returns") if isinstance(doc.get("historical_returns"), list) else None + + if not historical_returns or len(historical_returns) < 2: + result = { + "formula_id": "WALK_FORWARD_BOOTSTRAP_V1", + "gate": "DATA_MISSING", + "net_profit_distribution_after_tax_fee_slippage": None, + "sample_count_total": len(historical_returns) if historical_returns else 0, + "sample_count_same_regime": 0, + "source_paths": [str(Path(args.historical_returns))], + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + rng = random.Random(args.seed) + if args.bootstrap_method == "walk_forward": + distribution = walk_forward_resample(historical_returns, args.resample_count, rng) + else: + distribution = regime_matched_resample(historical_returns, args.current_regime_state, args.resample_count, rng) + + sample_count_same_regime = len( + [r for r in historical_returns if r.get("regime_state") == args.current_regime_state] + ) + + if not distribution: + result = { + "formula_id": "WALK_FORWARD_BOOTSTRAP_V1", + "gate": "DATA_MISSING", + "net_profit_distribution_after_tax_fee_slippage": None, + "sample_count_total": len(historical_returns), + "sample_count_same_regime": sample_count_same_regime, + "source_paths": [str(Path(args.historical_returns))], + } + else: + result = { + "formula_id": "WALK_FORWARD_BOOTSTRAP_V1", + "gate": "PASS", + "net_profit_distribution_after_tax_fee_slippage": distribution, + "sample_count_total": len(historical_returns), + "sample_count_same_regime": sample_count_same_regime, + "source_paths": [str(Path(args.historical_returns))], + } + + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_weekly_legacy_transfer_plan_v1.py b/tools/build_weekly_legacy_transfer_plan_v1.py new file mode 100644 index 0000000..5d71e1a --- /dev/null +++ b/tools/build_weekly_legacy_transfer_plan_v1.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""WEEKLY_LEGACY_TRANSFER_PLAN_V1 — spec/formulas/domains/cash.yaml. + +A weekly legacy-stock-to-CMA transfer plan is a planning input, not deployable +cash, until the deposit is actually confirmed. governance/todo/v8_9_p3_adoption_plan.yaml P3-E. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUT = ROOT / "Temp" / "weekly_legacy_transfer_plan_v1.json" + + +def evaluate_transfer_plan( + weekly_legacy_to_cma_transfer_plan_krw: float, + transfer_confirmed: bool | None, + transfer_confirmed_amount_krw: float | None, +) -> dict: + confirmed = bool(transfer_confirmed) + if not confirmed: + return { + "deployable_cash_contribution_krw": 0.0, + "plan_status": "PLANNED_NOT_DEPLOYABLE", + "planned_amount_krw": weekly_legacy_to_cma_transfer_plan_krw, + } + amount = transfer_confirmed_amount_krw if transfer_confirmed_amount_krw is not None else 0.0 + return { + "deployable_cash_contribution_krw": amount, + "plan_status": "CONFIRMED_DEPLOYABLE", + "planned_amount_krw": weekly_legacy_to_cma_transfer_plan_krw, + } + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--planned-amount", type=float, default=4000000.0) + ap.add_argument("--transfer-confirmed", action="store_true") + ap.add_argument("--confirmed-amount", type=float, default=None) + ap.add_argument("--out", default=str(DEFAULT_OUT)) + args = ap.parse_args() + + result = { + "formula_id": "WEEKLY_LEGACY_TRANSFER_PLAN_V1", + **evaluate_transfer_plan(args.planned_amount, args.transfer_confirmed, args.confirmed_amount), + } + out = Path(args.out) + out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())