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:
@@ -40,9 +40,11 @@ from kis_trader.engine.momentum_tick_replay import (
|
||||
momentum_backtest_scan_sec,
|
||||
momentum_backtest_skip_pre_subscribe,
|
||||
momentum_backtest_use_tick_exit,
|
||||
momentum_backtest_wallclock_last_price,
|
||||
momentum_live_align_enabled,
|
||||
resolve_momentum_sell_for_bar,
|
||||
try_momentum_sell_on_ticks,
|
||||
update_momentum_bt_last_px,
|
||||
)
|
||||
from kis_trader.backtest.momentum_tick_loader import entry_before_first_tick
|
||||
from kis_trader.backtest.momentum_universe_timeline import (
|
||||
@@ -149,6 +151,8 @@ def _try_open_momentum_position(
|
||||
"target": pe["target"],
|
||||
"max_price": entry_price,
|
||||
"rsi": pe.get("rsi"),
|
||||
"_bt_last_px": entry_price,
|
||||
"_bt_last_px_t": str(entry_time or "")[:12],
|
||||
}
|
||||
src = str(pe.get("entry_source") or "ohlc_open")
|
||||
if src == "ws_ticks":
|
||||
@@ -215,7 +219,8 @@ def _record_momentum_sell(
|
||||
all_trades: List[Dict],
|
||||
tick_exit_count: int,
|
||||
ohlc_exit_count: int,
|
||||
) -> Tuple[int, int]:
|
||||
wallclock_exit_count: int = 0,
|
||||
) -> Tuple[int, int, int]:
|
||||
trade: Dict[str, Any] = {
|
||||
"code": code,
|
||||
"buy_time": pos["entry_time"],
|
||||
@@ -237,9 +242,11 @@ def _record_momentum_sell(
|
||||
del portfolio[code]
|
||||
if exit_source == "ws_ticks":
|
||||
tick_exit_count += 1
|
||||
elif exit_source == "wallclock_last":
|
||||
wallclock_exit_count += 1
|
||||
else:
|
||||
ohlc_exit_count += 1
|
||||
return tick_exit_count, ohlc_exit_count
|
||||
return tick_exit_count, ohlc_exit_count, wallclock_exit_count
|
||||
|
||||
|
||||
def _process_sells_for_scan(
|
||||
@@ -252,29 +259,68 @@ def _process_sells_for_scan(
|
||||
all_trades: List[Dict],
|
||||
tick_exit_count: int,
|
||||
ohlc_exit_count: int,
|
||||
wallclock_exit_count: int,
|
||||
scan_sec: int,
|
||||
) -> Tuple[int, int]:
|
||||
"""스캔 시각까지 틱·OHLC 청산 (실매 루프: 매도 먼저)."""
|
||||
) -> Tuple[int, int, int]:
|
||||
"""스캔 시각까지 틱·벽시계 last 청산 (실매 루프: 매도 먼저)."""
|
||||
bar_t = scan_key[:12]
|
||||
is_eod = is_strategy_eod_bar(bar_t, params, "MOMENTUM")
|
||||
use_wall = momentum_backtest_wallclock_last_price(params)
|
||||
for code in list(portfolio.keys()):
|
||||
ctx = ctx_by_code.get(code)
|
||||
if ctx is None:
|
||||
continue
|
||||
idx = ctx["time_index"].get(bar_t)
|
||||
if idx is None:
|
||||
continue
|
||||
candles = ctx["candles"]
|
||||
c = candles[idx]
|
||||
c = candles[idx] if idx is not None else None
|
||||
pos = portfolio[code]
|
||||
if str(pos.get("entry_time") or "")[:12] == bar_t:
|
||||
continue
|
||||
entry_time = str(pos.get("entry_time") or "")
|
||||
|
||||
sold = False
|
||||
minute_ticks = []
|
||||
if momentum_backtest_use_tick_exit(params) and ticks_by_code:
|
||||
minute_ticks = collect_minute_ticks(ticks_by_code, code, bar_t)
|
||||
# 공유메모리 컬럼 뷰면 dict 재구성 없이 뷰 캡핑(동일 문자열 비교). 아니면 기존 리스트 캡핑.
|
||||
|
||||
if minute_ticks:
|
||||
try:
|
||||
from kis_trader.backtest.shared_ticks import TickColumnView
|
||||
_is_view = isinstance(minute_ticks, TickColumnView)
|
||||
except Exception:
|
||||
_is_view = False
|
||||
if _is_view:
|
||||
capped = minute_ticks.cap_by_tick_time_le(scan_key[:14])
|
||||
else:
|
||||
capped = [
|
||||
tk for tk in minute_ticks
|
||||
if str(tk.get("tick_time") or "")[:14] <= scan_key[:14]
|
||||
]
|
||||
if capped:
|
||||
if _is_view:
|
||||
last_i = None
|
||||
for i in capped.iter_idx():
|
||||
last_i = i
|
||||
if last_i is not None:
|
||||
update_momentum_bt_last_px(
|
||||
pos, float(capped.owner._price[last_i]), bar_t,
|
||||
)
|
||||
else:
|
||||
for tk in reversed(capped):
|
||||
try:
|
||||
px = float(tk.get("price") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if px > 0:
|
||||
update_momentum_bt_last_px(pos, px, bar_t)
|
||||
break
|
||||
elif c is not None:
|
||||
try:
|
||||
update_momentum_bt_last_px(pos, float(c["close"]), bar_t)
|
||||
except (TypeError, ValueError, KeyError):
|
||||
pass
|
||||
|
||||
sold = False
|
||||
if minute_ticks:
|
||||
try:
|
||||
from kis_trader.backtest.shared_ticks import TickColumnView
|
||||
_is_view = isinstance(minute_ticks, TickColumnView)
|
||||
@@ -293,12 +339,13 @@ def _process_sells_for_scan(
|
||||
)
|
||||
if tick_res:
|
||||
reason, fill_px, sell_time, hold_min = tick_res
|
||||
tick_exit_count, ohlc_exit_count = _record_momentum_sell(
|
||||
tick_exit_count, ohlc_exit_count, wallclock_exit_count = _record_momentum_sell(
|
||||
portfolio=portfolio, code=code, ctx=ctx, pos=pos,
|
||||
reason=reason, exit_price=fill_px, sell_time_key=sell_time,
|
||||
hold_min=hold_min, exit_source="ws_ticks",
|
||||
all_trades=all_trades,
|
||||
tick_exit_count=tick_exit_count, ohlc_exit_count=ohlc_exit_count,
|
||||
wallclock_exit_count=wallclock_exit_count,
|
||||
)
|
||||
sold = True
|
||||
|
||||
@@ -308,13 +355,28 @@ def _process_sells_for_scan(
|
||||
if not _is_minute_tail_scan(scan_key, scan_sec):
|
||||
continue
|
||||
|
||||
cur_c_info = {
|
||||
"open": float(c["open"]),
|
||||
"high": float(c["high"]),
|
||||
"low": float(c["low"]),
|
||||
"close": float(c["close"]),
|
||||
"candle_time": bar_t,
|
||||
}
|
||||
if c is None and not (use_wall and pos.get("_bt_last_px")):
|
||||
continue
|
||||
|
||||
if c is not None:
|
||||
cur_c_info = {
|
||||
"open": float(c["open"]),
|
||||
"high": float(c["high"]),
|
||||
"low": float(c["low"]),
|
||||
"close": float(c["close"]),
|
||||
"candle_time": bar_t,
|
||||
}
|
||||
else:
|
||||
last_px = float(pos.get("_bt_last_px") or 0)
|
||||
if last_px <= 0:
|
||||
continue
|
||||
cur_c_info = {
|
||||
"open": last_px,
|
||||
"high": last_px,
|
||||
"low": last_px,
|
||||
"close": last_px,
|
||||
"candle_time": bar_t,
|
||||
}
|
||||
sell_res = resolve_momentum_sell_for_bar(
|
||||
pos, cur_c_info, params,
|
||||
is_eod=is_eod,
|
||||
@@ -324,14 +386,15 @@ def _process_sells_for_scan(
|
||||
if not sell_res:
|
||||
continue
|
||||
reason, exit_price, sell_time_key, hold_min, exit_source = sell_res
|
||||
tick_exit_count, ohlc_exit_count = _record_momentum_sell(
|
||||
tick_exit_count, ohlc_exit_count, wallclock_exit_count = _record_momentum_sell(
|
||||
portfolio=portfolio, code=code, ctx=ctx, pos=pos,
|
||||
reason=reason, exit_price=exit_price, sell_time_key=sell_time_key,
|
||||
hold_min=hold_min, exit_source=exit_source,
|
||||
all_trades=all_trades,
|
||||
tick_exit_count=tick_exit_count, ohlc_exit_count=ohlc_exit_count,
|
||||
wallclock_exit_count=wallclock_exit_count,
|
||||
)
|
||||
return tick_exit_count, ohlc_exit_count
|
||||
return tick_exit_count, ohlc_exit_count, wallclock_exit_count
|
||||
|
||||
|
||||
def _universe_codes_for_scan(
|
||||
@@ -522,6 +585,7 @@ def run_momentum_backtest_portfolio(
|
||||
skipped_micro_buys = 0
|
||||
tick_exit_count = 0
|
||||
ohlc_exit_count = 0
|
||||
wallclock_exit_count = 0
|
||||
entry_stats: Dict[str, int] = {}
|
||||
live_align = momentum_live_align_enabled(params)
|
||||
live_scan_queue = momentum_backtest_live_scan_queue_enabled(params)
|
||||
@@ -589,13 +653,14 @@ def run_momentum_backtest_portfolio(
|
||||
tp_pct = effective_tp_pct_from_params(params)
|
||||
slot_key = _slot_key(bar_t, int(params.get("scan_interval_min", 1)))
|
||||
|
||||
tick_exit_count, ohlc_exit_count = _process_sells_for_scan(
|
||||
tick_exit_count, ohlc_exit_count, wallclock_exit_count = _process_sells_for_scan(
|
||||
portfolio, ctx_by_code, scan_key,
|
||||
params=params,
|
||||
ticks_by_code=ticks_by_code,
|
||||
all_trades=all_trades,
|
||||
tick_exit_count=tick_exit_count,
|
||||
ohlc_exit_count=ohlc_exit_count,
|
||||
wallclock_exit_count=wallclock_exit_count,
|
||||
scan_sec=scan_sec,
|
||||
)
|
||||
|
||||
@@ -700,22 +765,41 @@ def run_momentum_backtest_portfolio(
|
||||
if ctx is None:
|
||||
continue
|
||||
idx = ctx["time_index"].get(t)
|
||||
if idx is None:
|
||||
continue
|
||||
candles = ctx["candles"]
|
||||
c = candles[idx]
|
||||
day = t[:8]
|
||||
if str(portfolio[code]["entry_time"])[:12] == str(t)[:12]:
|
||||
c = candles[idx] if idx is not None else None
|
||||
pos = portfolio[code]
|
||||
if str(pos.get("entry_time") or "")[:12] == str(t)[:12]:
|
||||
continue
|
||||
if c is not None:
|
||||
try:
|
||||
update_momentum_bt_last_px(pos, float(c["close"]), t)
|
||||
except (TypeError, ValueError, KeyError):
|
||||
pass
|
||||
elif not (
|
||||
momentum_backtest_wallclock_last_price(params)
|
||||
and pos.get("_bt_last_px")
|
||||
):
|
||||
continue
|
||||
is_eod = is_strategy_eod_bar(t, params, "MOMENTUM")
|
||||
cur_c_info = {
|
||||
"open": float(c["open"]),
|
||||
"high": float(c["high"]),
|
||||
"low": float(c["low"]),
|
||||
"close": float(c["close"]),
|
||||
"candle_time": t,
|
||||
}
|
||||
pos = portfolio[code]
|
||||
if c is not None:
|
||||
cur_c_info = {
|
||||
"open": float(c["open"]),
|
||||
"high": float(c["high"]),
|
||||
"low": float(c["low"]),
|
||||
"close": float(c["close"]),
|
||||
"candle_time": t,
|
||||
}
|
||||
else:
|
||||
last_px = float(pos.get("_bt_last_px") or 0)
|
||||
if last_px <= 0:
|
||||
continue
|
||||
cur_c_info = {
|
||||
"open": last_px,
|
||||
"high": last_px,
|
||||
"low": last_px,
|
||||
"close": last_px,
|
||||
"candle_time": t,
|
||||
}
|
||||
sell_res = resolve_momentum_sell_for_bar(
|
||||
pos, cur_c_info, params,
|
||||
is_eod=is_eod,
|
||||
@@ -725,12 +809,13 @@ def run_momentum_backtest_portfolio(
|
||||
if not sell_res:
|
||||
continue
|
||||
reason, exit_price, sell_time_key, hold_min, exit_source = sell_res
|
||||
tick_exit_count, ohlc_exit_count = _record_momentum_sell(
|
||||
tick_exit_count, ohlc_exit_count, wallclock_exit_count = _record_momentum_sell(
|
||||
portfolio=portfolio, code=code, ctx=ctx, pos=pos,
|
||||
reason=reason, exit_price=exit_price, sell_time_key=sell_time_key,
|
||||
hold_min=hold_min, exit_source=exit_source,
|
||||
all_trades=all_trades,
|
||||
tick_exit_count=tick_exit_count, ohlc_exit_count=ohlc_exit_count,
|
||||
wallclock_exit_count=wallclock_exit_count,
|
||||
)
|
||||
|
||||
if len(portfolio) >= max_stocks:
|
||||
@@ -776,9 +861,10 @@ def run_momentum_backtest_portfolio(
|
||||
skip_stats: Dict[str, Any] = {}
|
||||
if skipped_micro_buys:
|
||||
skip_stats["skipped_micro_buys"] = skipped_micro_buys
|
||||
if tick_exit_count or ohlc_exit_count:
|
||||
if tick_exit_count or ohlc_exit_count or wallclock_exit_count:
|
||||
skip_stats["tick_exit_count"] = tick_exit_count
|
||||
skip_stats["ohlc_exit_count"] = ohlc_exit_count
|
||||
skip_stats["wallclock_exit_count"] = wallclock_exit_count
|
||||
if entry_stats:
|
||||
skip_stats.update(entry_stats)
|
||||
if live_scan_queue and live_align:
|
||||
|
||||
Reference in New Issue
Block a user