feat: Add DART strategy and related configurations
ㅇ Changes: - Introduced the DART strategy to the trading system, including its configuration and integration into the existing framework. - Updated the database schema to include DART-specific tables for disclosures and watchlists. - Enhanced the backtesting and parameter search functionalities to support the DART strategy. - Implemented new rules for browser verification and API interactions to ensure compliance with the updated DART strategy. Impact: - These additions expand the trading capabilities of the system, allowing for more comprehensive analysis and execution of DART-related strategies, while maintaining system integrity and performance.
This commit is contained in:
248
kis_trader/strategies/dart_strategy.py
Normal file
248
kis_trader/strategies/dart_strategy.py
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
kis_trader/strategies/dart_strategy.py — DART 수주 공시 실매 전략
|
||||
================================================================
|
||||
유니버스 = dart_watchlist (구독 스위치) 또는 당일 disclosures.
|
||||
매매 = STRATEGY_DART_ENABLED + DART_TRADE_ENABLED.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime as dt
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ..engine import dart_engine as de
|
||||
from ..scan.dart_watchlist import event_time_for_code, list_active_watch
|
||||
from ..utils.env import get_env_bool, get_env_int
|
||||
from .base import BaseStrategy
|
||||
|
||||
|
||||
class DartStrategy(BaseStrategy):
|
||||
strategy_id = "DART"
|
||||
loop_min_sleep = 1.0
|
||||
loop_max_sleep = 2.5
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.candle_tf = 1
|
||||
self._engine_params: Optional[Dict] = None
|
||||
self.reload_config()
|
||||
|
||||
def reload_config(self) -> None:
|
||||
try:
|
||||
base = de.get_dart_defaults_from_db()
|
||||
self._engine_params = base
|
||||
self.min_price = float(base.get("min_price", 1000.0))
|
||||
self.stop_loss_pct = -abs(float(base.get("sl_pct", 0.02)))
|
||||
self.take_profit_pct = abs(float(base.get("tp_pct", 0.04)))
|
||||
self.slot_money = int(base.get("slot_money", 300_000))
|
||||
self.time_start_hm = int(base.get("time_start_hm", 930))
|
||||
self.time_end_hm = int(base.get("time_end_hm", 1520))
|
||||
except Exception as e:
|
||||
self.logger.debug("dart defaults 실패: %s", e)
|
||||
self._engine_params = {}
|
||||
|
||||
def _trade_on(self) -> bool:
|
||||
return get_env_bool("DART_TRADE_ENABLED", False)
|
||||
|
||||
def _in_trade_window(self) -> bool:
|
||||
now = dt.now()
|
||||
hm = now.hour * 100 + now.minute
|
||||
return self.time_start_hm <= hm < self.time_end_hm
|
||||
|
||||
def _db_raw(self):
|
||||
return self.db.raw if hasattr(self.db, "raw") else self.db
|
||||
|
||||
def _load_candidates(self) -> List[Dict]:
|
||||
out: List[Dict] = []
|
||||
lim = get_env_int("DART_CAND_LIMIT", 20) or 20
|
||||
try:
|
||||
conn_db = self._db_raw()
|
||||
watches = list_active_watch(conn_db)
|
||||
for w in watches:
|
||||
code = (w.get("stock_code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
out.append({
|
||||
"code": code.zfill(6) if code.isdigit() else code,
|
||||
"name": w.get("corp_name") or code,
|
||||
"score": 0.0,
|
||||
})
|
||||
if out:
|
||||
return out[:lim]
|
||||
today0 = dt.now().strftime("%Y-%m-%d") + " 00:00:00"
|
||||
rows = conn_db.conn.execute(
|
||||
"""
|
||||
SELECT stock_code, corp_name FROM dart_disclosures
|
||||
WHERE first_seen_at >= %s AND stock_code <> ''
|
||||
ORDER BY first_seen_at DESC LIMIT %s
|
||||
""",
|
||||
(today0, lim),
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
code = (r.get("stock_code") or "").strip()
|
||||
if not code:
|
||||
continue
|
||||
out.append({
|
||||
"code": code.zfill(6) if code.isdigit() else code,
|
||||
"name": r.get("corp_name") or code,
|
||||
"score": 0.0,
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.debug("DART 후보 로드 실패: %s", e)
|
||||
return out
|
||||
|
||||
def _candidate_filter(self, candidate: Dict) -> bool:
|
||||
if not self._trade_on():
|
||||
return False
|
||||
if not candidate.get("code"):
|
||||
return False
|
||||
if not self._in_trade_window():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _norm_candle(self, c: dict) -> dict:
|
||||
ct = c.get("candle_time") or c.get("candle_time_str", "")
|
||||
if isinstance(ct, str) and len(ct) == 19 and " " in ct:
|
||||
ct = ct.replace("-", "").replace(" ", "").replace(":", "")[:12]
|
||||
return {
|
||||
"candle_time": ct,
|
||||
"open": float(c.get("open", 0)),
|
||||
"high": float(c.get("high", 0)),
|
||||
"low": float(c.get("low", 0)),
|
||||
"close": float(c.get("close", 0)),
|
||||
"volume": float(c.get("volume", 0)),
|
||||
"is_confirmed": c.get("is_confirmed", 1),
|
||||
}
|
||||
|
||||
def _force_buy_test(self, code: str, name: str) -> Optional[Dict]:
|
||||
wsd = self.ws.get_price(code)
|
||||
px = 0.0
|
||||
if wsd:
|
||||
try:
|
||||
px = abs(float(str(wsd.get("stck_prpr", 0)).replace(",", "")))
|
||||
except Exception:
|
||||
px = 0.0
|
||||
if px <= 0:
|
||||
return None
|
||||
qty, rej = self._resolve_buy_qty_live(px)
|
||||
if rej or qty < 1:
|
||||
return None
|
||||
return {
|
||||
"code": code,
|
||||
"name": name,
|
||||
"price": px,
|
||||
"qty": qty,
|
||||
"stop_price": px * (1 + self.stop_loss_pct),
|
||||
"target_price": px * (1 + self.take_profit_pct),
|
||||
"atr_entry": 0.0,
|
||||
"size_class": "",
|
||||
"entry_features": {"force_test": True},
|
||||
}
|
||||
|
||||
def check_buy(self, code: str, name: str) -> Optional[Dict]:
|
||||
if not self._trade_on() or not self._in_trade_window():
|
||||
return None
|
||||
try:
|
||||
if get_env_bool("FORCE_BUY_TEST", False):
|
||||
return self._force_buy_test(code, name)
|
||||
|
||||
params = self._engine_params or de.get_dart_defaults_from_db()
|
||||
ev = event_time_for_code(self._db_raw(), code)
|
||||
if not ev:
|
||||
return None
|
||||
|
||||
need = de.dart_min_bars_required(params)
|
||||
candles_raw = self.ws.get_candles(code, self.candle_tf, n=need + 30)
|
||||
if len(candles_raw) < need:
|
||||
try:
|
||||
self.ws.fill_gap([code], force=True)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
candles = [self._norm_candle(c) for c in candles_raw]
|
||||
reject, msg, sig = de.check_buy_signal_dart_live(
|
||||
candles, params, event_candle_time=ev,
|
||||
)
|
||||
if reject:
|
||||
self.logger.info("🔍 [%s] %s(%s) %s", reject, name, code, msg or "")
|
||||
return None
|
||||
if not sig:
|
||||
return None
|
||||
|
||||
curr_price = float(sig.get("entry_price") or 0)
|
||||
if curr_price <= 0 or curr_price < self.min_price:
|
||||
return None
|
||||
hard_cap = get_env_int("DART_MAX_BUY_AMOUNT", 0) or get_env_int(
|
||||
"MAX_BUY_AMOUNT_PER_STOCK", 0,
|
||||
)
|
||||
qty, rej = self._resolve_buy_qty_live(curr_price, hard_cap=hard_cap)
|
||||
if rej or qty < 1:
|
||||
return None
|
||||
return {
|
||||
"code": code,
|
||||
"name": name,
|
||||
"price": curr_price,
|
||||
"qty": qty,
|
||||
"stop_price": curr_price * (1 + self.stop_loss_pct),
|
||||
"target_price": curr_price * (1 + self.take_profit_pct),
|
||||
"atr_entry": 0.0,
|
||||
"size_class": "",
|
||||
"entry_features": {
|
||||
"rsi": sig.get("rsi"),
|
||||
"event_bars": sig.get("event_bars"),
|
||||
"strategy": "DART",
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.info("🔍 [탈락-예외] %s %s: %s", name, code, e)
|
||||
return None
|
||||
|
||||
def check_sell_signals(self) -> List[Dict]:
|
||||
if not self.holdings:
|
||||
return []
|
||||
signals: List[Dict] = []
|
||||
params = self._engine_params or de.get_dart_defaults_from_db()
|
||||
now = dt.now()
|
||||
hm = now.hour * 100 + now.minute
|
||||
for code, holding in list(self.holdings.items()):
|
||||
try:
|
||||
name = holding.get("name", code)
|
||||
buy_price = float(holding.get("buy_price", 0))
|
||||
qty = int(holding.get("qty", 0))
|
||||
max_price = float(holding.get("max_price", buy_price))
|
||||
if qty <= 0 or buy_price <= 0:
|
||||
continue
|
||||
current_price = 0.0
|
||||
wsd = self.ws.get_price(code)
|
||||
if wsd:
|
||||
try:
|
||||
current_price = abs(
|
||||
float(str(wsd.get("stck_prpr", 0)).replace(",", ""))
|
||||
)
|
||||
except Exception:
|
||||
current_price = 0.0
|
||||
if current_price <= 0:
|
||||
continue
|
||||
if current_price > max_price:
|
||||
max_price = current_price
|
||||
holding["max_price"] = max_price
|
||||
bars = int(holding.get("bars_held") or 0)
|
||||
sell, reason = de.check_sell_signal_dart_live(
|
||||
buy_price=buy_price,
|
||||
highest=max_price,
|
||||
last_price=current_price,
|
||||
bars_held=bars,
|
||||
params=params,
|
||||
now_hm=hm,
|
||||
)
|
||||
if sell:
|
||||
signals.append({
|
||||
"code": code,
|
||||
"name": name,
|
||||
"qty": qty,
|
||||
"reason": reason,
|
||||
"price": current_price,
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error("DART 매도 체크 %s: %s", code, e)
|
||||
return signals
|
||||
Reference in New Issue
Block a user