ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -81,12 +81,15 @@ class ScalpingStrategy(BaseStrategy):
"require_reversal_candle": get_env_bool(
"SCALP_REQUIRE_REVERSAL_CANDLE", True,
),
# True: 백테스트와 동일 — 신호봉(직전 확정봉) 조건 충족 시 현재봉에서 진입
"live_backtest_align": get_env_bool(
"SCALP_LIVE_BACKTEST_ALIGN", True,
# True: 백테스트와 동일 — 신호봉(T1) 충족 시 진입봉(T) 시가/첫 틱
# DB 빈문자("") 는 get_env_bool 이 False 로 읽히므로 defaults 우선
"live_backtest_align": bool(_d.get("live_backtest_align", True)),
"live_signal_lookback_bars": int(
_d.get("live_signal_lookback_bars", 1) or 1,
),
"live_signal_lookback_bars": get_env_int(
"SCALP_LIVE_SIGNAL_LOOKBACK_BARS", 1,
# 형성 중 봉(T)을 진입봉으로 — 확정봉만 쓰면 1봉 지연 (모멘텀과 동일)
"live_align_use_forming_bar": bool(
_d.get("live_align_use_forming_bar", True),
),
# 일일 진입 횟수 (reversal 기본 3회)
"max_daily": get_env_int(
@@ -127,12 +130,47 @@ class ScalpingStrategy(BaseStrategy):
if get_env_bool("FORCE_BUY_TEST", False):
return self._force_buy_test(code, name)
candles_raw = self.ws.get_candles(code, self.candle_tf, n=50)
if len(candles_raw) < 5:
candles_raw = list(self.ws.get_candles(code, self.candle_tf, n=50) or [])
# ALIGN: 형성 중 봉(T)을 진입봉으로 붙여 BT(portfolio next open/첫 틱)와 동일 시점
params = self._scan_engine_params or {}
use_forming = bool(params.get("live_align_use_forming_bar", True))
if params.get("live_backtest_align", True) and use_forming:
try:
self.ws.fill_gap([code], force=True)
cur = self.ws.get_current_candle(code, self.candle_tf)
except Exception:
cur = None
if cur and float(cur.get("open", 0) or 0) > 0:
ct = str(cur.get("candle_time") or "")[:12]
last_ct = ""
if candles_raw:
last_ct = str(candles_raw[-1].get("candle_time") or "")[:12]
if ct and ct != last_ct:
cur_d = dict(cur)
cur_d["is_confirmed"] = 0
candles_raw.append(cur_d)
if len(candles_raw) < 5:
force_sec = max(30, get_env_int("SCALP_CANDLE_GAP_FORCE_SEC", 120))
now_g = time.time()
if not hasattr(self, "_candle_gap_force_ts"):
self._candle_gap_force_ts = {}
last_g = float(self._candle_gap_force_ts.get(code, 0) or 0)
do_force = (now_g - last_g) >= float(force_sec)
if do_force:
self._candle_gap_force_ts[code] = now_g
try:
self.ws.fill_gap([code], force=do_force)
except Exception:
pass
log_sec = max(15, get_env_int("SCALP_CANDLE_SHORT_LOG_SEC", 60))
if not hasattr(self, "_candle_short_log_ts"):
self._candle_short_log_ts = {}
last_l = float(self._candle_short_log_ts.get(code, 0) or 0)
if now_g - last_l >= float(log_sec):
self._candle_short_log_ts[code] = now_g
self.logger.info(
"🔍 [캔들부족] %s(%s) need>=5 have=%d",
name, code, len(candles_raw),
)
return None
candles = [self._norm_candle(c) for c in candles_raw]
@@ -156,7 +194,6 @@ class ScalpingStrategy(BaseStrategy):
daily_cnt = 0
state = {"last_exit_dt": last_exit_dt, "daily_cnt": daily_cnt}
params = self._scan_engine_params or {}
# SCALP 는 reversal 단일 모드 (momentum 은 MomentumStrategy 로 분리됨)
reject, msg, sig = se.check_buy_signal_live(candles, params, state)
if reject:
@@ -165,19 +202,39 @@ class ScalpingStrategy(BaseStrategy):
if not sig:
return None
latest = candles[-1]
curr_price = float(latest["close"])
if curr_price < self.min_price:
# 중분 편입 → 같은 진입봉 시가 매수 보류 (다음 분부터)
_defer = self._defer_mid_enroll_entry(
code, sig.get("entry_bar_key"), int(self.candle_tf or 1), params,
)
if _defer:
self.logger.info("🔍 [%s] %s(%s)", _defer, name, code)
return None
# 현재가 보정 (WS → 없으면 REST)
wsd = self.ws.get_price(code)
if wsd:
try:
curr_price = abs(float(str(wsd.get("stck_prpr", curr_price)).replace(",", ""))) or curr_price
except Exception:
pass
if curr_price <= 0:
# 진입 계산가: align 시 T봉 첫 틱(RAM) → 없으면 시가. 폴백만 WS 현재가
align_on = bool(params.get("live_backtest_align", True))
entry_open = float(sig.get("entry_price", 0) or 0)
entry_src = "ohlc_open"
if align_on and entry_open > 0:
from kis_trader.engine.tail_tick_replay import live_align_entry_price
curr_price, entry_src = live_align_entry_price(
self.ws,
code,
entry_open,
entry_bar_key=str(sig.get("entry_bar_key") or "")[:12],
tf_min=int(self.candle_tf or 1),
)
else:
latest = candles[-1]
curr_price = float(latest["close"])
wsd = self.ws.get_price(code)
if wsd:
try:
curr_price = abs(
float(str(wsd.get("stck_prpr", curr_price)).replace(",", ""))
) or curr_price
except Exception:
pass
if curr_price <= 0 or curr_price < self.min_price:
return None
hard_cap = get_env_int("SCALP_MAX_BUY_AMOUNT", 0) \
@@ -199,8 +256,8 @@ class ScalpingStrategy(BaseStrategy):
target_price = curr_price * (1 + eff_tp)
self.logger.info(
"🎯 [SCALP-REVERSAL 시그널] %s(%s) price=%.0f qty=%d RSI=%.1f",
name, code, curr_price, qty, sig.get("rsi", 0),
"🎯 [SCALP-REVERSAL 시그널] %s(%s) price=%.0f qty=%d RSI=%.1f entry_src=%s",
name, code, curr_price, qty, sig.get("rsi", 0), entry_src,
)
return {
"code": code,