브랜치 분리 방식: A / B / C
A 선택 시 커밋 메시지: 위 초안 OK / 수정 / 직접 작성 작업 시점: 지금 / 운영 데이터 1~2일 쌓고 / 주말
This commit is contained in:
214
database.py
214
database.py
@@ -201,6 +201,8 @@ ENV_CONFIG_KEYS = (
|
||||
"USE_RANDOM_SPLIT", "FORCE_MARKET_OPEN", "FORCE_BUY_TEST", "TOTAL_DEPOSIT",
|
||||
# POP/LOCK·금액 손절 관련 추가 키
|
||||
"ROUND_TRIP_COST_PCT", "POP_NET_PCT", "LOCK_NET_PCT", "MAX_LOSS_PER_TRADE_KRW",
|
||||
# 금액손실컷 발동 최소 하락률(%): 이 하락률 미만이면 슬리피지/흔들림으로 간주하고 금액손실컷 미발동 (꼬리잡기)
|
||||
"MIN_DROP_PCT_FOR_LOSS_CUT",
|
||||
# 한투 API 관련 키 추가 (실전/모의 계좌 분리)
|
||||
"KIS_APP_KEY_REAL", "KIS_APP_SECRET_REAL",
|
||||
"KIS_APP_KEY_MOCK", "KIS_APP_SECRET_MOCK",
|
||||
@@ -271,6 +273,7 @@ ENV_CONFIG_KEYS = (
|
||||
"SCALP_GAP_RETRY_SEC",
|
||||
# 스캘핑 전용 재진입 쿨다운(초): 매도 후 같은 종목 N초 동안 재매수 차단 (기본 600=10분)
|
||||
# 백테스트와 동일 파라미터로 맞추려면 DB에 값 저장 후 봇/백테스트 모두 이 값 사용
|
||||
"SCALP_REQUIRE_REVERSAL_CANDLE", # true=직전 음봉+현재 양봉 필수, false=RSI 과매도만으로 진입 허용
|
||||
"SCALP_COOLDOWN_SEC",
|
||||
# 트레일링 발동 최소 수익률(%): 고점이 매수가 대비 이 이상 올라야 트레일링 활성화
|
||||
# 0.5%면 수수료(~0.21%) 뺀 나머지만 이익 → 1.5 이상 권장
|
||||
@@ -278,6 +281,13 @@ ENV_CONFIG_KEYS = (
|
||||
# 트레일링/본절사수 후 최소 순이익(%): 수수료+세금 위에 이 값만큼 추가 마진 확보
|
||||
# 0이면 본절(수수료 이후 0원), 0.2면 최소 +0.2% 순이익 보장
|
||||
"SCALP_MIN_PROFIT_PCT",
|
||||
# 스캘핑 전용 방어로직 (꼬리잡기와 값 분리: HIGH_PRICE_CHASE_THRESHOLD / MIN_PRICE_TAIL 등과 별도)
|
||||
"SCALP_HIGH_PRICE_CHASE_THRESHOLD", # 고점추격 방지: 당일고가 대비 비율 이하면 진입 허용 (0.96 등)
|
||||
"SCALP_MAX_DAILY_CHANGE_PCT", # 급등주 필터: 당일 등락률 이 값 초과 시 진입 금지 (%)
|
||||
"SCALP_MIN_PRICE", # 최소 가격(원): 이 값 미만 종목 진입 금지
|
||||
"SCALP_MAX_LOSS_PER_TRADE_KRW", # 1회 최대 손실(원): 종목당 손실 상한
|
||||
"SCALP_MIN_DROP_PCT_FOR_LOSS_CUT", # 금액손실컷 발동 최소 하락률(%): 이 미만은 흔들림으로 미발동
|
||||
"SCALP_USE_DEFENSE_FILTERS", # true/false: 스캘핑 진입 방어 필터(낙폭/고점추격/급등/최소가격) ON/OFF
|
||||
# ── 단타봇(kis_short_ver2) 전용 키 ───────────────────────────────────
|
||||
# 켈리 공식 사용 여부 (true=켈리 적용, false=고정 비중)
|
||||
"USE_KELLY_FORMULA",
|
||||
@@ -338,6 +348,75 @@ ENV_CONFIG_KEYS = (
|
||||
# USE_THEME_HEAT_FILTER : true=활성 (기본 false)
|
||||
# THEME_HEAT_RSI_MAX : 테마 평균 RSI 이 값 초과면 진입 차단 (기본 72)
|
||||
"USE_THEME_HEAT_FILTER", "THEME_HEAT_RSI_MAX",
|
||||
# ── kis_trader 통합봇 전용 키 (전략 스위치 / 유니버스 / 랭킹) ────────
|
||||
# 전략 on/off
|
||||
"STRATEGY_SCALP_ENABLED", "STRATEGY_SHORT_ENABLED", "STRATEGY_BREAKOUT_ENABLED",
|
||||
# 중복매매 정책: block(기본)=같은 종목 동시 양봇 차단 / allow=허용
|
||||
"STRATEGY_SAME_CODE_POLICY",
|
||||
# 유니버스 소스: ranking(기본) | condition
|
||||
"SCALP_UNIVERSE_SOURCE", "SHORT_UNIVERSE_SOURCE", "BREAKOUT_UNIVERSE_SOURCE",
|
||||
# 랭킹 정렬/크기 — sort: volume|trading_value|strength|fluct_up|fluct_down
|
||||
"RANK_SCALP_SORT", "RANK_SCALP_LIMIT",
|
||||
"RANK_SHORT_SORT", "RANK_SHORT_LIMIT",
|
||||
"RANK_BREAKOUT_SORT", "RANK_BREAKOUT_LIMIT",
|
||||
# 랭킹 폴링 주기(초) — 기본 10초 (모의 TPS 5건/s 대비 안전, 시간당 720회)
|
||||
"RANKING_POLL_INTERVAL_SEC",
|
||||
# 조건검색 파라미터
|
||||
"KIS_HTS_ID", "CONDITION_POLL_INTERVAL_SEC",
|
||||
"CONDITION_SCALP_NAME", "CONDITION_SCALP_SEQ",
|
||||
"CONDITION_SHORT_NAME", "CONDITION_SHORT_SEQ",
|
||||
"CONDITION_BREAKOUT_NAME", "CONDITION_BREAKOUT_SEQ",
|
||||
# 유니버스 히스토리(백테스트용) — 기본 true, 배치 INSERT 로 DB 부담 최소화
|
||||
"UNIVERSE_HISTORY_SAVE",
|
||||
# WS 갭보정 파라미터 (KIS_FALLBACK 은 기본 false — 모의서버 500 폭탄 회피)
|
||||
"WS_GAP_FILL_OFF_HOURS", "WS_GAP_FILL_LIMIT", "WS_TIMEFRAMES",
|
||||
"WS_GAP_FILL_KIS_FALLBACK",
|
||||
# 종목당 최대 매수금액 하드캡 (손절%가 작으면 포지션 사이즈 폭주 방지)
|
||||
"MAX_BUY_AMOUNT_PER_STOCK",
|
||||
"SCALP_MAX_BUY_AMOUNT", "SHORT_MAX_BUY_AMOUNT", "TAIL_MAX_BUY_AMOUNT",
|
||||
# KIS REST 안정화 파라미터
|
||||
"KIS_MIN_INTERVAL_SEC", "KIS_REST_MAX_RETRIES",
|
||||
"KIS_REST_BACKOFF_CAP_SEC", "KIS_REST_TIMEOUT_SEC",
|
||||
# ── 시장 급락 서킷브레이커 (KOSPI/KOSDAQ 지수 폭락 시 신규 매수 차단) ──
|
||||
# 거래소 공식 서킷브레이커는 -8% 이지만, 봇 보호용으로 더 빨리(-2%) 반응.
|
||||
# PANIC 모드 시 매수만 차단, 매도는 평소처럼 동작 (포지션 정리 가능).
|
||||
"MARKET_GUARD_ENABLED", # 활성화 토글 (true/false). 백테스트 못하므로 운영 후 켜기
|
||||
"MARKET_GUARD_5MIN_DROP_PCT", # 5분 내 -N% 하락 시 PANIC 진입 (기본 2.0)
|
||||
"MARKET_GUARD_DAILY_DROP_PCT", # 일중 누적 -N% 하락 시 PANIC 진입 (기본 3.0)
|
||||
"MARKET_GUARD_RECOVERY_PCT", # PANIC 중 5분 +N% 반등 시 자동 해제 (기본 1.0)
|
||||
"MARKET_GUARD_INDEX_CODE", # 감시 지수 ("0001"=KOSPI, "1001"=KOSDAQ, "both"=둘 다)
|
||||
"MARKET_GUARD_POLL_SEC", # 지수 폴링 주기 초 (기본 30)
|
||||
# ── 전략별 후보 하드캡 (WS 구독 41 한도 보호 + 매수 체크 회전율 보장) ─
|
||||
# cond/ranking 매니저가 폭주해도 본 전략은 상위 N개만 처리. 0=무제한.
|
||||
# 권장: 세 전략 합산 ≤ 30 (영구구독 + 보유 여유 11종목 확보).
|
||||
"SCALP_CAND_LIMIT", "SHORT_CAND_LIMIT", "BREAKOUT_CAND_LIMIT",
|
||||
# ── 전략별 매매 시간대 (HHMM 정수, 930=09:30, 1500=15:00) ──────────
|
||||
# 미설정 시 글로벌 TIME_START/TIME_END → 기본 900~1530.
|
||||
# 예: SCALP는 변동성 큰 9시 직후 회피하고 09:30~14:30만 → SCALP_TIME_START=930, SCALP_TIME_END=1430
|
||||
"SCALP_TIME_START", "SCALP_TIME_END",
|
||||
"SHORT_TIME_START", "SHORT_TIME_END",
|
||||
"BREAKOUT_TIME_START", "BREAKOUT_TIME_END",
|
||||
# ETN/ETF/스팩/우선주 자동 제외 토글 (기본 true) — 후보 매니저 결과를 한 번 더 거른다.
|
||||
"EXCLUDE_NON_STOCK",
|
||||
# ── SCALP 진입 모드 토글 (reversal | momentum) ───────────────────
|
||||
# reversal : 기존 RSI 과매도 V자 반전(원본 로직).
|
||||
# momentum : 키움 'scalp' 조건검색(갭상승+신고가 돌파+거래량 폭증)으로
|
||||
# 걸러진 종목군을 따라붙기. 봇은 모멘텀 살아있음/끝물 아님/
|
||||
# 양봉 마무리/거래량 평균 초과만 검증.
|
||||
"SCALP_MODE",
|
||||
# SCALP momentum 모드 전용 파라미터
|
||||
# mom_rsi_min ≤ RSI3 ≤ mom_rsi_max 만 진입 (모멘텀 살아있고 과열은 아님).
|
||||
"SCALP_MOM_RSI_MIN", "SCALP_MOM_RSI_MAX",
|
||||
# 직전 N봉 평균 거래량 × 배수 이상이어야 진입.
|
||||
"SCALP_MOM_VOL_MULT", "SCALP_MOM_VOL_WIN",
|
||||
# momentum 모드 진입 마감 시각(HHMM, 기본 1430). 종가 후폭풍 회피용.
|
||||
"SCALP_MOM_TIME_END_HM",
|
||||
# SCALP 일일 진입 횟수 (momentum은 회전 빠르므로 5 정도 권장)
|
||||
"SCALP_MAX_DAILY",
|
||||
# ── 전략별 동시 보유 종목 한도 (기존 글로벌 MAX_STOCKS 분리) ─────────
|
||||
# 미설정(0/빈값) 시 글로벌 MAX_STOCKS 로 폴백 → 구버전 호환.
|
||||
# 권장: 합계 ≤ MAX_STOCKS (계좌 슬롯 분산), 예: 3+2+2=7.
|
||||
"SCALP_MAX_STOCKS", "SHORT_MAX_STOCKS", "BREAKOUT_MAX_STOCKS",
|
||||
)
|
||||
|
||||
|
||||
@@ -345,7 +424,14 @@ class TradeDB:
|
||||
"""
|
||||
트레이딩 봇용 MariaDB 데이터베이스 관리 클래스.
|
||||
기존 SQLite 인터페이스와 100% 호환 (db_path 인수는 무시됨).
|
||||
|
||||
※ 호출자가 매번 ``TradeDB()`` → ``db.close()`` 패턴으로 쓰므로,
|
||||
DDL(`CREATE TABLE IF NOT EXISTS`) 과 INFO 로그는 **프로세스당 1회만** 실행한다.
|
||||
(내부 클래스 변수 ``_tables_created`` 가드)
|
||||
"""
|
||||
_tables_created = False # 프로세스 내 테이블 생성 1회만
|
||||
_tables_lock = threading.Lock() # 멀티스레드 race 방지
|
||||
|
||||
def __init__(self, db_path="quant_bot.db"):
|
||||
"""
|
||||
Args:
|
||||
@@ -353,8 +439,21 @@ class TradeDB:
|
||||
"""
|
||||
self.db_path = db_path # 호환용 보존
|
||||
self.conn = _MariaDBConn()
|
||||
self._create_tables()
|
||||
logger.info("✅ TradeDB 초기화 완료: MariaDB %s:%s/%s", _DB_HOST, _DB_PORT, _DB_NAME)
|
||||
# 테이블 생성은 첫 인스턴스에서만 — 매 호출마다 21개 DDL 폭탄 방지
|
||||
with TradeDB._tables_lock:
|
||||
if not TradeDB._tables_created:
|
||||
self._create_tables()
|
||||
logger.info(
|
||||
"✅ TradeDB 초기화 완료: MariaDB %s:%s/%s",
|
||||
_DB_HOST, _DB_PORT, _DB_NAME,
|
||||
)
|
||||
TradeDB._tables_created = True
|
||||
else:
|
||||
# 이후 인스턴스는 조용히 — 같은 정보 매초 찍어 로그 오염 방지
|
||||
logger.debug(
|
||||
"TradeDB 인스턴스 재생성 (테이블 스킵): %s:%s/%s",
|
||||
_DB_HOST, _DB_PORT, _DB_NAME,
|
||||
)
|
||||
|
||||
def _create_tables(self):
|
||||
"""DB 테이블 생성 (없을 경우)"""
|
||||
@@ -469,6 +568,24 @@ class TradeDB:
|
||||
) CHARACTER SET utf8mb4
|
||||
""")
|
||||
|
||||
# 6-2. 매수 후보군 이력 (5분마다 쌓아서 백테스트 시 '실제 그 시각 유니버스' 사용 가능)
|
||||
self.conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS target_candidates_history (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_key VARCHAR(12) NOT NULL,
|
||||
scan_time VARCHAR(30) NOT NULL,
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
score DOUBLE NOT NULL DEFAULT 0,
|
||||
price DOUBLE NOT NULL DEFAULT 0,
|
||||
market CHAR(1) DEFAULT 'Q',
|
||||
sector VARCHAR(100),
|
||||
theme VARCHAR(100),
|
||||
INDEX idx_slot (slot_key),
|
||||
INDEX idx_scan (scan_time)
|
||||
) CHARACTER SET utf8mb4
|
||||
""")
|
||||
|
||||
# 7. 종목 메타데이터 (테마·섹터·시장구분) — 스캐너가 채움, 조인 분석 용
|
||||
# market: 'K'=KOSPI, 'Q'=KOSDAQ, 'E'=ETF/기타
|
||||
# sector: 업종명 (KIS bstp_kor_isnm, 예: '반도체')
|
||||
@@ -624,6 +741,27 @@ class TradeDB:
|
||||
logger.info(f"📌 target_candidates.{col} 컬럼 추가")
|
||||
except Exception as e:
|
||||
logger.debug(f"migrate target_candidates theme cols: {e}")
|
||||
# ── target_candidates_history (후보 이력, 백테스트용) ──────────────────
|
||||
try:
|
||||
self.conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS target_candidates_history (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_key VARCHAR(12) NOT NULL,
|
||||
scan_time VARCHAR(30) NOT NULL,
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
score DOUBLE NOT NULL DEFAULT 0,
|
||||
price DOUBLE NOT NULL DEFAULT 0,
|
||||
market CHAR(1) DEFAULT 'Q',
|
||||
sector VARCHAR(100),
|
||||
theme VARCHAR(100),
|
||||
INDEX idx_slot (slot_key),
|
||||
INDEX idx_scan (scan_time)
|
||||
) CHARACTER SET utf8mb4
|
||||
""")
|
||||
logger.info("📌 target_candidates_history 테이블 확인/생성")
|
||||
except Exception as e:
|
||||
logger.warning(f"migrate target_candidates_history 실패(이력 미적재 가능): {e}")
|
||||
|
||||
def _migrate_env_config_to_columns(self):
|
||||
"""env_config가 예전 JSON 컬럼(snapshot_json)이면 컬럼 스키마로 이전"""
|
||||
@@ -1227,6 +1365,31 @@ class TradeDB:
|
||||
"""
|
||||
try:
|
||||
scan_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# 5분 슬롯 키 (백테스트 유니버스 이력 조회용): 2026-03-18 15:55:21 → 202603181555
|
||||
_now = datetime.datetime.now()
|
||||
slot_min = (_now.hour * 60 + _now.minute) // 5 * 5
|
||||
slot_key = _now.strftime("%Y%m%d") + "%02d%02d" % (slot_min // 60, slot_min % 60)
|
||||
|
||||
# 이력 테이블 없으면 생성 시도 (마이그레이션 누락/실패 시 자동 보정)
|
||||
try:
|
||||
self.conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS target_candidates_history (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_key VARCHAR(12) NOT NULL,
|
||||
scan_time VARCHAR(30) NOT NULL,
|
||||
code VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
score DOUBLE NOT NULL DEFAULT 0,
|
||||
price DOUBLE NOT NULL DEFAULT 0,
|
||||
market CHAR(1) DEFAULT 'Q',
|
||||
sector VARCHAR(100),
|
||||
theme VARCHAR(100),
|
||||
INDEX idx_slot (slot_key),
|
||||
INDEX idx_scan (scan_time)
|
||||
) CHARACTER SET utf8mb4
|
||||
""")
|
||||
except Exception as e:
|
||||
logger.debug("target_candidates_history CREATE 확인: %s", e)
|
||||
|
||||
# stock_meta 캐시: 테마/섹터가 없는 항목을 DB에서 보완
|
||||
meta_cache: Dict[str, Dict] = {}
|
||||
@@ -1271,8 +1434,14 @@ class TradeDB:
|
||||
sector,
|
||||
theme,
|
||||
))
|
||||
# [DEPRECATED] 구봇(kiwoom_universe_scanner 등) 의 5분 슬롯 이력 적재는 제거됨.
|
||||
# 신봇(kis_trader/**) 은 RankingManager/ConditionSearchManager 가
|
||||
# ``insert_condition_universe_snapshot()`` 으로 초단위 event_time 스냅샷을
|
||||
# 기록한다. 구봇이 우발적으로 실행되더라도 이력 혼선을 일으키지 않도록
|
||||
# 이 경로에서의 history 적재는 의도적으로 하지 않는다.
|
||||
# (과거 코드: INSERT INTO target_candidates_history ...)
|
||||
|
||||
logger.info(f"✅ 매수 후보군 DB 저장: {len(candidates)}개")
|
||||
logger.info(f"✅ 매수 후보군 DB 저장: {len(candidates)}개 (slot_key={slot_key}, 이력 적재 안함)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -1383,6 +1552,41 @@ class TradeDB:
|
||||
logger.error(f"❌ 후보군 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
def get_universe_history_for_backtest(
|
||||
self, start_ymd: str, end_ymd: str
|
||||
) -> Dict[str, List[str]]:
|
||||
"""
|
||||
target_candidates_history에서 기간 내 5분별 유니버스 조회.
|
||||
백테스트 시 '실제 그 시각에 봇이 보던 후보'로 검사할 때 사용.
|
||||
|
||||
Args:
|
||||
start_ymd: 시작일 YYYYMMDD
|
||||
end_ymd: 종료일 YYYYMMDD
|
||||
|
||||
Returns:
|
||||
slot_key(YYYYMMDDHHMM) → 해당 슬롯 후보 코드 리스트 (점수 순 유지)
|
||||
"""
|
||||
try:
|
||||
start_key = start_ymd + "0000"
|
||||
end_key = end_ymd + "2359"
|
||||
rows = self.conn.execute("""
|
||||
SELECT slot_key, code
|
||||
FROM target_candidates_history
|
||||
WHERE slot_key >= %s AND slot_key <= %s
|
||||
ORDER BY slot_key, score DESC
|
||||
""", (start_key, end_key)).fetchall()
|
||||
out: Dict[str, List[str]] = {}
|
||||
for r in rows:
|
||||
# 백테스트 엔진 _slot_key()는 항상 str → 조회 키도 str 통일 (DB 드라이버에 따라 int 올 수 있음)
|
||||
sk = str(r["slot_key"])
|
||||
if sk not in out:
|
||||
out[sk] = []
|
||||
out[sk].append(r["code"])
|
||||
return out
|
||||
except Exception as e:
|
||||
logger.debug(f"후보 이력 조회 실패: {e}")
|
||||
return {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# stock_meta 테마/섹터 메타데이터 헬퍼
|
||||
# ------------------------------------------------------------------
|
||||
@@ -1876,7 +2080,7 @@ class TradeDB:
|
||||
logger.error("cleanup_old_ws_candles 실패: %s", e)
|
||||
|
||||
def close(self):
|
||||
"""DB 연결 종료"""
|
||||
"""DB 연결 종료 (호출자가 매번 close 하므로 로그는 DEBUG 로 강등)."""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
logger.info("🔒 DB 연결 종료")
|
||||
logger.debug("🔒 DB 연결 종료")
|
||||
|
||||
Reference in New Issue
Block a user