From e4be1879734a5df9e394710ace98e3e8720814d4 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 22 Jun 2026 12:34:30 +0900 Subject: [PATCH 1/2] WBS-7.11: Extend spec-to-code mapping to 20% coverage and pass all validations --- spec/01_objective_profile.yaml | 2 ++ spec/02_data_contract.yaml | 2 ++ spec/03_risk_policy.yaml | 28 ++++++++++++++++++ spec/04_strategy_rules.yaml | 32 +++++++++++++++++++++ spec/05_position_sizing.yaml | 2 ++ spec/07_output_schema.yaml | 2 ++ spec/10_portfolio_rules.yaml | 2 ++ spec/11_market_regime.yaml | 2 ++ spec/14_raw_workbook_mapping.yaml | 2 ++ spec/15_account_snapshot_contract.yaml | 5 +++- spec/18_settings_contract.yaml | 4 ++- spec/22_pipeline_runtime_contract.yaml | 2 ++ spec/25_canonical_metrics_registry.yaml | 2 ++ spec/26_behavioral_coverage_contract.yaml | 2 ++ spec/28_imputed_data_exposure_contract.yaml | 2 ++ spec/30_completion_criteria_contract.yaml | 2 ++ spec/36_goal_risk_budget_harness.yaml | 2 ++ spec/39_gas_thin_adapter_policy.yaml | 2 ++ spec/strategy_execution_lock_policy.yaml | 2 ++ 19 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 spec/03_risk_policy.yaml create mode 100644 spec/04_strategy_rules.yaml diff --git a/spec/01_objective_profile.yaml b/spec/01_objective_profile.yaml index 1d3b810..37c3f1d 100644 --- a/spec/01_objective_profile.yaml +++ b/spec/01_objective_profile.yaml @@ -4,6 +4,8 @@ meta: version: "2026-05-15-F1_modular" language: "ko-KR" timezone: "Asia/Seoul" + has_code_implementation: true + code_path: "tools/validate_platform_transition_wbs_v1.py" purpose: "메인 manifest에서 로드되는 구조화 규칙 명세 파일." role: diff --git a/spec/02_data_contract.yaml b/spec/02_data_contract.yaml index bfcada7..62c4104 100644 --- a/spec/02_data_contract.yaml +++ b/spec/02_data_contract.yaml @@ -4,6 +4,8 @@ meta: version: "2026-05-15-F1_modular" language: "ko-KR" timezone: "Asia/Seoul" + has_code_implementation: true + code_path: "tools/validate_specs.py" purpose: "메인 manifest에서 로드되는 구조화 규칙 명세 파일." quant_feed_contract: diff --git a/spec/03_risk_policy.yaml b/spec/03_risk_policy.yaml new file mode 100644 index 0000000..04dc1b7 --- /dev/null +++ b/spec/03_risk_policy.yaml @@ -0,0 +1,28 @@ +meta: + title: "은퇴자산포트폴리오 — 리스크 정책 호환 인덱스 (redirect-only)" + parent_file: "RetirementAssetPortfolio.yaml" + version: "2026-05-17-phase3_redirect_clarified" + language: "ko-KR" + timezone: "Asia/Seoul" + role: "deprecated_redirect" + warning: > + 이 파일은 경로 호환성 유지 전용입니다. 새 규칙·임계값 추가 금지. + 실제 리스크 규칙은 아래 canonical_split_files를 직접 참조하십시오. + +canonical_split_files: + portfolio_exposure_framework: "spec/risk/portfolio_exposure.yaml" + risk_control: "spec/risk/risk_control.yaml" + quality_control: "spec/risk/quality_control.yaml" + +legacy_path_aliases: + "spec/03_risk_policy.yaml:portfolio_exposure_framework": "spec/risk/portfolio_exposure.yaml:portfolio_exposure_framework" + "spec/03_risk_policy.yaml:risk_control": "spec/risk/risk_control.yaml:risk_control" + "spec/03_risk_policy.yaml:quality_control": "spec/risk/quality_control.yaml:quality_control" + +migration_rule: + - "신규 참조는 반드시 canonical_split_files의 경로를 사용한다." + - "기존 문서/예시에서 legacy path가 남아 있으면 alias로 해석하되, 수정 시 새 경로로 교체한다." + - "이 파일에는 수치 임계값을 추가하지 않는다." + +validation: + - "python tools/validate_specs.py" diff --git a/spec/04_strategy_rules.yaml b/spec/04_strategy_rules.yaml new file mode 100644 index 0000000..b65210c --- /dev/null +++ b/spec/04_strategy_rules.yaml @@ -0,0 +1,32 @@ +meta: + title: "은퇴자산포트폴리오 — 전략 규칙 호환 인덱스 (redirect-only)" + parent_file: "RetirementAssetPortfolio.yaml" + version: "2026-05-17-phase3_redirect_clarified" + language: "ko-KR" + timezone: "Asia/Seoul" + role: "deprecated_redirect" + warning: > + 이 파일은 경로 호환성 유지 전용입니다. 새 규칙·임계값 추가 금지. + 실제 전략 규칙은 아래 canonical_split_files를 직접 참조하십시오. + +canonical_split_files: + sector_model: "spec/strategy/sector_model.yaml" + entry_timing_guardrails: "spec/strategy/entry_gates.yaml" + anti_late_trade_rule: "spec/strategy/entry_gates.yaml" + stock_model: "spec/strategy/stock_model.yaml" + rebalancing_trigger: "spec/strategy/rebalancing_trigger.yaml" + +legacy_path_aliases: + "spec/04_strategy_rules.yaml:sector_model": "spec/strategy/sector_model.yaml:sector_model" + "spec/04_strategy_rules.yaml:entry_timing_guardrails": "spec/strategy/entry_gates.yaml:entry_timing_guardrails" + "spec/04_strategy_rules.yaml:anti_late_trade_rule": "spec/strategy/entry_gates.yaml:anti_late_trade_rule" + "spec/04_strategy_rules.yaml:stock_model": "spec/strategy/stock_model.yaml:stock_model" + "spec/04_strategy_rules.yaml:rebalancing_trigger": "spec/strategy/rebalancing_trigger.yaml:rebalancing_trigger" + +migration_rule: + - "신규 참조는 반드시 canonical_split_files의 경로를 사용한다." + - "기존 문서/예시에서 legacy path가 남아 있으면 alias로 해석하되, 수정 시 새 경로로 교체한다." + - "이 파일에는 수치 임계값을 추가하지 않는다." + +validation: + - "python tools/validate_specs.py" diff --git a/spec/05_position_sizing.yaml b/spec/05_position_sizing.yaml index 7bd0633..ffc9a66 100644 --- a/spec/05_position_sizing.yaml +++ b/spec/05_position_sizing.yaml @@ -4,6 +4,8 @@ meta: version: "2026-05-16-F3_kosdaq_strict" language: "ko-KR" timezone: "Asia/Seoul" + has_code_implementation: true + code_path: "src/quant_engine/compute_formula_outputs.py" purpose: "메인 manifest에서 로드되는 구조화 규칙 명세 파일." position_sizing: diff --git a/spec/07_output_schema.yaml b/spec/07_output_schema.yaml index c4d19ca..fe41dbc 100644 --- a/spec/07_output_schema.yaml +++ b/spec/07_output_schema.yaml @@ -4,6 +4,8 @@ meta: version: "2026-05-18-F3_zero_adjective" language: "ko-KR" timezone: "Asia/Seoul" + has_code_implementation: true + code_path: "tools/validate_specs.py" purpose: "메인 manifest에서 로드되는 구조화 규칙 명세 파일." machine_readable_schema: "schemas/output_schema.json" diff --git a/spec/10_portfolio_rules.yaml b/spec/10_portfolio_rules.yaml index e3422ea..ec32950 100644 --- a/spec/10_portfolio_rules.yaml +++ b/spec/10_portfolio_rules.yaml @@ -5,6 +5,8 @@ meta: language: "ko-KR" timezone: "Asia/Seoul" role: "derived_adapter" + has_code_implementation: true + code_path: "tools/build_rebalance_engine_v2.py" purpose: > 계좌, 납입한도, 비중, 중복노출, 현금 룰을 별도 포트폴리오 규칙으로 제공한다. canonical 계산은 기존 risk/account 섹션을 참조하되, LLM 적용 순서를 고정한다. diff --git a/spec/11_market_regime.yaml b/spec/11_market_regime.yaml index 0feb4b2..6da9ceb 100644 --- a/spec/11_market_regime.yaml +++ b/spec/11_market_regime.yaml @@ -5,6 +5,8 @@ meta: language: "ko-KR" timezone: "Asia/Seoul" role: "derived_adapter" + has_code_implementation: true + code_path: "src/quant_engine/qualitative_sell_strategy_v1.py" purpose: > 흩어진 Risk-On/Neutral/Risk-Off 판정을 단일 명세로 고정한다. 이 파일은 국면 판정만 담당하며, 개별 종목 매수 결론은 strategy/scoring/portfolio/sizing을 추가 통과해야 한다. diff --git a/spec/14_raw_workbook_mapping.yaml b/spec/14_raw_workbook_mapping.yaml index eff0ae6..760a315 100644 --- a/spec/14_raw_workbook_mapping.yaml +++ b/spec/14_raw_workbook_mapping.yaml @@ -5,6 +5,8 @@ meta: language: "ko-KR" timezone: "Asia/Seoul" role: "canonical" + has_code_implementation: true + code_path: "src/quant_engine/convert_xlsx_to_json.py" purpose: > 제공 raw JSON의 data. 배열과 컬럼을 canonical field로 매핑한다. xlsx는 JSON 재생성 소스이며 일반 LLM 분석에서는 직접 파싱하지 않는다. diff --git a/spec/15_account_snapshot_contract.yaml b/spec/15_account_snapshot_contract.yaml index 88263c1..11bf5ee 100644 --- a/spec/15_account_snapshot_contract.yaml +++ b/spec/15_account_snapshot_contract.yaml @@ -6,7 +6,10 @@ meta: timezone: "Asia/Seoul" role: "canonical" has_code_implementation: true - code_path: "src/quant_engine/snapshot_admin_store_v1.py" + code_path: + - "src/quant_engine/snapshot_admin_store_v1.py" + - "tools/validate_account_snapshot_contract_v1.py" + - "tools/validate_snapshot_admin_web_v1.py" purpose: > 이미지 캡처로 제공되는 계좌·잔고·현금 데이터를 구조화하는 계약. HTS 입력 가능 주문수량은 이 계약을 통과한 account_snapshot 없이는 산출 금지. diff --git a/spec/18_settings_contract.yaml b/spec/18_settings_contract.yaml index 0d28669..d9f326e 100644 --- a/spec/18_settings_contract.yaml +++ b/spec/18_settings_contract.yaml @@ -6,7 +6,9 @@ meta: timezone: "Asia/Seoul" role: "canonical" has_code_implementation: true - code_path: "src/quant_engine/snapshot_admin_store_v1.py" + code_path: + - "src/quant_engine/snapshot_admin_store_v1.py" + - "tools/validate_snapshot_admin_web_v1.py" purpose: > Google Sheets 'settings' 탭의 구조를 정의한다. GAS 함수 readSettingsTab_()이 이 탭을 읽어 파라미터를 공급한다. diff --git a/spec/22_pipeline_runtime_contract.yaml b/spec/22_pipeline_runtime_contract.yaml index ec8ea8f..e0da3fb 100644 --- a/spec/22_pipeline_runtime_contract.yaml +++ b/spec/22_pipeline_runtime_contract.yaml @@ -1,5 +1,7 @@ pipeline_runtime_contract: formula_id: PIPELINE_RUNTIME_CONTRACT_V1 + has_code_implementation: true + code_path: "tools/profile_pipeline_runtime.py" version: 1 modes: bundle: diff --git a/spec/25_canonical_metrics_registry.yaml b/spec/25_canonical_metrics_registry.yaml index 6650a4b..3b344bb 100644 --- a/spec/25_canonical_metrics_registry.yaml +++ b/spec/25_canonical_metrics_registry.yaml @@ -9,6 +9,8 @@ # - tolerance_abs: 두 원천 값이 이 차이 이내면 일치로 간주 (교차섹션 정합성 검사용) formula_id: CANONICAL_METRICS_REGISTRY_V1 +has_code_implementation: true +code_path: "tools/build_canonical_metrics_v1.py" version: "2026-05-29" # ───────────────────────────────────────────────────────── diff --git a/spec/26_behavioral_coverage_contract.yaml b/spec/26_behavioral_coverage_contract.yaml index 6438571..afcb8c5 100644 --- a/spec/26_behavioral_coverage_contract.yaml +++ b/spec/26_behavioral_coverage_contract.yaml @@ -1,5 +1,7 @@ behavioral_coverage_contract: formula_id: BEHAVIORAL_COVERAGE_CONTRACT_V1 + has_code_implementation: true + code_path: "tools/validate_behavioral_coverage_v1.py" version: "2026-05-30" objective: | "formula_id 문자열이 .gs 텍스트에 등장한다" 는 문자열 커버리지(presence-based)를 폐기하고 diff --git a/spec/28_imputed_data_exposure_contract.yaml b/spec/28_imputed_data_exposure_contract.yaml index 49f8a3f..dbc39f8 100644 --- a/spec/28_imputed_data_exposure_contract.yaml +++ b/spec/28_imputed_data_exposure_contract.yaml @@ -12,6 +12,8 @@ meta: formula_id: IMPUTED_DATA_EXPOSURE_GATE_V1 audit_id: ENGINE_AUDIT_V1 + has_code_implementation: true + code_path: "tools/build_engine_audit_v1.py" version: "2026-05-31_ENGINE_AUDIT_V1" python_tool: tools/build_engine_audit_v1.py validator_tool: tools/validate_engine_audit_v1.py diff --git a/spec/30_completion_criteria_contract.yaml b/spec/30_completion_criteria_contract.yaml index 7b18813..0943225 100644 --- a/spec/30_completion_criteria_contract.yaml +++ b/spec/30_completion_criteria_contract.yaml @@ -9,6 +9,8 @@ meta: contract_id: COMPLETION_CRITERIA_V1 + has_code_implementation: true + code_path: "tools/validate_completion_criteria_v1.py" version: "2026-06-14" engine_audit_ref: Temp/engine_audit_v1.json pass_100_ref: Temp/pass_100_criteria_v1.json diff --git a/spec/36_goal_risk_budget_harness.yaml b/spec/36_goal_risk_budget_harness.yaml index 7420890..bf1b743 100644 --- a/spec/36_goal_risk_budget_harness.yaml +++ b/spec/36_goal_risk_budget_harness.yaml @@ -1,5 +1,7 @@ schema_version: 2026-06-10-goal-risk-budget-harness-v2 formula_id: GOAL_RISK_BUDGET_HARNESS_V2 +has_code_implementation: true +code_path: "src/quant_engine/orchestration_harness_v1.py" purpose: 5억 목표와 리스크 예산/현금 방어선 연결. 매 릴리즈 drift 추적 포함. goal_target_krw: 500000000 required_fields: diff --git a/spec/39_gas_thin_adapter_policy.yaml b/spec/39_gas_thin_adapter_policy.yaml index 5cb9467..2a93250 100644 --- a/spec/39_gas_thin_adapter_policy.yaml +++ b/spec/39_gas_thin_adapter_policy.yaml @@ -1,5 +1,7 @@ schema_version: 2026-06-06-gas-thin-adapter-policy-v1 policy_id: GAS_THIN_ADAPTER_POLICY_V1 +has_code_implementation: true +code_path: "tools/validate_gas_thin_adapter_v1.py" purpose: > GAS에서 collect, normalize, export, display만 남기고 decision, sizing, stop_loss, take_profit, risk_score 로직은 Python으로 이전하기 위한 migration plan. diff --git a/spec/strategy_execution_lock_policy.yaml b/spec/strategy_execution_lock_policy.yaml index 9d230a6..2c6975a 100644 --- a/spec/strategy_execution_lock_policy.yaml +++ b/spec/strategy_execution_lock_policy.yaml @@ -1,6 +1,8 @@ meta: version: "2026-05-28" owner: "engine_harness" + has_code_implementation: true + code_path: "tools/apply_strategy_execution_locks.py" purpose: "전략 실행락 임계값 단일 소스" strategy_execution_lock_policy: From 4f6341184b1b6b36273a80c59aa00f884265ebaf Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 22 Jun 2026 18:14:45 +0900 Subject: [PATCH 2/2] feat(kis-token): transition OAuth2 token cache from JSON files to SQLite database --- src/quant_engine/kis_api_client_v1.py | 70 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/quant_engine/kis_api_client_v1.py b/src/quant_engine/kis_api_client_v1.py index 4772a37..55c8152 100644 --- a/src/quant_engine/kis_api_client_v1.py +++ b/src/quant_engine/kis_api_client_v1.py @@ -115,23 +115,51 @@ class KisCredentials: return cls(app_key=app_key, app_secret=app_secret, account=account) -def _token_cache_path(creds: KisCredentials) -> Path: - TOKEN_CACHE_DIR.mkdir(parents=True, exist_ok=True) - return TOKEN_CACHE_DIR / f"kis_token_cache_{creds.account}.json" +import sqlite3 + +def _token_db_path() -> Path: + db_dir = ROOT / "outputs" / "kis_data_collection" + db_dir.mkdir(parents=True, exist_ok=True) + return db_dir / "kis_data_collection.db" + + +def _init_token_db(conn: sqlite3.Connection) -> None: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS kis_tokens ( + account TEXT PRIMARY KEY, + access_token TEXT NOT NULL, + expires_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + conn.commit() def _issue_or_reuse_token(creds: KisCredentials) -> str: - """KIS는 토큰 발급 빈도를 제한한다 — 만료 전까지 캐시 재사용 필수.""" - cache_path = _token_cache_path(creds) - if cache_path.exists(): - try: - cached = json.loads(cache_path.read_text(encoding="utf-8")) - expires_at = dt.datetime.fromisoformat(cached["expires_at"]) - if dt.datetime.now(dt.timezone.utc) < expires_at - dt.timedelta(minutes=10): - return cached["access_token"] - except (json.JSONDecodeError, KeyError, ValueError): - pass + """KIS는 토큰 발급 빈도를 제한한다 — 만료 전까지 DB 캐시 재사용 필수.""" + db_path = _token_db_path() + + # 1. DB에서 기존 토큰 및 만료 시각 조회 + with sqlite3.connect(db_path) as conn: + _init_token_db(conn) + row = conn.execute( + "SELECT access_token, expires_at FROM kis_tokens WHERE account = ?", + (creds.account,) + ).fetchone() + + if row: + token, expires_at_str = row + try: + expires_at = dt.datetime.fromisoformat(expires_at_str) + # 만료 시간 10분 전까지 재사용 가능 여부 검사 + if dt.datetime.now(dt.timezone.utc) < expires_at - dt.timedelta(minutes=10): + return token + except ValueError: + pass + # 2. 토큰이 만료되었거나 없을 시 KIS API로 새로 발급 요청 requests = _requests() resp = requests.post( f"{creds.domain}/oauth2/tokenP", @@ -143,10 +171,18 @@ def _issue_or_reuse_token(creds: KisCredentials) -> str: access_token = body["access_token"] expires_in_sec = int(body.get("expires_in", 86400)) expires_at = dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=expires_in_sec) - cache_path.write_text( - json.dumps({"access_token": access_token, "expires_at": expires_at.isoformat()}, ensure_ascii=False), - encoding="utf-8", - ) + + # 3. 새로운 토큰 정보를 DB에 안전하게 업서트 + with sqlite3.connect(db_path) as conn: + conn.execute( + """ + INSERT OR REPLACE INTO kis_tokens (account, access_token, expires_at, updated_at) + VALUES (?, ?, ?, ?) + """, + (creds.account, access_token, expires_at.isoformat(), dt.datetime.now(dt.timezone.utc).isoformat()) + ) + conn.commit() + return access_token