fix: persist WF shadow state to DB and tighten ATR max stop

- core/price_db.py: add wf_state table CRUD (ensure/upsert/load/delete)
  to persist shadow_cons_wins across restarts
- core/trader.py: save WF blocked state on shadow enter/close,
  restore shadow_cons_wins on startup from DB
- core/monitor.py: lower ATR_MAX_STOP 4.0% → 2.0% based on sweep results
- atr_sweep.py: new ATR_MAX_STOP sweep tool using real ATR calc from DB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-02 13:49:32 +09:00
parent 324d69dde0
commit 4b6cb8ca0e
4 changed files with 310 additions and 2 deletions

View File

@@ -282,6 +282,71 @@ def delete_sell_price(ticker: str) -> None:
)
# ── WF 상태 영구 저장 (재시작 후 shadow 재활 상태 유지) ──────────────────────
def ensure_wf_state_table() -> None:
"""wf_state 테이블이 없으면 생성."""
ddl = """
CREATE TABLE wf_state (
ticker VARCHAR2(20) NOT NULL PRIMARY KEY,
is_blocked NUMBER(1) DEFAULT 0 NOT NULL,
shadow_cons_wins NUMBER DEFAULT 0 NOT NULL,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
)
"""
with _conn() as conn:
try:
conn.cursor().execute(ddl)
except oracledb.DatabaseError as e:
if e.args[0].code != 955: # ORA-00955: 이미 존재
raise
def upsert_wf_state(ticker: str, is_blocked: bool, shadow_cons_wins: int) -> None:
"""WF 차단 상태 저장 또는 갱신."""
sql = """
MERGE INTO wf_state w
USING (SELECT :ticker AS ticker FROM dual) d
ON (w.ticker = d.ticker)
WHEN MATCHED THEN
UPDATE SET is_blocked = :is_blocked,
shadow_cons_wins = :shadow_cons_wins,
updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (ticker, is_blocked, shadow_cons_wins)
VALUES (:ticker, :is_blocked, :shadow_cons_wins)
"""
with _conn() as conn:
conn.cursor().execute(sql, {
"ticker": ticker,
"is_blocked": 1 if is_blocked else 0,
"shadow_cons_wins": shadow_cons_wins,
})
def load_wf_states() -> dict[str, dict]:
"""저장된 WF 상태 전체 로드.
Returns:
{ticker: {"is_blocked": bool, "shadow_cons_wins": int}}
"""
with _conn() as conn:
cur = conn.cursor()
cur.execute("SELECT ticker, is_blocked, shadow_cons_wins FROM wf_state")
return {
r[0]: {"is_blocked": bool(r[1]), "shadow_cons_wins": int(r[2])}
for r in cur.fetchall()
}
def delete_wf_state(ticker: str) -> None:
"""WF 상태 삭제 (WF 해제 시)."""
with _conn() as conn:
conn.cursor().execute(
"DELETE FROM wf_state WHERE ticker = :ticker", {"ticker": ticker}
)
def load_positions() -> list[dict]:
"""저장된 전체 포지션 로드."""
sql = """