refactor: rename stock-lab → stock (graduation)
- git mv stock-lab/ → stock/ - docker-compose.yml: 서비스 키 + container_name + build.context + frontend.depends_on + agent-office STOCK_LAB_URL → STOCK_URL - agent-office/app: config.py, service_proxy.py, agents/stock.py, tests/ STOCK_LAB_URL → STOCK_URL - nginx/default.conf: proxy_pass http://stock-lab → http://stock (3 lines) - CLAUDE.md / README.md / STATUS.md / scripts/ 문구 갱신 - stock/ 내부 자기 참조 갱신 lab 네이밍 정책 (feedback_lab_naming.md) graduation. API URL / Python import / DB 파일명 변경 없음.
This commit is contained in:
204
stock/app/screener/schema.py
Normal file
204
stock/app/screener/schema.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Screener schema bootstrap. Called once at module import via db.py."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
|
||||
DEFAULT_WEIGHTS = {
|
||||
"foreign_buy": 1.0,
|
||||
"volume_surge": 1.0,
|
||||
"momentum": 1.0,
|
||||
"high52w": 1.2,
|
||||
"rs_rating": 1.2,
|
||||
"ma_alignment": 1.0,
|
||||
"vcp_lite": 0.8,
|
||||
# ai_news: 검증 전 gradient 차단 (4주 IC > 0.05 확인 후 활성화).
|
||||
# 데이터 수집은 계속, 가중합 영향만 0.
|
||||
"ai_news": 0.0,
|
||||
}
|
||||
DEFAULT_NODE_PARAMS = {
|
||||
"foreign_buy": {"window_days": 5},
|
||||
"volume_surge": {"baseline_days": 20, "eval_days": 3},
|
||||
"momentum": {"window_days": 20},
|
||||
"high52w": {"window_days": 252},
|
||||
"rs_rating": {"weights": {"3m": 2, "6m": 1, "9m": 1, "12m": 1}},
|
||||
"ma_alignment": {"ma_periods": [50, 150, 200]},
|
||||
"vcp_lite": {"short_window": 40, "long_window": 252},
|
||||
"ai_news": {"min_news_count": 1},
|
||||
}
|
||||
DEFAULT_GATE_PARAMS = {
|
||||
"min_market_cap_won": 50_000_000_000,
|
||||
"min_avg_value_won": 500_000_000,
|
||||
"min_listed_days": 60,
|
||||
"skip_managed": True,
|
||||
"skip_preferred": True,
|
||||
"skip_spac": True,
|
||||
"skip_halted_days": 3,
|
||||
}
|
||||
|
||||
DDL = """
|
||||
CREATE TABLE IF NOT EXISTS krx_master (
|
||||
ticker TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
market_cap INTEGER,
|
||||
is_managed INTEGER NOT NULL DEFAULT 0,
|
||||
is_preferred INTEGER NOT NULL DEFAULT 0,
|
||||
is_spac INTEGER NOT NULL DEFAULT 0,
|
||||
listed_date TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS krx_daily_prices (
|
||||
ticker TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
open INTEGER, high INTEGER, low INTEGER, close INTEGER,
|
||||
volume INTEGER,
|
||||
value INTEGER,
|
||||
PRIMARY KEY (ticker, date)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_prices_date ON krx_daily_prices(date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS krx_flow (
|
||||
ticker TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
foreign_net INTEGER,
|
||||
institution_net INTEGER,
|
||||
PRIMARY KEY (ticker, date)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_flow_date ON krx_flow(date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS screener_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
weights_json TEXT NOT NULL,
|
||||
node_params_json TEXT NOT NULL,
|
||||
gate_params_json TEXT NOT NULL,
|
||||
top_n INTEGER NOT NULL DEFAULT 20,
|
||||
rr_ratio REAL NOT NULL DEFAULT 2.0,
|
||||
atr_window INTEGER NOT NULL DEFAULT 14,
|
||||
atr_stop_mult REAL NOT NULL DEFAULT 2.0,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS screener_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
asof TEXT NOT NULL,
|
||||
mode TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
error TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT,
|
||||
weights_json TEXT NOT NULL,
|
||||
node_params_json TEXT NOT NULL,
|
||||
gate_params_json TEXT NOT NULL,
|
||||
top_n INTEGER NOT NULL,
|
||||
survivors_count INTEGER,
|
||||
telegram_sent INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_runs_asof ON screener_runs(asof DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS screener_results (
|
||||
run_id INTEGER NOT NULL,
|
||||
rank INTEGER NOT NULL,
|
||||
ticker TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
total_score REAL NOT NULL,
|
||||
scores_json TEXT NOT NULL,
|
||||
close INTEGER,
|
||||
market_cap INTEGER,
|
||||
entry_price INTEGER,
|
||||
stop_price INTEGER,
|
||||
target_price INTEGER,
|
||||
atr14 REAL,
|
||||
PRIMARY KEY (run_id, ticker),
|
||||
FOREIGN KEY (run_id) REFERENCES screener_runs(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_results_run_rank ON screener_results(run_id, rank);
|
||||
|
||||
-- articles 테이블 (도메스틱/해외 뉴스 원본).
|
||||
-- 메인 app.db.init_db() 에서도 생성하지만, 테스트 환경 및 단독 screener 컨텍스트
|
||||
-- (ai_news.articles_source 등)에서도 참조 가능하도록 idempotent 하게 보장한다.
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hash TEXT UNIQUE NOT NULL,
|
||||
category TEXT DEFAULT 'domestic',
|
||||
title TEXT NOT NULL,
|
||||
link TEXT,
|
||||
summary TEXT,
|
||||
press TEXT,
|
||||
pub_date TEXT,
|
||||
crawled_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_crawled ON articles(crawled_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS news_sentiment (
|
||||
ticker TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
score_raw REAL NOT NULL,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
news_count INTEGER NOT NULL DEFAULT 0,
|
||||
tokens_input INTEGER NOT NULL DEFAULT 0,
|
||||
tokens_output INTEGER NOT NULL DEFAULT 0,
|
||||
model TEXT NOT NULL DEFAULT 'claude-haiku-4-5-20251001',
|
||||
source TEXT NOT NULL DEFAULT 'articles',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
|
||||
PRIMARY KEY (ticker, date)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_news_sentiment_date ON news_sentiment(date DESC);
|
||||
"""
|
||||
|
||||
|
||||
def ensure_screener_schema(conn: sqlite3.Connection) -> None:
|
||||
"""Create tables and seed default settings (idempotent)."""
|
||||
conn.executescript(DDL)
|
||||
# ai_news 키 누락 시 1회 보충 (이미 운영 중인 환경에 대해)
|
||||
row = conn.execute(
|
||||
"SELECT weights_json, node_params_json FROM screener_settings WHERE id=1"
|
||||
).fetchone()
|
||||
if row is not None:
|
||||
w = json.loads(row[0])
|
||||
p = json.loads(row[1])
|
||||
changed = False
|
||||
if "ai_news" not in w:
|
||||
w["ai_news"] = DEFAULT_WEIGHTS["ai_news"]
|
||||
changed = True
|
||||
# One-time reset: ai_news default 0.8 → 0.0 (검증 전 gradient 차단).
|
||||
# 사용자가 명시적으로 0.8 외 값을 설정했다면 영향 없음.
|
||||
elif w.get("ai_news") == 0.8:
|
||||
w["ai_news"] = 0.0
|
||||
changed = True
|
||||
if "ai_news" not in p:
|
||||
p["ai_news"] = DEFAULT_NODE_PARAMS["ai_news"]
|
||||
changed = True
|
||||
if changed:
|
||||
conn.execute(
|
||||
"UPDATE screener_settings SET weights_json=?, node_params_json=? WHERE id=1",
|
||||
(json.dumps(w), json.dumps(p)),
|
||||
)
|
||||
# news_sentiment.source 컬럼 1회 추가 (기존 운영 환경)
|
||||
cols = {r[1] for r in conn.execute(
|
||||
"PRAGMA table_info(news_sentiment)"
|
||||
).fetchall()}
|
||||
if "source" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE news_sentiment "
|
||||
"ADD COLUMN source TEXT NOT NULL DEFAULT 'articles'"
|
||||
)
|
||||
existing = conn.execute("SELECT id FROM screener_settings WHERE id=1").fetchone()
|
||||
if existing is None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO screener_settings (
|
||||
id, weights_json, node_params_json, gate_params_json,
|
||||
top_n, rr_ratio, atr_window, atr_stop_mult, updated_at
|
||||
) VALUES (1, ?, ?, ?, 20, 2.0, 14, 2.0, ?)
|
||||
""",
|
||||
(
|
||||
json.dumps(DEFAULT_WEIGHTS),
|
||||
json.dumps(DEFAULT_NODE_PARAMS),
|
||||
json.dumps(DEFAULT_GATE_PARAMS),
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
Reference in New Issue
Block a user