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:
@@ -4,6 +4,8 @@
|
||||
|
||||
- 진입: live_align 신호봉(T-1) → 진입봉(T) 첫 틱/시가
|
||||
- 청산: 1~2초 폴링 근사로 ``check_sell_signal_momentum_live`` → **틱 체결가**
|
||||
- 틱 공백: OHLC intrabar 폴백(기본 OFF) 대신 **last price + 벽시계**
|
||||
(``MOMENTUM_BACKTEST_WALLCLOCK_LAST_PRICE``, 기본 ON)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -51,6 +53,145 @@ def momentum_backtest_tick_fallback_ohlc(params: Optional[Dict[str, Any]] = None
|
||||
)
|
||||
|
||||
|
||||
def momentum_backtest_wallclock_last_price(params: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""틱 공백 시 실매처럼 last price + 벽시계로 청산 검사 (기본 ON).
|
||||
|
||||
OHLC intrabar 폴백과 다름 — 고가·저가 경로를 만들지 않고
|
||||
직전 틱가(없으면 분봉 종가 1개)만 사용. 파람서치가 OHLC 가짜경로에
|
||||
맞추는 것을 피하면서 시간컷/EOD·현재가 청산을 실매에 맞춘다.
|
||||
끄려면 ``MOMENTUM_BACKTEST_WALLCLOCK_LAST_PRICE=0``.
|
||||
"""
|
||||
return _param_bool(
|
||||
params,
|
||||
"backtest_wallclock_last_price",
|
||||
"MOMENTUM_BACKTEST_WALLCLOCK_LAST_PRICE",
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def update_momentum_bt_last_px(
|
||||
position: Dict[str, Any],
|
||||
px: float,
|
||||
t_key: str = "",
|
||||
) -> None:
|
||||
"""백테 보유 중 last price 캐시 (틱·봉 종가 갱신)."""
|
||||
try:
|
||||
v = float(px)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if v <= 0:
|
||||
return
|
||||
position["_bt_last_px"] = v
|
||||
tk = str(t_key or "").strip()
|
||||
if tk:
|
||||
position["_bt_last_px_t"] = tk[:12]
|
||||
|
||||
|
||||
def resolve_momentum_wallclock_last_px(
|
||||
position: Dict[str, Any],
|
||||
bar: Optional[Dict[str, Any]],
|
||||
*,
|
||||
ticks_by_code: Optional[Dict[str, Dict[str, List[Dict[str, Any]]]]] = None,
|
||||
code: str = "",
|
||||
minute_key: str = "",
|
||||
) -> Optional[float]:
|
||||
"""last price: 해당 분 마지막 틱 → 분봉 종가 → 캐시 → 진입가."""
|
||||
mk = str(minute_key or (bar or {}).get("candle_time") or "")[:12]
|
||||
if ticks_by_code and code and mk:
|
||||
minute_ticks = collect_minute_ticks(ticks_by_code, code, mk)
|
||||
if minute_ticks:
|
||||
try:
|
||||
from kis_trader.backtest.shared_ticks import TickColumnView
|
||||
if isinstance(minute_ticks, TickColumnView):
|
||||
last_i = None
|
||||
for i in minute_ticks.iter_idx():
|
||||
last_i = i
|
||||
if last_i is not None:
|
||||
px = float(minute_ticks.owner._price[last_i])
|
||||
if px > 0:
|
||||
return px
|
||||
else:
|
||||
for tick in reversed(list(minute_ticks)):
|
||||
px = float(tick.get("price") or 0)
|
||||
if px > 0:
|
||||
return px
|
||||
except Exception:
|
||||
for tick in reversed(list(minute_ticks)):
|
||||
try:
|
||||
px = float(tick.get("price") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if px > 0:
|
||||
return px
|
||||
if bar is not None:
|
||||
try:
|
||||
px = float(bar.get("close") or 0)
|
||||
if px > 0:
|
||||
return px
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
cached = position.get("_bt_last_px")
|
||||
if cached is not None:
|
||||
try:
|
||||
px = float(cached)
|
||||
if px > 0:
|
||||
return px
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
px = float(position.get("entry_price") or 0)
|
||||
return px if px > 0 else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def try_momentum_sell_wallclock_last(
|
||||
position: Dict[str, Any],
|
||||
last_px: float,
|
||||
candle_time: str,
|
||||
params: Dict[str, Any],
|
||||
*,
|
||||
is_eod: bool = False,
|
||||
) -> Optional[Tuple[str, float, str, float]]:
|
||||
"""실매 ``check_sell_signals`` 와 동일 — last 1가 + 벽시계 candle_time.
|
||||
|
||||
Returns:
|
||||
(reason, fill_price, sell_time, hold_min) 또는 None
|
||||
"""
|
||||
try:
|
||||
px = float(last_px)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if px <= 0:
|
||||
return None
|
||||
ct = str(candle_time or "")[:12]
|
||||
if len(ct) < 12:
|
||||
return None
|
||||
|
||||
mp = max(float(position.get("max_price", position.get("entry_price") or px)), px)
|
||||
position["max_price"] = mp
|
||||
candle = {
|
||||
"high": mp,
|
||||
"low": px,
|
||||
"close": px,
|
||||
"candle_time": ct,
|
||||
}
|
||||
res = check_sell_signal_momentum_live(position, candle, params, is_eod=is_eod)
|
||||
if not res:
|
||||
return None
|
||||
reason, _theoretical = res
|
||||
slip_pct = abs(float(get_env_float("MOMENTUM_BACKTEST_SELL_SLIP_PCT", 0.0)))
|
||||
fill_px = px * (1.0 - slip_pct / 100.0) if slip_pct > 0 else px
|
||||
entry_time = str(position.get("entry_time") or "")
|
||||
try:
|
||||
entry_dt = parse_backtest_time(entry_time)
|
||||
sell_dt = parse_backtest_time(ct)
|
||||
hold_min = round((sell_dt - entry_dt).total_seconds() / 60.0, 1)
|
||||
except ValueError:
|
||||
hold_min = 0.0
|
||||
return reason, float(fill_px), ct, hold_min
|
||||
|
||||
|
||||
def momentum_backtest_tick_only_codes(params: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""틱재생 시 **틱 데이터가 전혀 없는 종목을 백테/파람서치에서 제외** (기본 ON).
|
||||
|
||||
@@ -403,11 +544,11 @@ def resolve_momentum_sell_for_bar(
|
||||
code: str = "",
|
||||
) -> Optional[Tuple[str, float, str, float, str]]:
|
||||
"""
|
||||
한 분봉 청산 — 틱 우선, 없으면 OHLC intrabar 폴백.
|
||||
한 분봉 청산 — 틱 우선 → (옵션) OHLC intrabar → last-price 벽시계.
|
||||
|
||||
Returns:
|
||||
(reason, fill_price, sell_time, hold_min, exit_source)
|
||||
exit_source: ws_ticks | ohlc_bar
|
||||
exit_source: ws_ticks | ohlc_bar | wallclock_last
|
||||
"""
|
||||
from kis_trader.engine.momentum_engine import check_sell_signal_momentum_backtest_bar
|
||||
|
||||
@@ -424,18 +565,51 @@ def resolve_momentum_sell_for_bar(
|
||||
if tick_res:
|
||||
reason, fill_px, sell_time, hold_min = tick_res
|
||||
return reason, fill_px, sell_time, hold_min, "ws_ticks"
|
||||
# 틱은 있었으나 미청산 → last 갱신 후 벽시계 경로에서 시간컷 등 재검사
|
||||
try:
|
||||
from kis_trader.backtest.shared_ticks import TickColumnView
|
||||
if isinstance(minute_ticks, TickColumnView):
|
||||
last_i = None
|
||||
for i in minute_ticks.iter_idx():
|
||||
last_i = i
|
||||
if last_i is not None:
|
||||
update_momentum_bt_last_px(
|
||||
position, float(minute_ticks.owner._price[last_i]), ct,
|
||||
)
|
||||
else:
|
||||
for tick in reversed(list(minute_ticks)):
|
||||
px = float(tick.get("price") or 0)
|
||||
if px > 0:
|
||||
update_momentum_bt_last_px(position, px, ct)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not momentum_backtest_tick_fallback_ohlc(params):
|
||||
if momentum_backtest_tick_fallback_ohlc(params):
|
||||
res = check_sell_signal_momentum_backtest_bar(position, bar, params, is_eod=is_eod)
|
||||
if res:
|
||||
reason, exit_price = res
|
||||
try:
|
||||
entry_dt = parse_backtest_time(entry_time)
|
||||
sell_dt = parse_backtest_time(ct)
|
||||
hold_min = round((sell_dt - entry_dt).total_seconds() / 60.0, 1)
|
||||
except ValueError:
|
||||
hold_min = 0.0
|
||||
return reason, float(exit_price), ct, hold_min, "ohlc_bar"
|
||||
|
||||
if not momentum_backtest_wallclock_last_price(params):
|
||||
return None
|
||||
|
||||
res = check_sell_signal_momentum_backtest_bar(position, bar, params, is_eod=is_eod)
|
||||
if not res:
|
||||
last_px = resolve_momentum_wallclock_last_px(
|
||||
position, bar, ticks_by_code=ticks_by_code, code=code, minute_key=ct,
|
||||
)
|
||||
if last_px is None:
|
||||
return None
|
||||
reason, exit_price = res
|
||||
try:
|
||||
entry_dt = parse_backtest_time(entry_time)
|
||||
sell_dt = parse_backtest_time(ct)
|
||||
hold_min = round((sell_dt - entry_dt).total_seconds() / 60.0, 1)
|
||||
except ValueError:
|
||||
hold_min = 0.0
|
||||
return reason, float(exit_price), ct, hold_min, "ohlc_bar"
|
||||
update_momentum_bt_last_px(position, last_px, ct)
|
||||
wall_res = try_momentum_sell_wallclock_last(
|
||||
position, last_px, ct, params, is_eod=is_eod,
|
||||
)
|
||||
if not wall_res:
|
||||
return None
|
||||
reason, fill_px, sell_time, hold_min = wall_res
|
||||
return reason, fill_px, sell_time, hold_min, "wallclock_last"
|
||||
|
||||
Reference in New Issue
Block a user