브랜치 분리 방식: A / B / C
A 선택 시 커밋 메시지: 위 초안 OK / 수정 / 직접 작성 작업 시점: 지금 / 운영 데이터 1~2일 쌓고 / 주말
This commit is contained in:
@@ -812,9 +812,9 @@ class ScalpingBotV2:
|
||||
self.atr_up_mult = get_env_float("SCALP_ATR_UP_MULT", 1.5)
|
||||
self.atr_down_mult = get_env_float("SCALP_ATR_DOWN_MULT", 0.8)
|
||||
|
||||
# 피뢰침/급등 필터
|
||||
self.high_chase_thr = get_env_float("HIGH_PRICE_CHASE_THRESHOLD", 0.96)
|
||||
self.max_daily_chg = get_env_float("MAX_DAILY_CHANGE_PCT", 20.0)
|
||||
# 피뢰침/급등 필터 (스캘핑 전용 키 우선, 없으면 꼬리잡기 공용 키 fallback)
|
||||
self.high_chase_thr = get_env_float("SCALP_HIGH_PRICE_CHASE_THRESHOLD", 0) or get_env_float("HIGH_PRICE_CHASE_THRESHOLD", 0.96)
|
||||
self.max_daily_chg = get_env_float("SCALP_MAX_DAILY_CHANGE_PCT", 0) or get_env_float("MAX_DAILY_CHANGE_PCT", 20.0)
|
||||
|
||||
# 거래량 배율 필터
|
||||
self.vol_multiplier = get_env_float("VOLUME_AVG_MULTIPLIER", 1.5)
|
||||
@@ -823,8 +823,8 @@ class ScalpingBotV2:
|
||||
self.use_ml_signal = get_env_bool("USE_ML_SIGNAL", False)
|
||||
self.ml_min_prob = get_env_float("ML_MIN_PROBABILITY", 0.55)
|
||||
|
||||
# 최소 가격
|
||||
self.min_price = get_env_float("MIN_PRICE_TAIL", 1000.0)
|
||||
# 최소 가격 (스캘핑 전용 키 우선)
|
||||
self.min_price = get_env_float("SCALP_MIN_PRICE", 0) or get_env_float("MIN_PRICE_TAIL", 1000.0)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 장 상태 체크
|
||||
@@ -968,17 +968,19 @@ class ScalpingBotV2:
|
||||
state = {"last_exit_dt": last_exit_dt, "daily_cnt": daily_cnt}
|
||||
|
||||
# 루프당 1회 설정된 params 사용 (후보마다 DB 열지 않음)
|
||||
# RSI/되돌림 등은 매번 DB 최신값 반영 (재시작 없이 env_config 변경 적용)
|
||||
params = getattr(self, "_scan_engine_params", None)
|
||||
if params is None:
|
||||
_d = se.get_scalping_defaults_from_db()
|
||||
params = {
|
||||
**_d,
|
||||
"rsi_oversold": self.rsi_oversold,
|
||||
"rsi_overbought": self.rsi_overbought,
|
||||
"rsi_oversold": get_env_float("SCALP_RSI_OVERSOLD", 25.0),
|
||||
"rsi_overbought": get_env_float("SCALP_RSI_OVERBOUGHT", 75.0),
|
||||
"sl_pct": abs(self.scalp_stop_loss_pct),
|
||||
"tp_pct": self.scalp_take_profit_pct,
|
||||
"drop_rate": self.scalp_min_drop_rate,
|
||||
"vol_mult": self.vol_multiplier if self.vol_multiplier > 0 else 0,
|
||||
"require_reversal_candle": get_env_bool("SCALP_REQUIRE_REVERSAL_CANDLE", True),
|
||||
}
|
||||
|
||||
reject_reason, reject_msg, sig = se.check_buy_signal_live(candles, params, state)
|
||||
@@ -1136,7 +1138,7 @@ class ScalpingBotV2:
|
||||
# MAX_LOSS_PER_TRADE_KRW(원) ÷ |SCALP_STOP_LOSS_PCT| = 최대 투자 가능 금액
|
||||
# 스캘핑 손절 1.5% 기준 예: 손실한도 20만원 → 투자상한 = 200,000 / 0.015 = 13,333,333원
|
||||
# SLOT_MONEY_DEFAULT 가 더 낮으면 그쪽 사용 (포지션 과집중 방지)
|
||||
max_loss_krw = get_env_int("MAX_LOSS_PER_TRADE_KRW", 200000)
|
||||
max_loss_krw = get_env_int("SCALP_MAX_LOSS_PER_TRADE_KRW", 0) or get_env_int("MAX_LOSS_PER_TRADE_KRW", 200000)
|
||||
# 스캘핑 전용 손절 비율 (SCALP_STOP_LOSS_PCT, 기본 1.5%)
|
||||
scalp_sl_pct = abs(self.scalp_stop_loss_pct) # 양수로 처리
|
||||
if max_loss_krw > 0 and scalp_sl_pct > 0:
|
||||
@@ -1196,7 +1198,7 @@ class ScalpingBotV2:
|
||||
self.ws_cache.subscribe(code)
|
||||
|
||||
rsi_val = signal["entry_features"].get("rsi", 0)
|
||||
msg = (f"🛒 **[스캘핑 매수]** {name}({code})\n"
|
||||
msg = (f"🔷 **[스캘핑 매수]** {name}({code})\n"
|
||||
f"매수가: {price:,.0f}원 × {qty}주 = {price*qty:,.0f}원\n"
|
||||
f"손절: {stop_price:,.0f}원(-{abs(self.scalp_stop_loss_pct)*100:.1f}%) | "
|
||||
f"익절: {target_price:,.0f}원(+{self.scalp_take_profit_pct*100:.1f}%)\n"
|
||||
@@ -1286,9 +1288,13 @@ class ScalpingBotV2:
|
||||
if current_price <= stop_price:
|
||||
reason = f"손절 ({profit_pct*100:.2f}%)"
|
||||
|
||||
# [2] 금액 손실컷 (원화 기준) — 고가 종목에서 % 손절 이전에 원화 손실이 터지는 경우 대비
|
||||
# 예) 10만원짜리 20주 보유, 손절 -3% → 20만원 손실 → 설정값 초과 시 먼저 컷
|
||||
elif profit_val <= -get_env_int("MAX_LOSS_PER_TRADE_KRW", 200000):
|
||||
# [2] 금액 손실컷 — 손실 한도 초과 **이면서** 하락률이 최소값 이상일 때만 발동 (슬리피지/흔들림 방지)
|
||||
_max_loss_krw = get_env_int("SCALP_MAX_LOSS_PER_TRADE_KRW", 0) or get_env_int("MAX_LOSS_PER_TRADE_KRW", 200000)
|
||||
_min_drop = get_env_float("SCALP_MIN_DROP_PCT_FOR_LOSS_CUT", 0) or 0.015
|
||||
if _min_drop >= 1:
|
||||
_min_drop = _min_drop / 100.0
|
||||
drop_pct = (buy_price - current_price) / buy_price if buy_price > 0 else 0
|
||||
if profit_val <= -_max_loss_krw and drop_pct >= _min_drop:
|
||||
reason = f"금액손실컷 ({profit_val:,.0f}원)"
|
||||
|
||||
# [3] 익절 (% 기준)
|
||||
@@ -1375,9 +1381,9 @@ class ScalpingBotV2:
|
||||
elif "잔고" in msg1 or "보유" in msg1 or "APBK3020" in msg_cd:
|
||||
logger.warning("⚠️ [유령잔고 정리] %s %s: 브로커 잔고 없음 → 로컬 기록 강제 삭제",
|
||||
name, code)
|
||||
self.db.close_trade(code=code, sell_price=0,
|
||||
sell_reason="잔고없음(강제정리)",
|
||||
strategy="SCALP_RSI_REVERSAL")
|
||||
# 0원 강제정리를 trade_history에 남기면 손익 통계가 왜곡됨.
|
||||
# 브로커에 실제 포지션이 없는 동기화 이슈 케이스는 active_trades만 정리.
|
||||
self.db.delete_active_trade(code=code, strategy="SCALP_RSI_REVERSAL")
|
||||
self.holdings.pop(code, None)
|
||||
return
|
||||
|
||||
@@ -1477,16 +1483,17 @@ class ScalpingBotV2:
|
||||
# ── 매수 체크 ─────────────────────────────────────────
|
||||
active_cnt = len(self.holdings)
|
||||
if candidates and active_cnt < self.max_stocks:
|
||||
# 루프당 1회만 DB에서 기본값 로드 (후보마다 TradeDB 생성 방지 → 로그 스팸 제거)
|
||||
# 루프당 1회만 DB에서 기본값 로드 (RSI/되돌림은 매 루프 최신값 사용 → 재시작 없이 env 반영)
|
||||
_d = se.get_scalping_defaults_from_db()
|
||||
self._scan_engine_params = {
|
||||
**_d,
|
||||
"rsi_oversold": self.rsi_oversold,
|
||||
"rsi_overbought": self.rsi_overbought,
|
||||
"rsi_oversold": get_env_float("SCALP_RSI_OVERSOLD", 25.0),
|
||||
"rsi_overbought": get_env_float("SCALP_RSI_OVERBOUGHT", 75.0),
|
||||
"sl_pct": abs(self.scalp_stop_loss_pct),
|
||||
"tp_pct": self.scalp_take_profit_pct,
|
||||
"drop_rate": self.scalp_min_drop_rate,
|
||||
"vol_mult": self.vol_multiplier if self.vol_multiplier > 0 else 0,
|
||||
"require_reversal_candle": get_env_bool("SCALP_REQUIRE_REVERSAL_CANDLE", True),
|
||||
}
|
||||
logger.info("🔍 [매수체크] 후보 %d개 순회 (보유 %d/%d)",
|
||||
len(candidates), active_cnt, self.max_stocks)
|
||||
|
||||
Reference in New Issue
Block a user