refactor(web-ai): rename signal_v2→ai_trade, deprecate signal_v1
박재오 결정 2026-05-19 — V2를 정식 명칭 ai_trade로 graduation, V1은 deprecated 마킹 (legacy 디렉토리 이동은 file lock 풀린 후 후속). 변경 사항: - signal_v2/ → ai_trade/ (git mv, import 일괄 sed: signal_v2.x → ai_trade.x) - root start.bat → legacy/start_v1.bat (V1 자동 시작 차단) - ai_trade/start.bat 내부 uvicorn target signal_v2.main → ai_trade.main - signal_v1/DEPRECATED.md 추가 (사용 금지 명시) - CLAUDE.md 디렉토리 표·서버 시작 방식 갱신 - services/ 디렉토리 미래 예정 (Plan-B-Insta 작업 시 신설) ai_trade tests 59/59 PASS 확인. signal_v1/ 디렉토리 자체 이동(legacy/signal_v1/)은 telegram_bot.log + data/news_snapshots.db file lock으로 보류. lock 해제 후 후속 커밋. 후속 작업: Plan-B-Insta (services/insta-render + NAS insta 분할) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
ai_trade/rate_limit.py
Normal file
73
ai_trade/rate_limit.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""SignalDedup — SQLite-backed 24h duplicate signal blocker."""
|
||||
from __future__ import annotations
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
KST = ZoneInfo("Asia/Seoul")
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
"""Test seam — overridable via monkeypatch."""
|
||||
return datetime.now(KST).isoformat()
|
||||
|
||||
|
||||
_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS signal_dedup (
|
||||
ticker TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
last_sent TEXT NOT NULL,
|
||||
confidence REAL NOT NULL,
|
||||
PRIMARY KEY (ticker, action)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_signal_dedup_last_sent
|
||||
ON signal_dedup(last_sent);
|
||||
"""
|
||||
|
||||
|
||||
class SignalDedup:
|
||||
"""24h dedup interface. WAL + busy_timeout=120000."""
|
||||
|
||||
def __init__(self, db_path: Path):
|
||||
self._db_path = Path(db_path)
|
||||
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_schema()
|
||||
|
||||
@contextmanager
|
||||
def _conn(self):
|
||||
conn = sqlite3.connect(self._db_path, timeout=120.0)
|
||||
try:
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA busy_timeout=120000")
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _init_schema(self) -> None:
|
||||
with self._conn() as conn:
|
||||
conn.executescript(_SCHEMA)
|
||||
conn.commit()
|
||||
|
||||
def is_recent(self, ticker: str, action: str, within_hours: int = 24) -> bool:
|
||||
threshold_dt = datetime.fromisoformat(_now_iso()) - timedelta(hours=within_hours)
|
||||
threshold_iso = threshold_dt.isoformat()
|
||||
with self._conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT last_sent FROM signal_dedup WHERE ticker = ? AND action = ?",
|
||||
(ticker, action),
|
||||
).fetchone()
|
||||
return row is not None and row[0] >= threshold_iso
|
||||
|
||||
def record(self, ticker: str, action: str, confidence: float) -> None:
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO signal_dedup (ticker, action, last_sent, confidence)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (ticker, action) DO UPDATE
|
||||
SET last_sent = excluded.last_sent,
|
||||
confidence = excluded.confidence""",
|
||||
(ticker, action, _now_iso(), confidence),
|
||||
)
|
||||
conn.commit()
|
||||
Reference in New Issue
Block a user