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:
Your Name
2026-07-21 07:50:24 +09:00
parent 74db49149f
commit 61bec4bd1d
86 changed files with 4811 additions and 297 deletions

View File

@@ -10,6 +10,7 @@ from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from kis_trader.backtest.backtest_portfolio_common import (
flatten_remaining_portfolio_trades,
min_invest_ratio_of_slot,
portfolio_exposure_krw,
target_qty_and_cost,
@@ -26,6 +27,11 @@ from kis_trader.engine.scalping_engine import (
compute_rsi_series,
effective_tp_pct_from_params,
)
from kis_trader.engine.strategy_eod import (
eod_bar_time_key,
is_strategy_eod_bar,
resolve_strategy_eod_params,
)
from kis_trader.engine.tick_exit_common import (
backtest_sell_slip_pct,
backtest_tick_poll_ms,
@@ -125,7 +131,6 @@ def run_scalping_backtest_portfolio(
rsi_period = int(params.get("rsi_period", 3))
min_bars = rsi_period + 5
force_eod_exit = _to_bool(params.get("force_eod_exit"), False)
sl_pct = abs(float(params.get("sl_pct", 0.015)))
tp_pct = effective_tp_pct_from_params(params)
max_stocks = _max_stocks_from_params(params)
@@ -153,6 +158,7 @@ def run_scalping_backtest_portfolio(
ctx_by_code: Dict[str, Dict[str, Any]] = {}
all_times_set = set()
period_start = str(params.get("_backtest_period_start_key") or "")[:12]
for code, raw_rows in codes_candles.items():
if len(raw_rows) < min_bars:
continue
@@ -170,7 +176,10 @@ def run_scalping_backtest_portfolio(
"pending_entry": None,
}
for c in candles:
all_times_set.add(c["candle_time"])
ct = str(c.get("candle_time") or "")
if period_start and ct < period_start:
continue
all_times_set.add(ct)
all_times = sorted(all_times_set)
portfolio: Dict[str, Dict[str, Any]] = {}
@@ -248,27 +257,75 @@ def run_scalping_backtest_portfolio(
break # 1시각 1매수
# ── Phase 1: 보유 종목 청산 ──
# 실매는 벽시계 EOD(15:25) — 해당 분봉이 없는 종목도 직전가로 장마감청산
is_eod_t = is_strategy_eod_bar(t, params, "SCALP")
for code in list(portfolio.keys()):
ctx = ctx_by_code.get(code)
if ctx is None:
continue
idx = ctx["time_index"].get(t)
if idx is None:
pos = portfolio[code]
entry_t = str(pos.get("entry_time") or "")
entry_key = entry_t[:12] if entry_t else ""
t_key = str(t)[:12]
if entry_key and t_key <= entry_key:
continue
idx = ctx["time_index"].get(t)
candles = ctx["candles"]
day = t_key[:8] if len(t_key) >= 8 else str(t)[:8]
if idx is None:
if not is_eod_t:
continue
# 벽시계 EOD: 이 시각 봉 없음 → 진입 이후 마지막 확정봉 종가
last = None
for c in reversed(candles):
ct = str(c.get("candle_time") or "")
if not ct:
continue
if entry_key and ct[:12] < entry_key:
continue
if ct[:12] > t_key:
continue
last = c
break
if last is None:
continue
exit_price = float(last.get("close") or 0)
if exit_price <= 0:
continue
_eod_on, eod_hm = resolve_strategy_eod_params(params, "SCALP")
sell_time = eod_bar_time_key(day, eod_hm, default_hm="15:25") or t_key
trade = {
"code": code,
"buy_time": pos["entry_time"],
"sell_time": sell_time,
"buy_price": pos["entry_price"],
"sell_price": round(exit_price, 2),
"qty": pos.get("qty", 1),
"pnl": 0,
"sell_reason": "장마감청산",
"hold_min": 0,
"exit_source": "wallclock_eod",
}
if pos.get("rsi") is not None:
try:
trade["rsi_entry"] = round(float(pos["rsi"]), 1)
except (TypeError, ValueError):
pass
all_trades.append(trade)
ctx["last_exit_dt"][day] = _t2dt(sell_time)
ctx["daily_cnt"][day] = ctx["daily_cnt"].get(day, 0) + 1
del portfolio[code]
continue
c = candles[idx]
day = t[:8]
hi = float(c["high"])
lo = float(c["low"])
cl = float(c["close"])
op = float(c["open"])
is_eod_raw = (idx == len(candles) - 1) or (candles[idx + 1]["candle_time"][:8] != day)
is_eod = is_eod_raw and force_eod_exit
pos = portfolio[code]
if t == pos["entry_time"]:
continue
is_eod = is_eod_t
cur_c_info = {
"open": op,
@@ -308,6 +365,12 @@ def run_scalping_backtest_portfolio(
reason, exit_price = res
sell_time = t
ohlc_exit_count += 1
# EOD 봉이 15:30만 있어도 사유·시각은 실매 EOD(15:25)에 맞춤
if reason == "장마감청산":
eod_on, eod_hm = resolve_strategy_eod_params(params, "SCALP")
eod_key = eod_bar_time_key(day, eod_hm, default_hm="15:25")
if eod_key:
sell_time = eod_key
trade: Dict[str, Any] = {
"code": code,
"buy_time": pos["entry_time"],
@@ -419,6 +482,12 @@ def run_scalping_backtest_portfolio(
skip_stats["ohlc_exit_count"] = ohlc_exit_count
if tick_entry_count:
skip_stats["tick_entry_count"] = tick_entry_count
flat_n = flatten_remaining_portfolio_trades(
portfolio, ctx_by_code, all_trades,
params=params, strategy=strategy,
)
if flat_n:
skip_stats["bt_flatten_count"] = flat_n
if skip_stats:
params["_portfolio_skip_stats"] = skip_stats
all_trades.sort(key=lambda x: x["sell_time"])