ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -9,6 +9,29 @@ from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
def ensure_meta_db(meta_out: Optional[Dict[str, Any]]) -> Any:
|
||||
"""``meta_out['db']`` 보장 — 틱/스냅샷 로드용.
|
||||
|
||||
웹 API는 항상 db 를 넣지만, CLI·잡·일부 스크립트에서 빠지면
|
||||
틱 미로드 → OHLC만 청산 → 실매와 크게 어긋난다.
|
||||
없을 때만 TradeDB 를 만들고 ``_db_created_for_bt`` 표시.
|
||||
"""
|
||||
if meta_out is None:
|
||||
return None
|
||||
db = meta_out.get("db")
|
||||
if db is not None:
|
||||
return db
|
||||
try:
|
||||
from database import TradeDB
|
||||
|
||||
db = TradeDB()
|
||||
meta_out["db"] = db
|
||||
meta_out["_db_created_for_bt"] = True
|
||||
return db
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def flatten_remaining_portfolio_trades(
|
||||
portfolio: Dict[str, Dict[str, Any]],
|
||||
ctx_by_code: Dict[str, Dict[str, Any]],
|
||||
@@ -168,6 +191,13 @@ STRATEGY_PORTFOLIO_KEYS: Dict[str, Dict[str, Tuple[str, ...]]] = {
|
||||
"per_stock_cap": ("MOMENTUM_MAX_BUY_AMOUNT", "MAX_BUY_AMOUNT_PER_STOCK"),
|
||||
"min_invest_env": ("MOMENTUM_MIN_INVEST_RATIO_OF_SLOT", "MIN_INVEST_RATIO_OF_SLOT"),
|
||||
},
|
||||
"US_MOMENTUM": {
|
||||
"max_stocks": ("US_MOMENTUM_MAX_STOCKS",),
|
||||
"total_budget": ("US_MOMENTUM_TOTAL_BUDGET", "US_MOMENTUM_TOTAL_BUDGET_KRW"),
|
||||
"slot": ("US_MOMENTUM_SLOT_MONEY",),
|
||||
"per_stock_cap": ("US_MOMENTUM_MAX_BUY_AMOUNT",),
|
||||
"min_invest_env": ("US_MOMENTUM_MIN_INVEST_RATIO_OF_SLOT", "MIN_INVEST_RATIO_OF_SLOT"),
|
||||
},
|
||||
"BREAKOUT": {
|
||||
"max_stocks": ("BREAKOUT_MAX_STOCKS", "MAX_STOCKS"),
|
||||
"total_budget": ("BREAKOUT_TOTAL_BUDGET_KRW", "SHORT_TOTAL_BUDGET_KRW"),
|
||||
@@ -323,7 +353,18 @@ def merge_param_search_apply_source(
|
||||
|
||||
|
||||
def session_env_patch(strategy: str, p: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""매매시간 HHMM → env (전략별 TIME_*). 꼬리(SHORT/TAIL)는 TAIL_TIME_* 만."""
|
||||
"""매매시간 HHMM → env (전략별 TIME_*). 꼬리(SHORT/TAIL)는 TAIL_TIME_* 만.
|
||||
|
||||
기본 OFF — 파라서치/Optuna apply 가 TIME_* 를 덮어쓰지 않음
|
||||
(짧은 창 과적합 → 실매 오후 진입 잠금 방지). 운영 시간은 DB·웹에서 수동.
|
||||
켜려면 PARAM_SEARCH_APPLY_SESSION_TIME=1.
|
||||
"""
|
||||
try:
|
||||
from kis_trader.utils.env import get_env_bool
|
||||
if not get_env_bool("PARAM_SEARCH_APPLY_SESSION_TIME", False):
|
||||
return {}
|
||||
except Exception:
|
||||
return {}
|
||||
patch: Dict[str, str] = {}
|
||||
ts = p.get("time_start_hm")
|
||||
te = p.get("time_end_hm")
|
||||
@@ -341,6 +382,8 @@ def session_env_patch(strategy: str, p: Dict[str, Any]) -> Dict[str, str]:
|
||||
patch["TIME_START"] = v
|
||||
elif strat == "MOMENTUM":
|
||||
patch["MOMENTUM_TIME_START"] = v
|
||||
elif strat == "US_MOMENTUM":
|
||||
patch["US_MOMENTUM_TIME_START"] = v
|
||||
elif strat == "BREAKOUT":
|
||||
patch["BREAKOUT_TIME_START"] = v
|
||||
elif strat == "RANGE_BREAK":
|
||||
@@ -356,6 +399,8 @@ def session_env_patch(strategy: str, p: Dict[str, Any]) -> Dict[str, str]:
|
||||
patch["TIME_END"] = v
|
||||
elif strat == "MOMENTUM":
|
||||
patch["MOMENTUM_TIME_END"] = v
|
||||
elif strat == "US_MOMENTUM":
|
||||
patch["US_MOMENTUM_TIME_END"] = v
|
||||
elif strat == "BREAKOUT":
|
||||
patch["BREAKOUT_TIME_END"] = v
|
||||
patch["BREAKOUT_GOLDEN_END_HM"] = v
|
||||
@@ -368,8 +413,9 @@ def session_env_patch(strategy: str, p: Dict[str, Any]) -> Dict[str, str]:
|
||||
|
||||
# 전략 전용 키 prefix — 쓰기 시 '남의 전략' 키 오염 방지용.
|
||||
# (읽기 fallback 튜플엔 타 전략 키가 폴백으로 들어있어, 그대로 쓰면 SCALP·SHORT 등이 덮인다)
|
||||
# US_MOMENTUM_ 은 MOMENTUM_ 보다 먼저 매칭되도록 앞에 둠.
|
||||
_KNOWN_STRATEGY_PREFIXES: Tuple[str, ...] = (
|
||||
"TAIL_", "SHORT_", "SCALP_", "MOMENTUM_",
|
||||
"TAIL_", "SHORT_", "SCALP_", "US_MOMENTUM_", "MOMENTUM_",
|
||||
"BREAKOUT_", "RANGE_BREAK_", "UPDOW_", "DBBAND_",
|
||||
)
|
||||
|
||||
@@ -613,6 +659,10 @@ def apply_daily_profit_halt_sim(
|
||||
고점대비 trail_drop_pct% 되돌림이 처음 발생한 시각 이후의 신규 진입을 차단한다.
|
||||
(보유분 청산은 그대로 — 실매 buy_allowed 와 동일: 신규매수만 중단.)
|
||||
|
||||
B(리스크버짓): ``_backtest_daily_profit_risk_budget`` ON 이면 hit 시점에
|
||||
미청산 포지션의 최악손절합 > cushion 인 종목을 **손절가 전량청산**으로 근사
|
||||
(실매 plan_risk_budget_trim 과 동일 선정 · 체결가는 stop 보수 가정).
|
||||
|
||||
게이트:
|
||||
- params['_backtest_daily_profit_trail'] 명시 시 그 값 (웹 손익감시 스위치).
|
||||
- 미지정이면 실매 ``{pfx}_DAILY_PROFIT_TARGET_ENABLED`` 추종.
|
||||
@@ -634,12 +684,29 @@ def apply_daily_profit_halt_sim(
|
||||
|
||||
from kis_trader.engine.daily_profit_halt import (
|
||||
_guard_active, _target_reached, _trail_reached,
|
||||
plan_risk_budget_trim, resolve_stop_price, position_stop_risk_krw,
|
||||
)
|
||||
cfg = _daily_halt_cfg(p)
|
||||
if not _guard_active(cfg):
|
||||
return trades
|
||||
mode = str(cfg.get("mode") or "fixed").lower()
|
||||
|
||||
rb_gate = p.get("_backtest_daily_profit_risk_budget")
|
||||
if rb_gate is None:
|
||||
sid = str(p.get("_daily_profit_strategy_id") or p.get("strategy_id") or "SHORT").strip().upper()
|
||||
if sid == "TAIL":
|
||||
sid = "SHORT"
|
||||
try:
|
||||
from kis_trader.engine.daily_profit_halt import load_strategy_profit_target
|
||||
rb_gate = bool(load_strategy_profit_target(sid).get("risk_budget_enabled"))
|
||||
except Exception:
|
||||
from kis_trader.utils.env import get_env_bool
|
||||
rb_gate = get_env_bool(f"{sid}_DAILY_PROFIT_RISK_BUDGET_ENABLED", False)
|
||||
rb_on = bool(rb_gate)
|
||||
sid_rb = str(p.get("_daily_profit_strategy_id") or p.get("strategy_id") or "SHORT").strip().upper()
|
||||
if sid_rb == "TAIL":
|
||||
sid_rb = "SHORT"
|
||||
|
||||
by_day: Dict[str, List[Dict]] = {}
|
||||
for t in trades:
|
||||
et = str(t.get("entry_time") or t.get("buy_time") or "")
|
||||
@@ -653,6 +720,8 @@ def apply_daily_profit_halt_sim(
|
||||
cum = 0.0
|
||||
peak = 0.0
|
||||
trigger_time: Optional[str] = None
|
||||
trail_hit = False
|
||||
fixed_hit = False
|
||||
for t in closes:
|
||||
cum += float(t.get("pnl") or 0)
|
||||
peak = max(peak, cum)
|
||||
@@ -662,9 +731,63 @@ def apply_daily_profit_halt_sim(
|
||||
if fixed_hit or trail_hit:
|
||||
trigger_time = str(t.get("exit_time") or t.get("sell_time") or "")
|
||||
break
|
||||
|
||||
# B: hit 시점 미청산 → 리스크버짓 전량(손절가) 근사
|
||||
force_codes: set = set()
|
||||
if rb_on and trigger_time:
|
||||
open_rows: List[Dict[str, Any]] = []
|
||||
for t in ts:
|
||||
et = str(t.get("entry_time") or t.get("buy_time") or "")
|
||||
xt = str(t.get("exit_time") or t.get("sell_time") or "")
|
||||
if et and et < trigger_time and (not xt or xt > trigger_time):
|
||||
entry = float(t.get("buy_price") or t.get("entry") or 0)
|
||||
qty = int(t.get("qty") or 1)
|
||||
stop = float(t.get("stop_price") or 0)
|
||||
stop = resolve_stop_price(
|
||||
entry=entry, stop_price=stop, strategy_id=sid_rb,
|
||||
)
|
||||
risk = position_stop_risk_krw(qty=qty, entry=entry, stop_price=stop)
|
||||
code = str(t.get("code") or t.get("ticker") or id(t))
|
||||
open_rows.append({
|
||||
"code": code,
|
||||
"_trade": t,
|
||||
"qty": qty,
|
||||
"buy_price": entry,
|
||||
"stop_price": stop,
|
||||
"risk_krw": risk,
|
||||
})
|
||||
plan = plan_risk_budget_trim(
|
||||
pnl_krw=cum,
|
||||
peak_krw=peak,
|
||||
cfg=cfg,
|
||||
budget_krw=budget_krw,
|
||||
positions=open_rows,
|
||||
trail_hit=trail_hit,
|
||||
fixed_hit=fixed_hit,
|
||||
)
|
||||
if plan.get("action") == "trim":
|
||||
for row in plan.get("closes") or []:
|
||||
tr = row.get("_trade")
|
||||
if not tr:
|
||||
continue
|
||||
code = str(row.get("code") or "")
|
||||
force_codes.add(code)
|
||||
stop = float(row.get("stop_price") or 0)
|
||||
entry = float(row.get("buy_price") or 0)
|
||||
qty = int(row.get("qty") or 1)
|
||||
# 보수: 손절가 청산으로 pnl 재계산 (수수료 미차감 — 기존 trade pnl 스케일 유지)
|
||||
if entry > 0 and stop > 0:
|
||||
tr["sell_price"] = stop
|
||||
tr["exit"] = stop
|
||||
tr["pnl"] = round((stop - entry) * qty)
|
||||
tr["exit_time"] = trigger_time
|
||||
tr["sell_time"] = trigger_time
|
||||
tr["sell_reason"] = "리스크버짓"
|
||||
tr["_risk_budget_force"] = True
|
||||
|
||||
for t in ts:
|
||||
et = str(t.get("entry_time") or t.get("buy_time") or "")
|
||||
if trigger_time and et >= trigger_time:
|
||||
if trigger_time and et >= trigger_time and not t.get("_risk_budget_force"):
|
||||
continue # 트리거 후 신규매수 차단
|
||||
kept.append(t)
|
||||
return kept
|
||||
@@ -676,14 +799,21 @@ def attach_scalp_trade_pnl(
|
||||
fee_rate: float,
|
||||
sell_tax: float,
|
||||
slip_pct: float = 0.0,
|
||||
fx_fee_rate: float = 0.0,
|
||||
pnl_decimals: int = 0,
|
||||
) -> None:
|
||||
"""스캘핑·모멘텀·돌파 공통 손익 (buy_time/sell_time 필드).
|
||||
|
||||
slip_pct: 백테 체결 슬리피지(편도 %). 시장가 즉시 체결 가정이라 매수는 불리(+)·
|
||||
매도는 불리(-) 로 진입·청산 양측 반영. 표시용 가격은 그대로 두고 pnl 에만 반영.
|
||||
0=OFF(동작 불변).
|
||||
fx_fee_rate: 환전 스프레드 **편도** 비율 (예: 0.0005=0.05%). 매수·매도 금액에 각각 적용.
|
||||
해외 US 등 — 0=OFF(국장 동작 불변).
|
||||
pnl_decimals: 손익 반올림 자리 (국장 기본 0=원 단위 int, 해외 USD는 4 권장).
|
||||
"""
|
||||
slip = max(0.0, float(slip_pct or 0.0)) / 100.0
|
||||
fx = max(0.0, float(fx_fee_rate or 0.0))
|
||||
nd = max(0, int(pnl_decimals or 0))
|
||||
for t in trades:
|
||||
if t.get("pnl") is not None and t.get("pnl") != 0:
|
||||
continue
|
||||
@@ -695,9 +825,19 @@ def attach_scalp_trade_pnl(
|
||||
xp = xp * (1.0 - slip) # 매도 체결 불리
|
||||
buy_amt = ep * qty
|
||||
sell_amt = xp * qty
|
||||
t["pnl"] = round(
|
||||
sell_amt - buy_amt - buy_amt * fee_rate - sell_amt * fee_rate - sell_amt * sell_tax
|
||||
# 매매수수료(편도×2) + 매도세 + 환전(편도×매수·매도)
|
||||
pnl = (
|
||||
sell_amt - buy_amt
|
||||
- buy_amt * float(fee_rate)
|
||||
- sell_amt * float(fee_rate)
|
||||
- sell_amt * float(sell_tax)
|
||||
- (buy_amt + sell_amt) * fx
|
||||
)
|
||||
t["pnl"] = round(pnl, nd) if nd > 0 else round(pnl)
|
||||
t["fee_rate"] = float(fee_rate)
|
||||
t["sell_tax"] = float(sell_tax)
|
||||
if fx > 0:
|
||||
t["fx_fee_rate"] = fx
|
||||
bt = t.get("buy_time") or t.get("entry_time")
|
||||
st = t.get("sell_time") or t.get("exit_time")
|
||||
if bt and st:
|
||||
@@ -763,6 +903,8 @@ def resolve_trigger_snapshots_for_backtest(
|
||||
start_key = str(meta_out.get("start_key") or "")
|
||||
end_key = str(meta_out.get("end_key") or "")
|
||||
db = meta_out.get("db")
|
||||
if db is None and start_key and end_key:
|
||||
db = ensure_meta_db(meta_out)
|
||||
if db and start_key and end_key:
|
||||
ob_loaded, pg_loaded, snap_meta = load_trigger_snapshots_by_code(
|
||||
db, start_key, end_key, set(candles_by_code.keys()),
|
||||
|
||||
Reference in New Issue
Block a user