비기계적 매도전략(가치보존) + 위성종목 추천 엔진 추가
매크로·실적·펀더멘털·공매도수급·호가미시구조·대내외 변수 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 일일 스케줄(장마감 후) + 파이프라인 계약 검증 게이트
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user