feat(quant-engine): v8.9 제안서 P0-P3 로드맵 채택 — 15개 의사결정 엔진 신규 구현
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 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,10 @@ spec_files:
|
|||||||
formula_domain_fundamental: "spec/formulas/domains/fundamental.yaml"
|
formula_domain_fundamental: "spec/formulas/domains/fundamental.yaml"
|
||||||
formula_domain_smart_money: "spec/formulas/domains/smart_money.yaml"
|
formula_domain_smart_money: "spec/formulas/domains/smart_money.yaml"
|
||||||
formula_domain_macro: "spec/formulas/domains/macro.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"
|
harness_formula_registry: "spec/13b_harness_formulas.yaml"
|
||||||
raw_workbook_mapping: "spec/14_raw_workbook_mapping.yaml"
|
raw_workbook_mapping: "spec/14_raw_workbook_mapping.yaml"
|
||||||
account_snapshot_contract: "spec/15_account_snapshot_contract.yaml"
|
account_snapshot_contract: "spec/15_account_snapshot_contract.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
|
||||||
@@ -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]
|
||||||
@@ -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]
|
||||||
@@ -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]
|
||||||
@@ -15,6 +15,18 @@ active_aliases:
|
|||||||
final_decision_packet_active: Temp/final_decision_packet_active.json
|
final_decision_packet_active: Temp/final_decision_packet_active.json
|
||||||
source_precedence:
|
source_precedence:
|
||||||
- final_decision_packet_active
|
- 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
|
- final_execution_decision_v2
|
||||||
- smart_cash_recovery_v7
|
- smart_cash_recovery_v7
|
||||||
- smart_cash_recovery_v6
|
- smart_cash_recovery_v6
|
||||||
@@ -42,3 +54,39 @@ manifest_rows:
|
|||||||
- formula_id: smart_cash_recovery_v7
|
- formula_id: smart_cash_recovery_v7
|
||||||
active_artifact: Temp/smart_cash_recovery_v9.json
|
active_artifact: Temp/smart_cash_recovery_v9.json
|
||||||
value: 57841575
|
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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
+115
-5
@@ -9,7 +9,7 @@ meta:
|
|||||||
각 상태는 통과 조건, 실패 시 행동, 참조 파일을 가진다.
|
각 상태는 통과 조건, 실패 시 행동, 참조 파일을 가진다.
|
||||||
|
|
||||||
decision_flow:
|
decision_flow:
|
||||||
initial_state: "INPUT_VALIDATION"
|
initial_state: "MODEL_GOVERNANCE_GATE"
|
||||||
terminal_states: ["FINAL_DECISION", "INSUFFICIENT_DATA", "BLOCKED"]
|
terminal_states: ["FINAL_DECISION", "INSUFFICIENT_DATA", "BLOCKED"]
|
||||||
deterministic_execution_control:
|
deterministic_execution_control:
|
||||||
purpose: "텍스트 해석 차이로 매번 다른 결론이 나오는 것을 줄이기 위한 결정 추적·동률 처리·first-match 규칙."
|
purpose: "텍스트 해석 차이로 매번 다른 결론이 나오는 것을 줄이기 위한 결정 추적·동률 처리·first-match 규칙."
|
||||||
@@ -37,6 +37,18 @@ decision_flow:
|
|||||||
null_propagation_rule: "필수 입력이 null이면 해당 계산은 null로 유지하고 prohibited_calculations에 사유를 남긴다. null을 0으로 대체 금지."
|
null_propagation_rule: "필수 입력이 null이면 해당 계산은 null로 유지하고 prohibited_calculations에 사유를 남긴다. null을 0으로 대체 금지."
|
||||||
output_requirement: "OUTPUT_VALIDATION에서 decision_trace 누락 시 schema_validation_status=FAIL."
|
output_requirement: "OUTPUT_VALIDATION에서 decision_trace 누락 시 schema_validation_status=FAIL."
|
||||||
states:
|
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:
|
INPUT_VALIDATION:
|
||||||
purpose: "요청, 기준일, 계좌, 보유수량, 가격/수급/ATR 입력 존재 여부 확인"
|
purpose: "요청, 기준일, 계좌, 보유수량, 가격/수급/ATR 입력 존재 여부 확인"
|
||||||
required_refs:
|
required_refs:
|
||||||
@@ -57,6 +69,20 @@ decision_flow:
|
|||||||
computed_outputs: ["data_completeness_matrix", "missing_fields", "field_unit_conflicts"]
|
computed_outputs: ["data_completeness_matrix", "missing_fields", "field_unit_conflicts"]
|
||||||
pass_condition: "data_completeness_matrix produced"
|
pass_condition: "data_completeness_matrix produced"
|
||||||
fail_state: "INSUFFICIENT_DATA"
|
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:
|
HARD_FILTER_CHECK:
|
||||||
purpose: "하드 필터를 점수보다 먼저 적용"
|
purpose: "하드 필터를 점수보다 먼저 적용"
|
||||||
required_refs:
|
required_refs:
|
||||||
@@ -130,6 +156,18 @@ decision_flow:
|
|||||||
- "trim_assignments 없이 현금 부족 해소 주장 금지"
|
- "trim_assignments 없이 현금 부족 해소 주장 금지"
|
||||||
- "QEH_AUDIT_BLOCK.SELL_PRIORITY_V1 행에 배정 결과 요약 필수"
|
- "QEH_AUDIT_BLOCK.SELL_PRIORITY_V1 행에 배정 결과 요약 필수"
|
||||||
- "1순위 소진 전 2순위 배정 금지 (sell_priority_engine 순서 준수)"
|
- "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:
|
POSITION_SIZING:
|
||||||
purpose: "ATR20·현금·목표비중·유동성으로 정수 수량 산출"
|
purpose: "ATR20·현금·목표비중·유동성으로 정수 수량 산출"
|
||||||
required_refs:
|
required_refs:
|
||||||
@@ -175,6 +213,44 @@ decision_flow:
|
|||||||
- "CRITICAL_ALERT 시 코어 포지션 포함 전면 재검토 강제"
|
- "CRITICAL_ALERT 시 코어 포지션 포함 전면 재검토 강제"
|
||||||
- "LLM이 레이더 결과를 완화하는 서사 출력 금지 (Section B 해설만 허용)"
|
- "LLM이 레이더 결과를 완화하는 서사 출력 금지 (Section B 해설만 허용)"
|
||||||
- "RADAR_MISSING(데이터 부족) 시 soft-block: 보유 포지션 수동 점검 권고 문구 출력"
|
- "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:
|
OUTPUT_VALIDATION:
|
||||||
purpose: "JSON Schema와 HTS 표 필드 검증"
|
purpose: "JSON Schema와 HTS 표 필드 검증"
|
||||||
required_refs:
|
required_refs:
|
||||||
@@ -188,25 +264,38 @@ decision_flow:
|
|||||||
purpose: "BUY/HOLD/SELL/TRIM/ROTATE/AVOID/WATCH/INSUFFICIENT_DATA 중 하나로 결론"
|
purpose: "BUY/HOLD/SELL/TRIM/ROTATE/AVOID/WATCH/INSUFFICIENT_DATA 중 하나로 결론"
|
||||||
required_refs:
|
required_refs:
|
||||||
- "spec/07_output_schema.yaml:recommendation_grade"
|
- "spec/07_output_schema.yaml:recommendation_grade"
|
||||||
|
- "spec/formulas/domains/governance.yaml:IMMUTABLE_DECISION_LEDGER_V1"
|
||||||
output_required:
|
output_required:
|
||||||
- "final_action"
|
- "final_action"
|
||||||
- "grade"
|
- "grade"
|
||||||
- "orders or prohibited_calculations"
|
- "orders or prohibited_calculations"
|
||||||
- "rules_used"
|
- "rules_used"
|
||||||
|
- "ledger_append_status"
|
||||||
|
post_state_action: "tools/build_immutable_decision_ledger_v1.py 호출 — decision_id 재기록 시도는 DUPLICATE_DECISION_ID로 거부."
|
||||||
INSUFFICIENT_DATA:
|
INSUFFICIENT_DATA:
|
||||||
purpose: "데이터 부족으로 산출 불가. 다음 확인 출처를 제시."
|
purpose: "데이터 부족으로 산출 불가. 다음 확인 출처를 제시."
|
||||||
output_required:
|
output_required:
|
||||||
- "missing_fields"
|
- "missing_fields"
|
||||||
- "prohibited_calculations"
|
- "prohibited_calculations"
|
||||||
- "next_source_to_check"
|
- "next_source_to_check"
|
||||||
|
- "ledger_append_status"
|
||||||
|
post_state_action: "INSUFFICIENT_DATA로 종료해도 IMMUTABLE_DECISION_LEDGER_V1에 기록한다(결정 없음도 기록 대상)."
|
||||||
BLOCKED:
|
BLOCKED:
|
||||||
purpose: "하드 필터 또는 리스크 정책으로 행동 차단."
|
purpose: "하드 필터 또는 리스크 정책으로 행동 차단."
|
||||||
output_required:
|
output_required:
|
||||||
- "triggered_rules"
|
- "triggered_rules"
|
||||||
- "blocked_action"
|
- "blocked_action"
|
||||||
- "allowed_alternative"
|
- "allowed_alternative"
|
||||||
|
- "ledger_append_status"
|
||||||
|
post_state_action: "BLOCKED로 종료해도 IMMUTABLE_DECISION_LEDGER_V1에 기록한다."
|
||||||
|
|
||||||
transitions:
|
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"
|
- from: "INPUT_VALIDATION"
|
||||||
to: "DATA_COMPLETENESS_CHECK"
|
to: "DATA_COMPLETENESS_CHECK"
|
||||||
condition: "minimum request context exists"
|
condition: "minimum request context exists"
|
||||||
@@ -214,11 +303,14 @@ transitions:
|
|||||||
to: "INSUFFICIENT_DATA"
|
to: "INSUFFICIENT_DATA"
|
||||||
condition: "account/request context missing and cannot be inferred"
|
condition: "account/request context missing and cannot be inferred"
|
||||||
- from: "DATA_COMPLETENESS_CHECK"
|
- from: "DATA_COMPLETENESS_CHECK"
|
||||||
to: "HARD_FILTER_CHECK"
|
to: "STATE_VECTOR_CONSTRUCTION"
|
||||||
condition: "data_completeness_matrix produced"
|
condition: "data_completeness_matrix produced"
|
||||||
- from: "DATA_COMPLETENESS_CHECK"
|
- from: "DATA_COMPLETENESS_CHECK"
|
||||||
to: "INSUFFICIENT_DATA"
|
to: "INSUFFICIENT_DATA"
|
||||||
condition: "matrix cannot be produced"
|
condition: "matrix cannot be produced"
|
||||||
|
- from: "STATE_VECTOR_CONSTRUCTION"
|
||||||
|
to: "HARD_FILTER_CHECK"
|
||||||
|
condition: "state_vector 산출(결측 component 포함 가능)"
|
||||||
- from: "HARD_FILTER_CHECK"
|
- from: "HARD_FILTER_CHECK"
|
||||||
to: "BLOCKED"
|
to: "BLOCKED"
|
||||||
condition: "blocking hard_filter failed"
|
condition: "blocking hard_filter failed"
|
||||||
@@ -235,17 +327,35 @@ transitions:
|
|||||||
to: "BLOCKED"
|
to: "BLOCKED"
|
||||||
condition: "cash_floor, duplicate exposure, account limit, or Total_Heat blocks requested action"
|
condition: "cash_floor, duplicate exposure, account limit, or Total_Heat blocks requested action"
|
||||||
- from: "PORTFOLIO_CONSTRAINT_CHECK"
|
- from: "PORTFOLIO_CONSTRAINT_CHECK"
|
||||||
to: "POSITION_SIZING"
|
to: "SECTOR_EXPOSURE_REVIEW"
|
||||||
condition: "requested action requires quantity"
|
condition: "requested action requires quantity or affects sector exposure"
|
||||||
- from: "PORTFOLIO_CONSTRAINT_CHECK"
|
- from: "PORTFOLIO_CONSTRAINT_CHECK"
|
||||||
to: "EXIT_POLICY_CHECK"
|
to: "EXIT_POLICY_CHECK"
|
||||||
condition: "requested action is hold/trim/sell review"
|
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"
|
- from: "POSITION_SIZING"
|
||||||
to: "EXIT_POLICY_CHECK"
|
to: "EXIT_POLICY_CHECK"
|
||||||
condition: "quantity calculated or NO_QUANTITY reason emitted"
|
condition: "quantity calculated or NO_QUANTITY reason emitted"
|
||||||
- from: "EXIT_POLICY_CHECK"
|
- from: "EXIT_POLICY_CHECK"
|
||||||
to: "OUTPUT_VALIDATION"
|
to: "PORTFOLIO_TRANSITION_REVIEW"
|
||||||
condition: "order/hold/watch decision prepared"
|
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"
|
- from: "OUTPUT_VALIDATION"
|
||||||
to: "FINAL_DECISION"
|
to: "FINAL_DECISION"
|
||||||
condition: "schema and required output fields valid"
|
condition: "schema and required output fields valid"
|
||||||
|
|||||||
@@ -2189,6 +2189,502 @@ field_dictionary:
|
|||||||
aliases: ["Base_Qty", "base_sell_qty"]
|
aliases: ["Base_Qty", "base_sell_qty"]
|
||||||
note: "SELL_QUANTITY_ALLOCATOR_V1 산출 기준 매도 수량. CASH_PRESERVATION_SELL_ENGINE_V2 입력."
|
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:
|
normalization_rules:
|
||||||
- id: "FIELD_ALIAS_CANONICALIZATION"
|
- id: "FIELD_ALIAS_CANONICALIZATION"
|
||||||
rule: "모든 입력은 계산 전 canonical_name으로 변환한다."
|
rule: "모든 입력은 계산 전 canonical_name으로 변환한다."
|
||||||
|
|||||||
@@ -97,6 +97,21 @@ formula_registry:
|
|||||||
- HORIZON_REBALANCE_PLAN_V1
|
- HORIZON_REBALANCE_PLAN_V1
|
||||||
- PIPELINE_RUNTIME_PROFILE_V1
|
- PIPELINE_RUNTIME_PROFILE_V1
|
||||||
- STRATEGY_ROUTING_AUDIT_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:
|
implementation_map:
|
||||||
REGIME_CONDITIONAL_MACRO_FACTOR_V1: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1
|
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
|
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
|
HORIZON_REBALANCE_PLAN_V1: tools/build_horizon_rebalance_plan_v1.py
|
||||||
PIPELINE_RUNTIME_PROFILE_V1: src/quant_engine/pipeline_runtime_anomaly_lib_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
|
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:
|
formulas:
|
||||||
FLOW_CREDIT_V1:
|
FLOW_CREDIT_V1:
|
||||||
purpose: 가격·거래량·5D 수급 품질을 0~1 점수로 계산
|
purpose: 가격·거래량·5D 수급 품질을 0~1 점수로 계산
|
||||||
@@ -2740,6 +2770,398 @@ formula_registry:
|
|||||||
- rebound_tp_price가 있으면 HTS 주문표에 '반등 익절가' 컬럼 필수 표기
|
- rebound_tp_price가 있으면 HTS 주문표에 '반등 익절가' 컬럼 필수 표기
|
||||||
canonical_ref: AGENTS.md:Direction C1, K2
|
canonical_ref: AGENTS.md:Direction C1, K2
|
||||||
version: 2026-05-22_3RD_HARNESS
|
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:
|
SELL_EXECUTION_TIMING_V1:
|
||||||
purpose: '장중 가격 움직임에 따라 매도 주문 유형과 타이밍을 결정론적으로 판정. 장초반 패닉 매도, 반등 직전 저점 투매 방지.
|
purpose: '장중 가격 움직임에 따라 매도 주문 유형과 타이밍을 결정론적으로 판정. 장초반 패닉 매도, 반등 직전 저점 투매 방지.
|
||||||
|
|
||||||
@@ -4530,6 +4952,44 @@ formula_registry:
|
|||||||
implementation: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1
|
implementation: tools/build_predictive_alpha_dialectic_engine_v2.py:NF1
|
||||||
calibration_ref: spec/calibration_registry.yaml:NF1 (EXPERT_PRIOR)
|
calibration_ref: spec/calibration_registry.yaml:NF1 (EXPERT_PRIOR)
|
||||||
version: 2026-06-04_NF1
|
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:
|
REBOUND_CAPTURE_THESIS_FACTOR_V1:
|
||||||
purpose: 과매도 반등 진입을 thesis 팩터로 명시 — 영구 약세편향 해소 (Direction SFP1)
|
purpose: 과매도 반등 진입을 thesis 팩터로 명시 — 영구 약세편향 해소 (Direction SFP1)
|
||||||
agents_md_ref: 'Direction SFP1: SINGLE_FACTOR_DOMINANCE_CAP_V1 — REBOUND_CAPTURE
|
agents_md_ref: 'Direction SFP1: SINGLE_FACTOR_DOMINANCE_CAP_V1 — REBOUND_CAPTURE
|
||||||
|
|||||||
@@ -6,6 +6,254 @@ note: >
|
|||||||
|
|
||||||
golden_cases:
|
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 케이스 ─────────────────────
|
# ── STOP_BREACH_V1: profit_pct < -20% 경계값 3 케이스 ─────────────────────
|
||||||
- formula_id: STOP_BREACH_V1
|
- formula_id: STOP_BREACH_V1
|
||||||
id: GV4_STOP_001
|
id: GV4_STOP_001
|
||||||
|
|||||||
@@ -537,6 +537,73 @@ formulas:
|
|||||||
activation_threshold:
|
activation_threshold:
|
||||||
min_t20_sample: 30
|
min_t20_sample: 30
|
||||||
retirement_condition: performance_degradation
|
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:
|
SMART_MONEY_LIQUIDITY_GATE_V1:
|
||||||
purpose: '스마트머니·유동성 차단 게이트. SM001(외국인+기관 동시 순매도→BLOCK_BUY), SM002(5일 평균 거래대금 <
|
purpose: '스마트머니·유동성 차단 게이트. SM001(외국인+기관 동시 순매도→BLOCK_BUY), SM002(5일 평균 거래대금 <
|
||||||
50억→LIMIT_QUANTITY), SM003(RSI14>70 AND flow_credit<0.3→BLOCK_BUY) 결정론 구현. FINAL_JUDGMENT_GATE_V1의
|
50억→LIMIT_QUANTITY), SM003(RSI14>70 AND flow_credit<0.3→BLOCK_BUY) 결정론 구현. FINAL_JUDGMENT_GATE_V1의
|
||||||
@@ -946,3 +1013,50 @@ formulas:
|
|||||||
activation_threshold:
|
activation_threshold:
|
||||||
min_t20_sample: 30
|
min_t20_sample: 30
|
||||||
retirement_condition: performance_degradation
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -668,3 +668,264 @@ formulas:
|
|||||||
activation_threshold:
|
activation_threshold:
|
||||||
min_t20_sample: 30
|
min_t20_sample: 30
|
||||||
retirement_condition: performance_degradation
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -423,6 +423,30 @@ portfolio_exposure_framework:
|
|||||||
- "cluster_state 필드 없이 반도체 종목 SELL 판단 금지"
|
- "cluster_state 필드 없이 반도체 종목 SELL 판단 금지"
|
||||||
- "CLUSTER_HOLD_ONLY 상태를 LLM이 임의로 CLUSTER_OPEN으로 변경 금지"
|
- "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)
|
cash_floor: # ── 구조 수정: duplicate_exposure_rule 자식(4칸) → portfolio_exposure_framework 직속(2칸) (2026-05-14)
|
||||||
normal: "총자산 7~10%"
|
normal: "총자산 7~10%"
|
||||||
overheated_or_event_week: "총자산 10~15%"
|
overheated_or_event_week: "총자산 10~15%"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
+48
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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]
|
||||||
@@ -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"] == []
|
||||||
@@ -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 == []
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"] == []
|
||||||
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user