feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경: - tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규 * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합 * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일) - src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규 * Logger.log / getSpreadsheet_() 로 run_all 연동 수정 - src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs * _mergePositionRecord_(): 소수주 중복 행 합산 신규 * parseInt → parseFloat (qty, availQty) - src/gas_adapter_parts/gdf_01_price_metrics.gs * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL - spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63) - spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""generate_models_from_schema.py — schema model generation
|
||||
|
||||
Mirrors schemas/generated/*.schema.json into src/quant_engine/models/generated
|
||||
as lightweight Python model descriptors.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCHEMA_DIR = ROOT / "schemas" / "generated"
|
||||
OUT_DIR = ROOT / "src" / "quant_engine" / "models" / "generated"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
obj = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
return obj if isinstance(obj, dict) else {}
|
||||
|
||||
|
||||
def to_module_name(path: Path) -> str:
|
||||
return path.stem.replace(".", "_").lower()
|
||||
|
||||
|
||||
def render_module(schema_path: Path, schema: dict[str, Any]) -> str:
|
||||
title = str(schema.get("title") or schema_path.stem)
|
||||
schema_id = str(schema.get("$id") or f"schema://{title}")
|
||||
props = schema.get("properties") if isinstance(schema.get("properties"), dict) else {}
|
||||
required = schema.get("required") if isinstance(schema.get("required"), list) else []
|
||||
prop_names = list(props.keys())
|
||||
return (
|
||||
'"""Auto-generated schema model descriptor."""\n'
|
||||
"from __future__ import annotations\n\n"
|
||||
"from dataclasses import dataclass\n"
|
||||
"import json\n"
|
||||
"from pathlib import Path\n"
|
||||
"from typing import Any\n\n"
|
||||
f"SCHEMA_TITLE = {title!r}\n"
|
||||
f"SCHEMA_ID = {schema_id!r}\n"
|
||||
f"SCHEMA_PATH = {str(schema_path.relative_to(ROOT)).replace('\\', '/')!r}\n"
|
||||
f"SCHEMA_PROPERTIES = {prop_names!r}\n"
|
||||
f"SCHEMA_REQUIRED = {required!r}\n\n"
|
||||
"@dataclass(frozen=True)\n"
|
||||
"class SchemaModel:\n"
|
||||
" title: str\n"
|
||||
" schema_id: str\n"
|
||||
" path: str\n"
|
||||
" properties: list[str]\n"
|
||||
" required: list[str]\n\n"
|
||||
"def load_schema() -> dict[str, Any]:\n"
|
||||
" return json.loads(Path(__file__).with_suffix('.schema.json').read_text(encoding='utf-8'))\n\n"
|
||||
"def describe() -> SchemaModel:\n"
|
||||
" return SchemaModel(\n"
|
||||
" title=SCHEMA_TITLE,\n"
|
||||
" schema_id=SCHEMA_ID,\n"
|
||||
" path=SCHEMA_PATH,\n"
|
||||
" properties=list(SCHEMA_PROPERTIES),\n"
|
||||
" required=list(SCHEMA_REQUIRED),\n"
|
||||
" )\n"
|
||||
)
|
||||
|
||||
|
||||
def write_text(path: Path, text: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--schemas", default=str(SCHEMA_DIR))
|
||||
parser.add_argument("--out", default=str(OUT_DIR))
|
||||
parser.add_argument("--report", default=str(ROOT / "Temp" / "schema_model_generation_v1.json"))
|
||||
args = parser.parse_args()
|
||||
|
||||
schema_dir = Path(args.schemas)
|
||||
out_dir = Path(args.out)
|
||||
|
||||
schema_files = sorted(schema_dir.glob("*.schema.json"))
|
||||
modules = []
|
||||
for schema_file in schema_files:
|
||||
schema = load_json(schema_file)
|
||||
module_name = to_module_name(schema_file)
|
||||
modules.append(module_name)
|
||||
write_text(out_dir / f"{module_name}.py", render_module(schema_file, schema))
|
||||
write_text(out_dir / f"{module_name}.schema.json", json.dumps(schema, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
write_text(out_dir / "__init__.py", '"""Auto-generated schema model package."""\n')
|
||||
write_text(
|
||||
out_dir.parent / "__init__.py",
|
||||
'"""Auto-generated quant_engine.models package."""\n',
|
||||
)
|
||||
write_text(
|
||||
out_dir.parent.parent / "__init__.py",
|
||||
'"""Canonical quant_engine package."""\n',
|
||||
)
|
||||
write_text(
|
||||
out_dir.parent.parent.parent / "__init__.py",
|
||||
'"""Canonical src package."""\n',
|
||||
)
|
||||
|
||||
report = {
|
||||
"status": "OK",
|
||||
"schema_count": len(schema_files),
|
||||
"generated_module_count": len(modules),
|
||||
"package_root": "src/quant_engine/models/generated",
|
||||
"report_path": str(Path(args.report).relative_to(ROOT)),
|
||||
}
|
||||
write_text(Path(args.report), json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user