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:
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user