aedabdd37b
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>
210 lines
7.7 KiB
Python
210 lines
7.7 KiB
Python
#!/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())
|