Files
QuantEngineByItz/src/quant_engine/qualitative_sell_strategy_store_v1.py
T
kjh2064 da0e1b0f7e 비기계적 매도전략(가치보존) + 위성종목 추천 엔진 추가
매크로·실적·펀더멘털·공매도수급·호가미시구조·대내외 변수 5개 독립
팩터군의 confluence(최소 3/5 합의) 없이는 매도 트리거를 금지하는
정성적 매도판단 엔진과, 보유종목 제외 위성후보 추천 로직을 추가한다.

- 단일 팩터 임계값 돌파만으로는 매도 신호를 생성하지 않음
  (mechanical_sell_prohibited=true)
- 데이터 결측 시 항상 DATA_MISSING/INSUFFICIENT_DATA_NO_ACTION —
  추정값으로 채우지 않음
- KIS 호가10단계·공매도거래비중 + Naver 시세/수급 스크래핑 입력 연동
- SQLite 시계열 저장 + 사후 적중률 자체평가
  (evaluate_qualitative_sell_strategy_accuracy_v1)
- Gitea 일일 스케줄(장마감 후) + 파이프라인 계약 검증 게이트
2026-06-21 20:05:55 +09:00

147 lines
5.1 KiB
Python

"""qualitative_sell_strategy_v1 산출물의 SQLite 시계열 저장소.
GAS/xlsx 구조와 완전히 분리된 추가(additive) 저장소다 — 이 모듈이 다루는 데이터는
순수 Python 산출물(KIS API 수집 + confluence 판단 결과)이며, GAS가 쓰지도 읽지도
않고 사람이 시트에서 직접 편집하지도 않는다. 기존 outputs/qualitative_sell_strategy/
*.json 파일 출력을 대체하지 않고 병행 저장한다(JSON은 1회성 점검용, SQLite는 시계열
추이 조회용). 표준 라이브러리 sqlite3만 사용 — 추가 의존성 없음.
"""
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from dataclasses import dataclass
from typing import Any
from src.quant_engine.storage_backend_v1 import StoreSpec, default_sqlite_store_path, normalize_store_spec
SCHEMA = """
CREATE TABLE IF NOT EXISTS sell_strategy_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
generated_at TEXT NOT NULL,
action TEXT,
conviction TEXT,
market_regime TEXT,
composite_score REAL,
rationale TEXT,
raw_json TEXT NOT NULL,
inserted_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sell_strategy_code_time
ON sell_strategy_results(code, generated_at);
CREATE TABLE IF NOT EXISTS satellite_recommendations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
generated_at TEXT NOT NULL,
satellite_action TEXT,
attractiveness_score REAL,
market_regime TEXT,
raw_json TEXT NOT NULL,
inserted_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_satellite_ticker_time
ON satellite_recommendations(ticker, generated_at);
"""
@dataclass(frozen=True)
class QualitativeSellStoreSpec(StoreSpec):
pass
def default_qualitative_sell_store_path(root: Path) -> Path:
return default_sqlite_store_path(root, "qualitative_sell_strategy/qualitative_sell_strategy.db")
def resolve_store_path(spec: QualitativeSellStoreSpec, root: Path) -> Path:
backend, location = normalize_store_spec(
spec,
root,
default_sqlite_name="qualitative_sell_strategy/qualitative_sell_strategy.db",
)
if backend != "sqlite":
raise ValueError(
"qualitative_sell_strategy_store_v1 currently executes on sqlite only; "
"the caller contract already allows future PostgreSQL swap-in."
)
return Path(location)
def init_db(db_path: Path) -> None:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path)
try:
conn.executescript(SCHEMA)
conn.commit()
finally:
conn.close()
def insert_sell_strategy_result(db_path: Path, result: dict[str, Any]) -> None:
"""build_qualitative_sell_inputs_v1.process_one()의 반환값(dict)을 그대로 받는다."""
init_db(db_path)
decision = result.get("decision") or {}
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO sell_strategy_results "
"(code, generated_at, action, conviction, market_regime, composite_score, rationale, raw_json) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
result.get("code"),
result.get("generated_at"),
decision.get("action"),
decision.get("conviction"),
decision.get("market_regime"),
decision.get("composite_score"),
decision.get("rationale"),
json.dumps(result, ensure_ascii=False, default=str),
),
)
conn.commit()
finally:
conn.close()
def insert_satellite_recommendation(db_path: Path, generated_at: str, candidate: dict[str, Any]) -> None:
"""build_satellite_candidate_recommendations_v1.py results[i] 항목 하나를 받는다."""
init_db(db_path)
score = candidate.get("score") or {}
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO satellite_recommendations "
"(ticker, generated_at, satellite_action, attractiveness_score, market_regime, raw_json) "
"VALUES (?, ?, ?, ?, ?, ?)",
(
candidate.get("ticker"),
generated_at,
score.get("satellite_action"),
score.get("attractiveness_score"),
score.get("market_regime"),
json.dumps(candidate, ensure_ascii=False, default=str),
),
)
conn.commit()
finally:
conn.close()
def fetch_recent_sell_strategy_results(db_path: Path, code: str, limit: int = 20) -> list[dict[str, Any]]:
if not db_path.exists():
return []
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"SELECT code, generated_at, action, conviction, market_regime, composite_score, rationale "
"FROM sell_strategy_results WHERE code = ? ORDER BY generated_at DESC LIMIT ?",
(code, limit),
).fetchall()
return [dict(row) for row in rows]
finally:
conn.close()