feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
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,
|
||||
@@ -14,7 +15,12 @@ from kis_trader.backtest.backtest_portfolio_common import (
|
||||
from kis_trader.engine.scalping_engine import (
|
||||
_t2dt,
|
||||
_to_bool,
|
||||
check_sell_signal_backtest_bar,
|
||||
)
|
||||
from kis_trader.engine.tick_exit_common import (
|
||||
backtest_sell_slip_pct,
|
||||
backtest_tick_poll_ms,
|
||||
collect_minute_ticks,
|
||||
resolve_backtest_sell,
|
||||
)
|
||||
from kis_trader.share.stock_share import share_denom_for_code
|
||||
from kis_trader.engine.indicator_cache import (
|
||||
@@ -25,6 +31,8 @@ from kis_trader.engine.whipsaw_filter import inject_whipsaw_ticks_into_params
|
||||
from kis_trader.backtest.trigger_snapshot_loader import inject_trigger_snapshots_into_params
|
||||
from kis_trader.strategies.breakout import (
|
||||
_bt_slot_key,
|
||||
breakout_backtest_tick_fallback_ohlc,
|
||||
breakout_backtest_use_tick_exit,
|
||||
breakout_entry_mode,
|
||||
breakout_invest_amount_krw,
|
||||
breakout_scan_buy_at_bar,
|
||||
@@ -108,11 +116,20 @@ def _total_budget_from_params(params: Dict[str, Any]) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _resolve_breakout_sl_pct_ui(params: Dict[str, Any]) -> float:
|
||||
"""실매와 동일 — ``stop_loss_pct``(비율) → UI%, 없으면 ``sl_pct``(UI% 또는 비율)."""
|
||||
if params.get("stop_loss_pct") not in (None, ""):
|
||||
return abs(float(params["stop_loss_pct"])) * 100.0
|
||||
raw = abs(float(params.get("sl_pct", 2.0)))
|
||||
# UI% 는 보통 ≥0.5, 비율은 0.05 등
|
||||
return raw if raw >= 0.5 else raw * 100.0
|
||||
|
||||
|
||||
def _resolve_breakout_invest_cap(params: Dict[str, Any]) -> float:
|
||||
slot_money = float(params.get("slot_money", 2_000_000))
|
||||
sl_pct = abs(float(params.get("stop_loss_pct", params.get("sl_pct", -0.02))))
|
||||
sl_pct_ui = _resolve_breakout_sl_pct_ui(params)
|
||||
max_loss_krw = normalize_breakout_max_loss_krw(params.get("max_loss_krw", 200_000))
|
||||
return breakout_invest_amount_krw(max_loss_krw, sl_pct * 100.0, slot_money)
|
||||
return breakout_invest_amount_krw(max_loss_krw, sl_pct_ui, slot_money)
|
||||
|
||||
|
||||
def run_breakout_backtest_portfolio(
|
||||
@@ -127,7 +144,7 @@ def run_breakout_backtest_portfolio(
|
||||
시각순 포트폴리오 돌파 백테스트.
|
||||
|
||||
- 매수: ``check_buy_signal_breakout_live`` → 다음 봉 시가 예약
|
||||
- 매도: ``check_sell_signal_breakout_live`` via ``check_sell_signal_backtest_bar``
|
||||
- 매도: 틱 우선 ``resolve_backtest_sell`` → ``check_sell_signal_breakout_live``
|
||||
"""
|
||||
lookback_min = int(params.get("lookback_min", 1))
|
||||
vol_window = int(params.get("vol_window", 7))
|
||||
@@ -157,9 +174,16 @@ def run_breakout_backtest_portfolio(
|
||||
attach_indicator_caches_to_params(buy_params, codes_candles)
|
||||
|
||||
skipped_micro_buys = 0
|
||||
use_tick_exit = bool(ticks_by_code) and breakout_backtest_use_tick_exit(params)
|
||||
tick_fallback_ohlc = breakout_backtest_tick_fallback_ohlc(params)
|
||||
tick_poll_ms = backtest_tick_poll_ms(params, strategy_env="BREAKOUT_BACKTEST_POLL_MS")
|
||||
tick_sell_slip = backtest_sell_slip_pct(params, strategy_env="BREAKOUT_BACKTEST_SELL_SLIP_PCT")
|
||||
tick_exit_count = 0
|
||||
ohlc_exit_count = 0
|
||||
|
||||
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
|
||||
@@ -189,7 +213,10 @@ def run_breakout_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]] = {}
|
||||
@@ -197,7 +224,33 @@ def run_breakout_backtest_portfolio(
|
||||
|
||||
universe_timeline = params.get("_universe_timeline")
|
||||
|
||||
from kis_trader.backtest.backtest_env_timeline import apply_env_timeline_at
|
||||
|
||||
for t in all_times:
|
||||
if apply_env_timeline_at(params, t, "BREAKOUT"):
|
||||
max_stocks = _max_stocks_from_params(params)
|
||||
slot_money = float(params.get("slot_money", 2_000_000))
|
||||
total_budget = _total_budget_from_params(params)
|
||||
if total_budget <= 0:
|
||||
total_budget = float(max_stocks * slot_money)
|
||||
invest_cap = _resolve_breakout_invest_cap(params)
|
||||
time_start_hm = int(params.get("time_start_hm", 900))
|
||||
time_end_hm = int(params.get("time_end_hm", 1030))
|
||||
buy_params["time_start_hm"] = time_start_hm
|
||||
buy_params["time_end_hm"] = time_end_hm
|
||||
for _bk in (
|
||||
"lookback_min", "vol_window", "vol_mult", "prev_chg_min", "prev_chg_max",
|
||||
"sl_pct", "tp_pct", "stop_loss_pct", "take_profit_pct",
|
||||
"trail_pct", "trail_arm_pct",
|
||||
"shoulder_min_high", "shoulder_min_high_pct", "shoulder_cut_pct",
|
||||
"max_daily", "cooldown_min", "max_daily_chg", "min_price", "skip_hts_scan_dupes",
|
||||
"confirm_margin_pct", "body_min_pct", "ratchet_tiers", "max_hold_bars",
|
||||
"sl_mode", "atr_period", "atr_sl_mult", "atr_sl_min_pct", "atr_sl_max_pct",
|
||||
"eod_enabled", "eod_hm", "entry_mode",
|
||||
):
|
||||
if _bk in params:
|
||||
buy_params[_bk] = params[_bk]
|
||||
|
||||
slot_key = _bt_slot_key(t, int(params.get("scan_interval_min", 1)))
|
||||
# 초단위 유니버스(실매 정합) — 타임라인 우선, 없으면 1분 슬롯 폴백
|
||||
uni_codes = _universe_codes_at(t, slot_key, universe_timeline, universe_by_slot)
|
||||
@@ -270,21 +323,33 @@ def run_breakout_backtest_portfolio(
|
||||
bar = dict(c)
|
||||
if "open" not in bar or bar.get("open") in (None, ""):
|
||||
bar["open"] = float(c.get("open") or cl)
|
||||
res = check_sell_signal_backtest_bar(
|
||||
minute_ticks = (
|
||||
collect_minute_ticks(ticks_by_code, code, t) if use_tick_exit else None
|
||||
)
|
||||
res = resolve_backtest_sell(
|
||||
pos,
|
||||
bar,
|
||||
params,
|
||||
is_eod=is_eod,
|
||||
sell_fn=check_sell_signal_breakout_live,
|
||||
low_mode="current",
|
||||
ticks=minute_ticks,
|
||||
use_tick_exit=use_tick_exit,
|
||||
tick_fallback_ohlc=tick_fallback_ohlc,
|
||||
poll_ms=tick_poll_ms,
|
||||
slip_pct=tick_sell_slip,
|
||||
)
|
||||
if not res:
|
||||
continue
|
||||
reason, exit_price = res
|
||||
reason, exit_price, sell_time, _hold_min, exit_src = res
|
||||
if exit_src == "ws_ticks":
|
||||
tick_exit_count += 1
|
||||
else:
|
||||
ohlc_exit_count += 1
|
||||
all_trades.append({
|
||||
"code": code,
|
||||
"buy_time": pos["entry_time"],
|
||||
"sell_time": t,
|
||||
"sell_time": sell_time or t,
|
||||
"buy_price": pos["entry_price"],
|
||||
"sell_price": round(exit_price, 2),
|
||||
"qty": pos.get("qty", 1),
|
||||
@@ -292,7 +357,7 @@ def run_breakout_backtest_portfolio(
|
||||
"sell_reason": reason,
|
||||
"hold_min": 0,
|
||||
})
|
||||
ctx["last_exit_dt"][day] = _t2dt(t)
|
||||
ctx["last_exit_dt"][day] = _t2dt(sell_time or t)
|
||||
del portfolio[code]
|
||||
|
||||
# ── Phase 2: 신규 매수 신호 ──
|
||||
@@ -407,7 +472,19 @@ def run_breakout_backtest_portfolio(
|
||||
_pri, pick_code, pe = candidates[0]
|
||||
ctx_by_code[pick_code]["pending_entry"] = pe
|
||||
|
||||
skip_stats: Dict[str, Any] = {}
|
||||
if skipped_micro_buys:
|
||||
params["_portfolio_skip_stats"] = {"skipped_micro_buys": skipped_micro_buys}
|
||||
skip_stats["skipped_micro_buys"] = skipped_micro_buys
|
||||
if tick_exit_count or ohlc_exit_count:
|
||||
skip_stats["tick_exit_count"] = tick_exit_count
|
||||
skip_stats["ohlc_exit_count"] = ohlc_exit_count
|
||||
flat_n = flatten_remaining_portfolio_trades(
|
||||
portfolio, ctx_by_code, all_trades,
|
||||
params=params, strategy="BREAKOUT",
|
||||
)
|
||||
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"])
|
||||
return all_trades
|
||||
|
||||
Reference in New Issue
Block a user