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:
124
scripts/_diag_mom_2min_gap.py
Normal file
124
scripts/_diag_mom_2min_gap.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SK 10:36 BT vs 10:38 live — DB 타임라인 + 틱/봉 진단."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from database import TradeDB
|
||||
|
||||
CODE = "475150"
|
||||
DAY = "20260713"
|
||||
DAY_DASH = "2026-07-13"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
db = TradeDB()
|
||||
try:
|
||||
cols = [r["Field"] for r in db.conn.execute("SHOW COLUMNS FROM trade_history").fetchall()]
|
||||
print("trade_history cols:", cols)
|
||||
|
||||
rows = db.conn.execute(
|
||||
"SELECT code, name, strategy, buy_price, sell_price, qty, realized_pnl, "
|
||||
"buy_date, sell_date, sell_reason, hold_minutes "
|
||||
"FROM trade_history WHERE code=%s AND buy_date LIKE %s ORDER BY buy_date",
|
||||
(CODE, DAY_DASH + "%"),
|
||||
).fetchall()
|
||||
print(f"\ntrade_history SK today n={len(rows)}")
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
print(
|
||||
f" buy={d.get('buy_date')} sell={d.get('sell_date')} "
|
||||
f"bp={d.get('buy_price')} sp={d.get('sell_price')} qty={d.get('qty')} "
|
||||
f"pnl={d.get('realized_pnl')} reason={d.get('sell_reason')} "
|
||||
f"strat={d.get('strategy')}"
|
||||
)
|
||||
|
||||
print("\n1m candles 10:34-10:40:")
|
||||
cans = db.conn.execute(
|
||||
"SELECT candle_time, open, high, low, close, volume FROM ws_candles "
|
||||
"WHERE code=%s AND timeframe=%s AND candle_time BETWEEN %s AND %s "
|
||||
"ORDER BY candle_time",
|
||||
(CODE, 1, DAY + "1034", DAY + "1040"),
|
||||
).fetchall()
|
||||
for c in cans:
|
||||
print(
|
||||
f" {c['candle_time']} O={c['open']} H={c['high']} "
|
||||
f"L={c['low']} C={c['close']} V={c['volume']}"
|
||||
)
|
||||
|
||||
print("\nfirst/last tick per minute 10:35-10:39:")
|
||||
for m in ("1035", "1036", "1037", "1038", "1039"):
|
||||
tt0, tt1 = DAY + m + "00", DAY + m + "59"
|
||||
r = db.conn.execute(
|
||||
"SELECT COUNT(*) n, MIN(tick_time) mn, MAX(tick_time) mx, "
|
||||
"MIN(price) lo, MAX(price) hi FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time BETWEEN %s AND %s",
|
||||
(CODE, tt0, tt1),
|
||||
).fetchone()
|
||||
first = db.conn.execute(
|
||||
"SELECT tick_time, price FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time BETWEEN %s AND %s "
|
||||
"ORDER BY tick_time ASC LIMIT 1",
|
||||
(CODE, tt0, tt1),
|
||||
).fetchone()
|
||||
print(
|
||||
f" {m}: n={r['n']} {r['mn']}~{r['mx']} "
|
||||
f"first={dict(first) if first else None} range={r['lo']}~{r['hi']}"
|
||||
)
|
||||
|
||||
# nearest tick to live buy 10:38:06 at 51600
|
||||
print("\nticks near live buy 51600 @10:38:")
|
||||
near = db.conn.execute(
|
||||
"SELECT tick_time, price, volume FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time BETWEEN %s AND %s AND price BETWEEN %s AND %s "
|
||||
"ORDER BY tick_time LIMIT 20",
|
||||
(CODE, DAY + "103700", DAY + "103900", 51500, 51700),
|
||||
).fetchall()
|
||||
for t in near:
|
||||
print(f" {dict(t)}")
|
||||
|
||||
# ticks near BT buy 50800 @10:36
|
||||
print("\nticks near BT buy 50800 @10:36:")
|
||||
near2 = db.conn.execute(
|
||||
"SELECT tick_time, price, volume FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time BETWEEN %s AND %s AND price BETWEEN %s AND %s "
|
||||
"ORDER BY tick_time LIMIT 20",
|
||||
(CODE, DAY + "103600", DAY + "103700", 50700, 50900),
|
||||
).fetchall()
|
||||
for t in near2:
|
||||
print(f" {dict(t)}")
|
||||
|
||||
hcols = [r["Field"] for r in db.conn.execute(
|
||||
"SHOW COLUMNS FROM target_candidates_history"
|
||||
).fetchall()]
|
||||
print("\nhistory cols:", hcols)
|
||||
if "slot_key" in hcols:
|
||||
hs2 = db.conn.execute(
|
||||
"SELECT slot_key, COUNT(*) n FROM target_candidates_history "
|
||||
"WHERE code=%s AND slot_key LIKE %s GROUP BY slot_key ORDER BY slot_key",
|
||||
(CODE, DAY + "103%"),
|
||||
).fetchall()
|
||||
print("SK slots 103x:")
|
||||
for h in hs2:
|
||||
print(f" {h['slot_key']} n={h['n']}")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
print("\n=== journal 10:34-10:41 ===")
|
||||
p = subprocess.run(
|
||||
[
|
||||
"journalctl", "-u", "kis_trader_main.service",
|
||||
"--since", "2026-07-13 10:34:00",
|
||||
"--until", "2026-07-13 10:41:00",
|
||||
"--no-pager",
|
||||
],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
keys = ("475150", "이터닉스", "MOMENTUM")
|
||||
for line in p.stdout.splitlines():
|
||||
if any(k in line for k in keys):
|
||||
print(line[:240])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
99
scripts/_diag_mom_tick_today.py
Normal file
99
scripts/_diag_mom_tick_today.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""오늘 모멘텀 거래종목 vs 유니버스 틱 커버 진단 (adhoc)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from database import TradeDB
|
||||
|
||||
DAY = "20260713"
|
||||
CODES = ["475150", "039340", "241710"]
|
||||
TRADE_MINS = [
|
||||
"202607130930",
|
||||
"202607130931",
|
||||
"202607130943",
|
||||
"202607130944",
|
||||
"202607130952",
|
||||
"202607130953",
|
||||
"202607130959",
|
||||
"202607131014",
|
||||
"202607131019",
|
||||
"202607131020",
|
||||
"202607131026",
|
||||
"202607131027",
|
||||
"202607131032",
|
||||
"202607131033",
|
||||
"202607131036",
|
||||
"202607131038",
|
||||
"202607131101",
|
||||
"202607131102",
|
||||
"202607131116",
|
||||
"202607131118",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
db = TradeDB()
|
||||
try:
|
||||
cols = [r["Field"] for r in db.conn.execute("SHOW COLUMNS FROM ws_ticks").fetchall()]
|
||||
print("ws_ticks sample cols:", cols[:12])
|
||||
|
||||
for code in CODES:
|
||||
rows = db.conn.execute(
|
||||
"SELECT COUNT(*) n, MIN(tick_time) mn, MAX(tick_time) mx "
|
||||
"FROM ws_ticks WHERE code=%s AND tick_time LIKE %s",
|
||||
(code, DAY + "%"),
|
||||
).fetchone()
|
||||
print(f"ticks {code}: n={rows['n']} range={rows['mn']}~{rows['mx']}")
|
||||
|
||||
sk = db.conn.execute(
|
||||
"SELECT LEFT(tick_time,12) m, COUNT(*) n FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time >= %s AND tick_time <= %s "
|
||||
"GROUP BY LEFT(tick_time,12) ORDER BY m",
|
||||
("475150", DAY + "090000", DAY + "113059"),
|
||||
).fetchall()
|
||||
have = {r["m"]: int(r["n"]) for r in sk}
|
||||
print(f"SK minutes with ticks 09:00-11:30: {len(have)}")
|
||||
print("trade-related minutes tick count:")
|
||||
for m in TRADE_MINS:
|
||||
print(f" {m[8:]} n={have.get(m, 0)}")
|
||||
|
||||
nc = db.conn.execute(
|
||||
"SELECT COUNT(DISTINCT code) c FROM ws_ticks WHERE tick_time LIKE %s",
|
||||
(DAY + "%",),
|
||||
).fetchone()
|
||||
print("distinct codes with ticks today:", nc["c"])
|
||||
|
||||
try:
|
||||
cc = db.conn.execute(
|
||||
"SELECT COUNT(DISTINCT code) c FROM ws_candles "
|
||||
"WHERE candle_time LIKE %s AND timeframe=%s",
|
||||
(DAY + "%", 1),
|
||||
).fetchone()
|
||||
print("distinct codes with 1m candles today:", cc["c"])
|
||||
except Exception as e:
|
||||
print("candles query skip:", e)
|
||||
|
||||
# 30% coverage 의미: 유니버스 전종목 전분봉 중 틱 있는 분 비율
|
||||
# 거래 3종만 보면?
|
||||
for code in CODES:
|
||||
bars = db.conn.execute(
|
||||
"SELECT COUNT(DISTINCT LEFT(candle_time,12)) n FROM ws_candles "
|
||||
"WHERE code=%s AND candle_time LIKE %s AND timeframe=%s "
|
||||
"AND LEFT(candle_time,12) BETWEEN %s AND %s",
|
||||
(code, DAY + "%", 1, DAY + "0900", DAY + "1530"),
|
||||
).fetchone()
|
||||
tmin = db.conn.execute(
|
||||
"SELECT COUNT(DISTINCT LEFT(tick_time,12)) n FROM ws_ticks "
|
||||
"WHERE code=%s AND tick_time LIKE %s "
|
||||
"AND LEFT(tick_time,12) BETWEEN %s AND %s",
|
||||
(code, DAY + "%", DAY + "0900", DAY + "1530"),
|
||||
).fetchone()
|
||||
bn = int(bars["n"] or 0)
|
||||
tn = int(tmin["n"] or 0)
|
||||
pct = (100.0 * tn / bn) if bn else 0.0
|
||||
print(f"cover {code}: tick_mins={tn} candle_mins={bn} pct={pct:.1f}%")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
scripts/_run_breakout_optuna_715.sh
Executable file
26
scripts/_run_breakout_optuna_715.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="breakout_fine_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_breakout_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_breakout_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_breakout_fine_latest.study
|
||||
# 120만 한도 정합 (구 study는 env_config 단독→600만 버그). 새 study-name 필수.
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy breakout \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--apply-best \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
28
scripts/_run_breakout_optuna_fine_wideTune_715.sh
Executable file
28
scripts/_run_breakout_optuna_fine_wideTune_715.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="breakout_fine_wideTune_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_breakout_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_breakout_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_breakout_fine_latest.study
|
||||
# wide(7/15) Top 분지 fine — apply 없음. 확인 후 최빈/1위 적용.
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy breakout \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by pnl \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=/home/hoon/kis_bot/$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
28
scripts/_run_breakout_optuna_wide_715.sh
Executable file
28
scripts/_run_breakout_optuna_wide_715.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="breakout_wide_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_breakout_wide_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_breakout_wide_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_breakout_wide_latest.study
|
||||
# wide 축 스크리닝 — apply 없음. 아침 fine/최빈은 wide Top 확인 후.
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy breakout \
|
||||
--mode wide \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 100 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by pnl \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=/home/hoon/kis_bot/$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
13
scripts/_run_error_watch_mm.sh
Executable file
13
scripts/_run_error_watch_mm.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# 오류감시 기동 (systemd 없이 nohup). sudo 있으면 deploy 유닛 사용 권장.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
mkdir -p logs
|
||||
pkill -f 'scripts/kis_error_watch_mm.py' 2>/dev/null || true
|
||||
sleep 1
|
||||
nohup "$ROOT/.venv/bin/python" -u "$ROOT/scripts/kis_error_watch_mm.py" \
|
||||
>> "$ROOT/logs/kis_error_watch_mm.log" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "LOG=$ROOT/logs/kis_error_watch_mm.log"
|
||||
echo "tail -f $ROOT/logs/kis_error_watch_mm.log"
|
||||
36
scripts/_run_momentum_optuna_715.sh
Executable file
36
scripts/_run_momentum_optuna_715.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="momentum_fine_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_momentum_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_momentum_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_momentum_fine_latest.study
|
||||
|
||||
# E(전일시가) ON 유지. DB 전일봉 없으면 prepare 시 키움 REST 1회/종목 → 메모리만.
|
||||
# 이전 E-OFF 탐색은 폐기 — 새 study-name.
|
||||
unset MOMENTUM_TRIGGER_E_CONFIRM || true
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP=1
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP_BARS="${MOMENTUM_BACKTEST_REST_WARMUP_BARS:-700}"
|
||||
export MOMENTUM_BACKTEST_REST_SLEEP_SEC="${MOMENTUM_BACKTEST_REST_SLEEP_SEC:-0.25}"
|
||||
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy momentum \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by pnl \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--apply-best \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
32
scripts/_run_momentum_optuna_fine_wideTune_715.sh
Normal file
32
scripts/_run_momentum_optuna_fine_wideTune_715.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="momentum_fine_lateRatchet_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_momentum_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_momentum_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_momentum_fine_latest.study
|
||||
unset MOMENTUM_TRIGGER_E_CONFIRM || true
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP=1
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP_BARS="${MOMENTUM_BACKTEST_REST_WARMUP_BARS:-700}"
|
||||
export MOMENTUM_BACKTEST_REST_SLEEP_SEC="${MOMENTUM_BACKTEST_REST_SLEEP_SEC:-0.25}"
|
||||
# fine + 늦게잠금 래칫 격자 + JSON 경로 고지 테스트 — apply 없음
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy momentum \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by pnl \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=/home/hoon/kis_bot/$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
34
scripts/_run_momentum_optuna_wide_715.sh
Executable file
34
scripts/_run_momentum_optuna_wide_715.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="momentum_wide_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_momentum_wide_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_momentum_wide_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_momentum_wide_latest.study
|
||||
|
||||
unset MOMENTUM_TRIGGER_E_CONFIRM || true
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP=1
|
||||
export MOMENTUM_BACKTEST_REST_WARMUP_BARS="${MOMENTUM_BACKTEST_REST_WARMUP_BARS:-700}"
|
||||
export MOMENTUM_BACKTEST_REST_SLEEP_SEC="${MOMENTUM_BACKTEST_REST_SLEEP_SEC:-0.25}"
|
||||
|
||||
# wide 축 스크리닝 — apply 없음. fine 재설계·적용은 wide Top 확인 후.
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy momentum \
|
||||
--mode wide \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 100 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by pnl \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
31
scripts/_run_scalp_optuna_715.sh
Executable file
31
scripts/_run_scalp_optuna_715.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="scalp_fine_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_scalp_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_scalp_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_scalp_fine_latest.study
|
||||
|
||||
# 모멘텀과 동일: 거래일 2026-07-15 · fine 200 · 호가 OFF · apply-best
|
||||
# 포트 한도 = config_scalp 병합 (SCALP_TOTAL_BUDGET_KRW=120만) — env_config 단독 금지(600만 버그)
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy scalp \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--min_win_rate 0 \
|
||||
--min_pf 0 \
|
||||
--sort-by score \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--apply-best \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
25
scripts/_run_tail_optuna_fine_715.sh
Executable file
25
scripts/_run_tail_optuna_fine_715.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="tail_fine_wide1Tune_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_tail_fine_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_tail_fine_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_tail_fine_latest.study
|
||||
# wide1 분지 fine — apply 없음. 격자 변경 → 새 study-name.
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy tail \
|
||||
--mode fine \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 200 \
|
||||
--min_trades 1 \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
25
scripts/_run_tail_optuna_wide2_715.sh
Executable file
25
scripts/_run_tail_optuna_wide2_715.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
# wide 격자 v2(확장) — categorical 변경이라 구 study 재사용 금지
|
||||
STUDY="tail_wide2_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_tail_wide2_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_tail_wide2_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_tail_wide2_latest.study
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy tail \
|
||||
--mode wide \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 100 \
|
||||
--min_trades 1 \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
25
scripts/_run_tail_optuna_wide_715.sh
Executable file
25
scripts/_run_tail_optuna_wide_715.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
mkdir -p logs
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
STUDY="tail_wide_20260715_${TS}"
|
||||
LOG="logs/param_search_optuna_tail_wide_${TS}.log"
|
||||
echo "$LOG" > logs/param_search_optuna_tail_wide_latest.logpath
|
||||
echo "$STUDY" > logs/param_search_optuna_tail_wide_latest.study
|
||||
# 7/15 축 스크리닝 — apply 없음. 새 study-name 필수(wide 그리드 신규).
|
||||
nohup python3 -u kis_trader/backtest/param_search_optuna.py \
|
||||
--strategy tail \
|
||||
--mode wide \
|
||||
--start 2026-07-15 \
|
||||
--end 2026-07-15 \
|
||||
--trials 100 \
|
||||
--min_trades 1 \
|
||||
--orderbook-filter off \
|
||||
--no-progress \
|
||||
--study-name "$STUDY" \
|
||||
> "$LOG" 2>&1 &
|
||||
echo "PID=$!"
|
||||
echo "STUDY=$STUDY"
|
||||
echo "LOG=$LOG"
|
||||
echo "tail -f /home/hoon/kis_bot/$LOG"
|
||||
183
scripts/append_tail_optuna_compare.py
Normal file
183
scripts/append_tail_optuna_compare.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Optuna 결과 JSON vs 현재 DB — 백테 비교표를 로그 파일 끝에 append (DB 미저장)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.engine import tail_engine as te
|
||||
|
||||
|
||||
COMPARE_KEYS = [
|
||||
"entry_mode",
|
||||
"min_drop_rate",
|
||||
"min_recovery_ratio",
|
||||
"tail_ratio_min",
|
||||
"tail_pct_min",
|
||||
"max_rec_3m",
|
||||
"shoulder_min_high",
|
||||
"shoulder_cut_pct",
|
||||
"stop_atr_mult",
|
||||
"target_atr_mult",
|
||||
"atr_sl_min_pct",
|
||||
"atr_sl_max_pct",
|
||||
"atr_tp_min_pct",
|
||||
"atr_tp_max_pct",
|
||||
"max_daily_change",
|
||||
"max_loss_krw",
|
||||
"limit_atr_mult",
|
||||
"tail_vol_mult",
|
||||
"tail_vol_win",
|
||||
"ratchet_tiers",
|
||||
"rsi_threshold",
|
||||
"cooldown_min",
|
||||
"bar_chg_min_pct",
|
||||
"bar_chg_max_pct",
|
||||
"symbol_daily_loss_limit_krw",
|
||||
"symbol_daily_loss_limit_pct",
|
||||
"reentry_min_edge_krw",
|
||||
"reentry_require_nonneg",
|
||||
"trail_tiers",
|
||||
"trail_drop_pct",
|
||||
"trail_arm_krw",
|
||||
"pattern_pin",
|
||||
"pattern_engulfing",
|
||||
"pattern_piercing",
|
||||
"max_daily",
|
||||
"max_spread_pct",
|
||||
"min_bid_ask_ratio",
|
||||
]
|
||||
|
||||
|
||||
def _same(a, b) -> bool:
|
||||
if a == b:
|
||||
return True
|
||||
try:
|
||||
return abs(float(a) - float(b)) < 1e-9
|
||||
except Exception:
|
||||
return str(a) == str(b)
|
||||
|
||||
|
||||
def _run(candles_by_code, universe, base, port, fee, tax, slot, budget, meta, overrides):
|
||||
params = dict(base)
|
||||
tbc.merge_tail_portfolio_into_params(params, port)
|
||||
params.update(overrides or {})
|
||||
trades = tbc.run_tail_backtest_web_aligned(
|
||||
candles_by_code,
|
||||
params,
|
||||
universe,
|
||||
slot_money=slot,
|
||||
fee_rate=fee,
|
||||
sell_tax=tax,
|
||||
total_budget_krw=budget,
|
||||
meta_out=dict(meta),
|
||||
)
|
||||
wins = [t for t in trades if float(t.get("pnl") or 0) > 0]
|
||||
pnl = sum(float(t.get("pnl") or 0) for t in trades)
|
||||
wr = (len(wins) / len(trades) * 100.0) if trades else 0.0
|
||||
return {
|
||||
"trades": len(trades),
|
||||
"wins": len(wins),
|
||||
"wr": wr,
|
||||
"pnl": pnl,
|
||||
"params": {k: params.get(k) for k in COMPARE_KEYS},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--json", required=True, help="optuna_tail_*.json 경로")
|
||||
ap.add_argument("--log", required=True, help="append 대상 로그 경로")
|
||||
ap.add_argument("--date", default="2026-07-10", help="백테 일자 YYYY-MM-DD")
|
||||
args = ap.parse_args()
|
||||
|
||||
json_path = Path(args.json)
|
||||
log_path = Path(args.log)
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
best = (data.get("results") or [None])[0]
|
||||
if not best:
|
||||
msg = "❌ Optuna results 비어 있음 — 비교 스킵\n"
|
||||
with log_path.open("a", encoding="utf-8") as f:
|
||||
f.write(msg)
|
||||
print(msg, end="")
|
||||
return 1
|
||||
|
||||
best_params = best.get("params") or {}
|
||||
day = args.date
|
||||
start_key = day.replace("-", "") + "0000"
|
||||
end_key = day.replace("-", "") + "2359"
|
||||
start_ymd, end_ymd = start_key[:8], end_key[:8]
|
||||
|
||||
db = TradeDB()
|
||||
base = te.get_tail_defaults_from_db(db)
|
||||
universe, src, n_slots, _ = tbc.resolve_tail_universe(
|
||||
start_ymd, end_ymd, use_saved_history=True, strategy_id="SHORT",
|
||||
)
|
||||
tf = int(base.get("timeframe") or 3)
|
||||
rsi = int(base.get("rsi_period") or 14)
|
||||
candles_by_code, _, _ = tbc.load_tail_candles_by_code(
|
||||
db, start_key, end_key, tf, rsi_period=rsi,
|
||||
)
|
||||
row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone()
|
||||
r = dict(row) if row else {}
|
||||
fee, tax, _ = tbc.fee_and_slot_from_env_row(r)
|
||||
port = tbc.resolve_tail_portfolio_params(r, base)
|
||||
slot = float(port["slot_money"])
|
||||
budget = float(port["total_budget_krw"])
|
||||
meta = {"db": db, "start_key": start_key, "end_key": end_key}
|
||||
|
||||
cur = _run(candles_by_code, universe, base, port, fee, tax, slot, budget, meta, {})
|
||||
bst = _run(
|
||||
candles_by_code, universe, base, port, fee, tax, slot, budget, meta, best_params,
|
||||
)
|
||||
delta = bst["pnl"] - cur["pnl"]
|
||||
trial_no = best.get("optuna_trial_number") or data.get("optuna_best_trial_number")
|
||||
mode = data.get("mode") or "?"
|
||||
elapsed = data.get("elapsed_sec")
|
||||
|
||||
lines = []
|
||||
lines.append("")
|
||||
lines.append("=" * 72)
|
||||
lines.append(f"[COMPARE] CURRENT_DB vs OPTUNA_{mode.upper()}_BEST | {day} | DB미저장")
|
||||
lines.append("=" * 72)
|
||||
lines.append(f"json={json_path}")
|
||||
lines.append(f"universe={src} slots={n_slots} | trial=#{trial_no} elapsed={elapsed}s")
|
||||
lines.append("")
|
||||
lines.append("| 구분 | 거래 | 승 | 승률 | 손익 |")
|
||||
lines.append("|------|------|----|------|------|")
|
||||
lines.append(
|
||||
f"| CURRENT_DB | {cur['trades']} | {cur['wins']} | {cur['wr']:.1f}% | {cur['pnl']:+,.0f} |"
|
||||
)
|
||||
lines.append(
|
||||
f"| OPTUNA_BEST | {bst['trades']} | {bst['wins']} | {bst['wr']:.1f}% | {bst['pnl']:+,.0f} |"
|
||||
)
|
||||
lines.append(f"| Δ(best-current) | | | | {delta:+,.0f} |")
|
||||
lines.append("")
|
||||
lines.append("| 파라미터 | CURRENT_DB | OPTUNA_BEST | diff |")
|
||||
lines.append("|----------|------------|--------------|------|")
|
||||
for k in COMPARE_KEYS:
|
||||
a = cur["params"].get(k)
|
||||
b = bst["params"].get(k)
|
||||
mark = "" if _same(a, b) else "<<"
|
||||
lines.append(f"| {k} | {a} | {b} | {mark} |")
|
||||
lines.append("")
|
||||
lines.append("DB 저장 없음 (--apply-best 미사용)")
|
||||
lines.append("=" * 72)
|
||||
lines.append("")
|
||||
text = "\n".join(lines)
|
||||
with log_path.open("a", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(text, end="")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
70
scripts/backfill_trade_candles.py
Normal file
70
scripts/backfill_trade_candles.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
청산·보유 종목의 매수~매도(또는 ~now) 구간 1분봉을 키움 REST로 즉시 백필.
|
||||
|
||||
예:
|
||||
nohup python3 -u scripts/backfill_trade_candles.py \\
|
||||
--like '2026-07-16%' > /tmp/backfill_trade_candles_0716.log 2>&1 &
|
||||
tail -f /tmp/backfill_trade_candles_0716.log
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.engine.post_sell_candle_backfill import backfill_trades_from_db
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--like", default="2026-07-16%", help="buy_date LIKE (%% 바인딩)")
|
||||
ap.add_argument(
|
||||
"--strategies",
|
||||
default="",
|
||||
help="콤마 구분 strategy (비우면 전체)",
|
||||
)
|
||||
ap.add_argument("--no-active", action="store_true", help="active_trades 제외")
|
||||
ap.add_argument("--active-days", type=int, default=5)
|
||||
args = ap.parse_args()
|
||||
|
||||
strategies = [s.strip() for s in str(args.strategies).split(",") if s.strip()] or None
|
||||
db = TradeDB()
|
||||
try:
|
||||
results = backfill_trades_from_db(
|
||||
db,
|
||||
buy_date_like=str(args.like),
|
||||
strategies=strategies,
|
||||
include_active=not args.no_active,
|
||||
active_max_age_days=int(args.active_days),
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ok = sum(1 for r in results if r.get("ok"))
|
||||
improved = sum(1 for r in results if int(r.get("after") or 0) > int(r.get("before") or 0))
|
||||
print(
|
||||
f"DONE jobs={len(results)} ok={ok} improved={improved} "
|
||||
f"upsert_sum={sum(int(r.get('upserted') or 0) for r in results)}"
|
||||
)
|
||||
for r in results:
|
||||
if not r.get("ok") or int(r.get("after") or 0) > int(r.get("before") or 0):
|
||||
print(
|
||||
f" {r.get('strategy')} {r.get('code')} "
|
||||
f"{r.get('start')}~{r.get('end')} "
|
||||
f"{r.get('before')}→{r.get('after')} "
|
||||
f"upsert={r.get('upserted')} err={r.get('error')!r}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
387
scripts/kis_error_watch_mm.py
Executable file
387
scripts/kis_error_watch_mm.py
Executable file
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
kis_error_watch_mm.py — kis_trader_main journalctl 실시간(tail -f) 감시 → Mattermost
|
||||
|
||||
실매 봇과 분리된 프로세스. journald 만 보고 오류 시 MM 알림.
|
||||
- Traceback / FATAL / dead=[...] / 유닛 다운 등
|
||||
- 동일·유사 알림은 쿨다운으로 스팸 방지
|
||||
- 상태 JSON 즉시 저장(재시작 후에도 쿨다운 유지)
|
||||
|
||||
실행:
|
||||
nohup .venv/bin/python -u scripts/kis_error_watch_mm.py \\
|
||||
>> logs/kis_error_watch_mm.log 2>&1 &
|
||||
tail -f logs/kis_error_watch_mm.log
|
||||
|
||||
테스트:
|
||||
.venv/bin/python scripts/kis_error_watch_mm.py --test-mm
|
||||
|
||||
systemd (선택):
|
||||
sudo cp deploy/kis_error_watch_mm.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload && sudo systemctl enable --now kis_error_watch_mm
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Pattern, Tuple
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from kis_trader.utils.env import ( # noqa: E402
|
||||
get_env_bool,
|
||||
get_env_float,
|
||||
get_env_from_db,
|
||||
get_env_int,
|
||||
)
|
||||
from kis_trader.utils.logger import atomic_load_json, atomic_save_json, msg_mm # noqa: E402
|
||||
|
||||
LOG_PATH = ROOT / "logs" / "kis_error_watch_mm.log"
|
||||
STATE_PATH = ROOT / "logs" / "kis_error_watch_mm_state.json"
|
||||
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[logging.StreamHandler(sys.stdout)],
|
||||
)
|
||||
log = logging.getLogger("error_watch")
|
||||
|
||||
_STOP = False
|
||||
|
||||
|
||||
def _on_signal(signum, _frame) -> None:
|
||||
global _STOP
|
||||
_STOP = True
|
||||
log.info("⏹ signal=%s → 종료 예약", signum)
|
||||
|
||||
|
||||
def _cfg() -> dict:
|
||||
"""DB/env 설정 — 하드코딩 수치 금지, get_env_* 만."""
|
||||
# Traceback·FATAL·비어있지 않은 dead=·유닛 크래시 시그니처
|
||||
default_match = (
|
||||
r"(?i)("
|
||||
r"Traceback \(most recent call last\)|"
|
||||
r"\bCRITICAL\b|\bFATAL\b|MemoryError|SIGBUS|Segmentation fault|"
|
||||
r"dead=\[[^\]]|" # dead=[] 제외, dead=['Strat-... 매칭
|
||||
r"Main process exited|Failed with result|"
|
||||
r"can't open file|"
|
||||
r"강제\s*종료|Out of memory"
|
||||
r")"
|
||||
)
|
||||
default_ignore = (
|
||||
r"(?i)("
|
||||
r"numexpr\.utils|"
|
||||
r"\[MM 스킵\]|"
|
||||
r"MM 발송 실패|"
|
||||
r"heartbeat ws="
|
||||
r")"
|
||||
)
|
||||
return {
|
||||
"enabled": get_env_bool("ERROR_WATCH_ENABLED", True),
|
||||
"unit": str(
|
||||
get_env_from_db("ERROR_WATCH_UNIT", "kis_trader_main.service")
|
||||
or "kis_trader_main.service"
|
||||
).strip(),
|
||||
"channel": str(
|
||||
get_env_from_db("ERROR_WATCH_MM_CHANNEL", "")
|
||||
or get_env_from_db("KIS_SYSTEM_MM_CHANNEL", "default")
|
||||
or "default"
|
||||
).strip()
|
||||
or "default",
|
||||
"cooldown_sec": max(30, get_env_int("ERROR_WATCH_COOLDOWN_SEC", 180)),
|
||||
"context_lines": max(1, min(20, get_env_int("ERROR_WATCH_CONTEXT_LINES", 5))),
|
||||
"traceback_extra": max(0, min(40, get_env_int("ERROR_WATCH_TRACEBACK_EXTRA_LINES", 12))),
|
||||
"health_sec": max(15, get_env_int("ERROR_WATCH_HEALTH_CHECK_SEC", 60)),
|
||||
"match_re": str(
|
||||
get_env_from_db("ERROR_WATCH_MATCH_REGEX", default_match) or default_match
|
||||
),
|
||||
"ignore_re": str(
|
||||
get_env_from_db("ERROR_WATCH_IGNORE_REGEX", default_ignore) or default_ignore
|
||||
),
|
||||
"jitter": get_env_bool("ERROR_WATCH_MM_JITTER", False),
|
||||
}
|
||||
|
||||
|
||||
def _compile_re(pat: str, name: str) -> Optional[Pattern[str]]:
|
||||
try:
|
||||
return re.compile(pat)
|
||||
except re.error as e:
|
||||
log.error("❌ regex 컴파일 실패 (%s): %s", name, e)
|
||||
return None
|
||||
|
||||
|
||||
def _load_state() -> dict:
|
||||
st = atomic_load_json(STATE_PATH, default={})
|
||||
if not isinstance(st, dict):
|
||||
return {}
|
||||
return st
|
||||
|
||||
|
||||
def _save_state(st: dict) -> None:
|
||||
atomic_save_json(STATE_PATH, st)
|
||||
|
||||
|
||||
def _fp(text: str) -> str:
|
||||
# 시각·PID 제거 후 지문 → 같은 오류 반복 쿨다운
|
||||
norm = re.sub(r"\d{2}:\d{2}:\d{2}", "", text)
|
||||
norm = re.sub(r"python\[\d+\]", "python[PID]", norm)
|
||||
norm = re.sub(r"\s+", " ", norm).strip()[:800]
|
||||
return hashlib.sha1(norm.encode("utf-8", errors="ignore")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _can_alert(st: dict, fingerprint: str, cooldown_sec: int) -> bool:
|
||||
now = time.time()
|
||||
last_ts = float(st.get("last_alert_ts") or 0)
|
||||
last_fp = str(st.get("last_fingerprint") or "")
|
||||
if fingerprint == last_fp and (now - last_ts) < cooldown_sec:
|
||||
return False
|
||||
if (now - last_ts) < float(get_env_float("ERROR_WATCH_GLOBAL_MIN_GAP_SEC", 20.0)):
|
||||
# 서로 다른 오류라도 최소 간격
|
||||
if fingerprint != last_fp and (now - last_ts) < cooldown_sec * 0.15:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _send_alert(title: str, lines: List[str], channel: str, jitter: bool, st: dict, fingerprint: str) -> bool:
|
||||
body_lines = [
|
||||
f"🚨 **[오류감시] {title}**",
|
||||
f"- 시각: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
f"- 유닛: `{get_env_from_db('ERROR_WATCH_UNIT', 'kis_trader_main.service')}`",
|
||||
"```",
|
||||
]
|
||||
clipped = "\n".join(lines)[:3500]
|
||||
body_lines.append(clipped)
|
||||
body_lines.append("```")
|
||||
body = "\n".join(body_lines)
|
||||
ok = msg_mm(body, channel_alias=channel, jitter=jitter)
|
||||
st["last_alert_ts"] = time.time()
|
||||
st["last_fingerprint"] = fingerprint
|
||||
st["last_title"] = title
|
||||
st["alert_count"] = int(st.get("alert_count") or 0) + 1
|
||||
_save_state(st)
|
||||
log.info("📤 MM %s title=%s fp=%s", "OK" if ok else "FAIL", title, fingerprint)
|
||||
return ok
|
||||
|
||||
|
||||
def _unit_active(unit: str) -> Tuple[bool, str]:
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["systemctl", "is-active", unit],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
state = (r.stdout or "").strip() or (r.stderr or "").strip() or "unknown"
|
||||
return state == "active", state
|
||||
except Exception as e:
|
||||
return False, f"check_error:{e}"
|
||||
|
||||
|
||||
def _follow_journal(unit: str) -> subprocess.Popen:
|
||||
# -n 0: 과거 덤프 없이 follow만 (기동 직후 과거 Traceback 폭주 방지)
|
||||
cmd = [
|
||||
"journalctl",
|
||||
"-u", unit,
|
||||
"-f",
|
||||
"-n", "0",
|
||||
"--output=short-iso",
|
||||
"--no-pager",
|
||||
]
|
||||
log.info("📡 follow: %s", " ".join(cmd))
|
||||
return subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
|
||||
def run_watch() -> int:
|
||||
cfg = _cfg()
|
||||
if not cfg["enabled"]:
|
||||
log.warning("ERROR_WATCH_ENABLED=false → 종료")
|
||||
return 0
|
||||
|
||||
match_re = _compile_re(cfg["match_re"], "MATCH")
|
||||
ignore_re = _compile_re(cfg["ignore_re"], "IGNORE")
|
||||
if match_re is None:
|
||||
return 2
|
||||
|
||||
unit = cfg["unit"]
|
||||
channel = cfg["channel"]
|
||||
st = _load_state()
|
||||
log.info(
|
||||
"✅ 감시 시작 unit=%s ch=%s cooldown=%ss health=%ss",
|
||||
unit, channel, cfg["cooldown_sec"], cfg["health_sec"],
|
||||
)
|
||||
|
||||
# 기동 알림 (감시자 살아있음 확인)
|
||||
if get_env_bool("ERROR_WATCH_STARTUP_NOTIFY", True):
|
||||
active, state = _unit_active(unit)
|
||||
msg_mm(
|
||||
f"👁️ **[오류감시 기동]** `{unit}` → `{state}`"
|
||||
f"{' ✅' if active else ' ⚠️ 비활성'}",
|
||||
channel_alias=channel,
|
||||
jitter=False,
|
||||
)
|
||||
|
||||
proc = _follow_journal(unit)
|
||||
buf: List[str] = []
|
||||
collecting_tb = False
|
||||
tb_left = 0
|
||||
last_health = time.time()
|
||||
was_active = True
|
||||
|
||||
assert proc.stdout is not None
|
||||
|
||||
while not _STOP:
|
||||
# health poll
|
||||
now = time.time()
|
||||
if now - last_health >= cfg["health_sec"]:
|
||||
last_health = now
|
||||
active, state = _unit_active(unit)
|
||||
if not active:
|
||||
fp = _fp(f"unit_down:{unit}:{state}")
|
||||
if _can_alert(st, fp, cfg["cooldown_sec"]):
|
||||
_send_alert(
|
||||
f"유닛 비활성 ({state})",
|
||||
[f"systemctl is-active {unit} → {state}"],
|
||||
channel,
|
||||
cfg["jitter"],
|
||||
st,
|
||||
fp,
|
||||
)
|
||||
was_active = False
|
||||
elif not was_active:
|
||||
# 복구 알림
|
||||
fp = _fp(f"unit_up:{unit}")
|
||||
if _can_alert(st, fp, max(30, cfg["cooldown_sec"] // 3)):
|
||||
_send_alert(
|
||||
"유닛 복구 (active)",
|
||||
[f"systemctl is-active {unit} → active"],
|
||||
channel,
|
||||
cfg["jitter"],
|
||||
st,
|
||||
fp,
|
||||
)
|
||||
was_active = True
|
||||
|
||||
# journalctl 죽었으면 재기동
|
||||
if proc.poll() is not None:
|
||||
log.warning("⚠️ journalctl 종료 code=%s → 재기동", proc.returncode)
|
||||
proc = _follow_journal(unit)
|
||||
assert proc.stdout is not None
|
||||
|
||||
# non-blocking-ish read with timeout via select
|
||||
import select
|
||||
|
||||
ready, _, _ = select.select([proc.stdout], [], [], 1.0)
|
||||
if not ready:
|
||||
continue
|
||||
line = proc.stdout.readline()
|
||||
if line == "":
|
||||
# EOF — 재기동
|
||||
time.sleep(1.0)
|
||||
if proc.poll() is not None:
|
||||
proc = _follow_journal(unit)
|
||||
assert proc.stdout is not None
|
||||
continue
|
||||
|
||||
line = line.rstrip("\n")
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# ignore
|
||||
if ignore_re is not None and ignore_re.search(line):
|
||||
continue
|
||||
|
||||
# Traceback 블록 수집
|
||||
if "Traceback (most recent call last)" in line:
|
||||
collecting_tb = True
|
||||
tb_left = cfg["traceback_extra"]
|
||||
buf = [line]
|
||||
continue
|
||||
|
||||
if collecting_tb:
|
||||
buf.append(line)
|
||||
tb_left -= 1
|
||||
# 들여쓴 프레임이 끝나고 일반 로그가 오면 종료
|
||||
if tb_left <= 0 or (
|
||||
len(buf) > 2
|
||||
and not line.startswith(" ")
|
||||
and not line.startswith("\t")
|
||||
and "File \"" not in line
|
||||
and not line.lstrip().startswith("File ")
|
||||
and "Error" not in line
|
||||
and "Exception" not in line
|
||||
):
|
||||
collecting_tb = False
|
||||
block = buf[:]
|
||||
buf = []
|
||||
fp = _fp("\n".join(block))
|
||||
if _can_alert(st, fp, cfg["cooldown_sec"]):
|
||||
_send_alert("Traceback", block, channel, cfg["jitter"], st, fp)
|
||||
continue
|
||||
|
||||
if match_re.search(line):
|
||||
# 직전 컨텍스트는 journal에 없으므로 히트 라인 + 이후 N줄은 어려움 → 히트만
|
||||
ctx = [line]
|
||||
fp = _fp(line)
|
||||
if _can_alert(st, fp, cfg["cooldown_sec"]):
|
||||
title = "로그 오류 매칭"
|
||||
if "dead=[" in line:
|
||||
title = "전략 dead 감지"
|
||||
elif "exited" in line.lower() or "Failed with result" in line:
|
||||
title = "프로세스 종료"
|
||||
_send_alert(title, ctx, channel, cfg["jitter"], st, fp)
|
||||
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
log.info("👋 오류감시 종료")
|
||||
return 0
|
||||
|
||||
|
||||
def run_test_mm() -> int:
|
||||
ch = str(
|
||||
get_env_from_db("ERROR_WATCH_MM_CHANNEL", "")
|
||||
or get_env_from_db("KIS_SYSTEM_MM_CHANNEL", "default")
|
||||
or "default"
|
||||
).strip() or "default"
|
||||
ok = msg_mm(
|
||||
"🧪 **[오류감시 테스트]** kis_error_watch_mm.py --test-mm OK",
|
||||
channel_alias=ch,
|
||||
jitter=False,
|
||||
)
|
||||
print(f"test_mm channel={ch} ok={ok}")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
signal.signal(signal.SIGINT, _on_signal)
|
||||
signal.signal(signal.SIGTERM, _on_signal)
|
||||
ap = argparse.ArgumentParser(description="kis_trader journal 오류 → Mattermost")
|
||||
ap.add_argument("--test-mm", action="` `", help="테스트 메시지 1회 발송 후 종료")
|
||||
args = ap.parse_args()
|
||||
if args.test_mm:
|
||||
return run_test_mm()
|
||||
return run_watch()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
362
scripts/market_open_sim_smoke.py
Normal file
362
scripts/market_open_sim_smoke.py
Normal file
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
내일(다음 거래일) 장 시작(09:05)을 가정한 격리 스모크.
|
||||
- 실매매 서비스(WS/주문)는 건드리지 않음
|
||||
- FORCE_MARKET_OPEN 을 DB/실매에 쓰지 않음 (프로세스 내 datetime 패치만)
|
||||
- 계좌 조회(REST) + 엔진/DB 기본값 + 장시작 리포트 문자열 + 직전 거래일 백테 스모크
|
||||
|
||||
로그: logs/market_open_sim_smoke.log
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Tuple
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
LOG_PATH = ROOT / "logs" / "market_open_sim_smoke.log"
|
||||
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler(LOG_PATH, encoding="utf-8"),
|
||||
],
|
||||
)
|
||||
log = logging.getLogger("market_open_sim")
|
||||
|
||||
errors: List[str] = []
|
||||
oks: List[str] = []
|
||||
|
||||
|
||||
def _ok(msg: str) -> None:
|
||||
oks.append(msg)
|
||||
log.info("✅ %s", msg)
|
||||
|
||||
|
||||
def _err(msg: str) -> None:
|
||||
errors.append(msg)
|
||||
log.error("❌ %s", msg)
|
||||
|
||||
|
||||
def _next_trading_day(from_d: date) -> date:
|
||||
from kis_trader.utils.kr_trading_day import is_kr_trading_day
|
||||
|
||||
d = from_d + timedelta(days=1)
|
||||
for _ in range(21):
|
||||
if is_kr_trading_day(d):
|
||||
return d
|
||||
d += timedelta(days=1)
|
||||
raise RuntimeError("next trading day not found")
|
||||
|
||||
|
||||
def step_calendar() -> datetime:
|
||||
from kis_trader.utils.kr_trading_day import (
|
||||
clamp_to_prev_kr_trading_day,
|
||||
is_kr_trading_day,
|
||||
trading_dates_payload,
|
||||
)
|
||||
|
||||
today = date.today()
|
||||
tom = _next_trading_day(today)
|
||||
assert is_kr_trading_day(tom), tom
|
||||
prev = clamp_to_prev_kr_trading_day(today)
|
||||
payload = trading_dates_payload(7)
|
||||
_ok(
|
||||
f"달력: today={today} next_open={tom} prev_td={prev} "
|
||||
f"web_defaults={payload.get('start')}~{payload.get('end')}"
|
||||
)
|
||||
# 장시작 09:05 가정
|
||||
return datetime(tom.year, tom.month, tom.day, 9, 5, 0)
|
||||
|
||||
|
||||
def step_strategy_flags() -> None:
|
||||
from kis_trader.utils.env import get_env_bool
|
||||
|
||||
flags = {
|
||||
"SCALP": get_env_bool("STRATEGY_SCALP_ENABLED", True),
|
||||
"SHORT": get_env_bool("STRATEGY_SHORT_ENABLED", True),
|
||||
"MOMENTUM": get_env_bool("STRATEGY_MOMENTUM_ENABLED", False),
|
||||
"BREAKOUT": get_env_bool("STRATEGY_BREAKOUT_ENABLED", False),
|
||||
"RANGE_BREAK": get_env_bool("STRATEGY_RANGE_BREAK_ENABLED", False),
|
||||
"UPDOW": get_env_bool("STRATEGY_UPDOW_ENABLED", False),
|
||||
"DBBAND": get_env_bool("STRATEGY_DBBAND_ENABLED", False),
|
||||
}
|
||||
on = [k for k, v in flags.items() if v]
|
||||
off = [k for k, v in flags.items() if not v]
|
||||
_ok(f"전략 ON={on} OFF={off}")
|
||||
# HTS 스킵은 false 유지 규칙
|
||||
for key in (
|
||||
"TAIL_SKIP_HTS_SCAN_DUPES",
|
||||
"SHORT_SKIP_HTS_SCAN_DUPES",
|
||||
"MOMENTUM_SKIP_HTS_SCAN_DUPES",
|
||||
"BREAKOUT_SKIP_HTS_SCAN_DUPES",
|
||||
"SCALP_SKIP_HTS_SCAN_DUPES",
|
||||
):
|
||||
if get_env_bool(key, False):
|
||||
_err(f"{key}=true (기본 false 유지 규칙 위반)")
|
||||
else:
|
||||
_ok(f"{key}=false")
|
||||
|
||||
|
||||
def step_market_hours(fake_now: datetime) -> None:
|
||||
from kis_trader.strategies.base import BaseStrategy
|
||||
from kis_trader.network.market_guard import MarketGuard
|
||||
|
||||
class _Dummy(BaseStrategy):
|
||||
strategy_id = "SHORT"
|
||||
|
||||
def __init__(self):
|
||||
# Thread/풀 초기화 우회: 최소 속성만
|
||||
self.strategy_id = "SHORT"
|
||||
|
||||
def check_buy(self, *a, **k): # pragma: no cover
|
||||
return False
|
||||
|
||||
def check_sell_signals(self, *a, **k): # pragma: no cover
|
||||
return None
|
||||
|
||||
def run(self): # pragma: no cover
|
||||
return None
|
||||
|
||||
with patch("kis_trader.strategies.base.dt") as mock_dt, patch(
|
||||
"kis_trader.network.market_guard.dt"
|
||||
) as mock_dt2:
|
||||
mock_dt.now.return_value = fake_now
|
||||
mock_dt2.now.return_value = fake_now
|
||||
d = _Dummy()
|
||||
# BaseStrategy.check_market_status 는 self 만 필요
|
||||
open_ok = BaseStrategy.check_market_status(d)
|
||||
buy_ok = BaseStrategy.check_buy_allowed(d)
|
||||
mg_ok = MarketGuard._is_market_hours()
|
||||
if open_ok and buy_ok and mg_ok:
|
||||
_ok(f"장시간 판정 (fake {fake_now}): market=True buy=True guard=True")
|
||||
else:
|
||||
_err(
|
||||
f"장시간 판정 실패: market={open_ok} buy={buy_ok} guard={mg_ok} "
|
||||
f"fake={fake_now}"
|
||||
)
|
||||
|
||||
|
||||
def step_engine_defaults() -> None:
|
||||
try:
|
||||
from kis_trader.engine import momentum_engine as me
|
||||
from kis_trader.engine import scalping_engine as se
|
||||
from kis_trader.engine import tail_engine as te
|
||||
from kis_trader.strategies import breakout as bo
|
||||
|
||||
te_d = te.get_tail_defaults_from_db() if hasattr(te, "get_tail_defaults_from_db") else None
|
||||
me_d = me.get_momentum_defaults_from_db()
|
||||
# scalping / breakout
|
||||
if hasattr(se, "get_scalping_defaults_from_db"):
|
||||
se_d = se.get_scalping_defaults_from_db()
|
||||
else:
|
||||
se_d = {"ok": True}
|
||||
from kis_trader.backtest import breakout_backtest_common as bbc
|
||||
from kis_trader.utils.env import get_merged_env_dict
|
||||
|
||||
env_row = get_merged_env_dict() or {}
|
||||
if hasattr(bbc, "get_breakout_defaults_from_env_row"):
|
||||
bo_d = bbc.get_breakout_defaults_from_env_row(env_row)
|
||||
else:
|
||||
bo_d = {}
|
||||
_ok(
|
||||
f"엔진 DB 기본값 로드: tail_keys={len(te_d or {})} "
|
||||
f"mom={len(me_d or {})} scalp={len(se_d or {})} bo={len(bo_d or {})}"
|
||||
)
|
||||
# 손절 키 존재 스모크
|
||||
for name, d in (("mom", me_d),):
|
||||
if d and "sl_pct" in d and float(d["sl_pct"]) <= 0:
|
||||
_err(f"{name} sl_pct 비정상: {d.get('sl_pct')}")
|
||||
except Exception as e:
|
||||
_err(f"엔진 기본값 로드 실패: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def step_account_and_open_report() -> None:
|
||||
"""REST 잔고만 — WS/주문 없음. 장시작 리포트 문자열 생성."""
|
||||
try:
|
||||
from kis_trader.execution.kis_client import KISClient
|
||||
|
||||
client = KISClient()
|
||||
# TradingBot._fetch_asset_snapshot 과 유사하게 잔고 조회
|
||||
bal = None
|
||||
for meth in ("get_balance", "inquire_balance", "account_balance"):
|
||||
fn = getattr(client, meth, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
bal = fn()
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if bal is None:
|
||||
# inquire-balance 계열 탐색
|
||||
for name in dir(client):
|
||||
if "balance" in name.lower() and callable(getattr(client, name)):
|
||||
try:
|
||||
bal = getattr(client, name)()
|
||||
if bal:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
mock = getattr(client, "mock", None)
|
||||
acct = getattr(client, "account_no", "") or ""
|
||||
_ok(f"KISClient 생성 ok mock={mock} acct=***{str(acct)[-4:]}")
|
||||
if bal is not None:
|
||||
_ok(f"잔고 조회 응답 type={type(bal).__name__}")
|
||||
else:
|
||||
# 주말이면 모의/실전 REST 가 비정상일 수 있음 — 치명으로 안 봄
|
||||
log.warning("⚠️ 잔고 조회 메서드 미확인/실패 (장외 REST 가능) — 계속")
|
||||
|
||||
# 장시작 리포트 포맷만 검증 (MM 미전송)
|
||||
lines = [
|
||||
"🌅 **[장 시작 알림 - 09:00]** (SIM)",
|
||||
f"- 🤖 활성 전략: (smoke)",
|
||||
f"- 계좌: {'모의' if mock else '실전'}",
|
||||
"📈 오늘도 안전 매매! 손절 라인 준수.",
|
||||
]
|
||||
body = "\n".join(lines)
|
||||
assert "장 시작" in body
|
||||
_ok("장시작 리포트 문자열 생성 OK (미전송)")
|
||||
except Exception as e:
|
||||
_err(f"계좌/장시작 리포트 실패: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def step_verify_three_paths() -> None:
|
||||
try:
|
||||
from kis_trader.scripts import verify_three_paths as v3
|
||||
|
||||
bad = 0
|
||||
for fn_name in ("verify_momentum", "verify_breakout", "verify_scalping", "verify_tail"):
|
||||
fn = getattr(v3, fn_name, None)
|
||||
if not callable(fn):
|
||||
continue
|
||||
n = int(fn() or 0)
|
||||
bad += n
|
||||
if n:
|
||||
_err(f"{fn_name} mismatch={n}")
|
||||
else:
|
||||
_ok(f"{fn_name} parity OK")
|
||||
if bad == 0:
|
||||
_ok("실매↔웹↔파람 변환 정합 스모크 통과")
|
||||
except Exception as e:
|
||||
_err(f"verify_three_paths 실패: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def step_prev_day_backtest_smoke() -> None:
|
||||
"""직전 거래일 1일 · 꼬리+돌파 웹 API 경로 (Flask test_client, 서버 불필요)."""
|
||||
from kis_trader.utils.kr_trading_day import clamp_to_prev_kr_trading_day
|
||||
|
||||
day = clamp_to_prev_kr_trading_day(date.today())
|
||||
try:
|
||||
from backtest_web import app
|
||||
|
||||
with app.test_client() as c:
|
||||
for label, path in (
|
||||
("꼬리", "/api/backtest/tail"),
|
||||
("돌파", "/api/backtest/breakout"),
|
||||
):
|
||||
r = c.get(
|
||||
path,
|
||||
query_string={
|
||||
"start": day,
|
||||
"end": day,
|
||||
"universe": "history",
|
||||
},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
_err(f"{label} 백테 HTTP {r.status_code}")
|
||||
continue
|
||||
d = r.get_json(silent=True) or {}
|
||||
if d.get("error"):
|
||||
_err(f"{label} 백테 error: {d.get('error')}")
|
||||
continue
|
||||
s = d.get("summary") or {}
|
||||
_ok(
|
||||
f"{label} 백테 {day}: trades={s.get('total_trades', '?')} "
|
||||
f"pnl={s.get('total_pnl', '?')}"
|
||||
)
|
||||
except Exception as e:
|
||||
_err(f"웹 백테 스모크 실패: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def step_holdings_db() -> None:
|
||||
try:
|
||||
from kis_trader.utils.env import _get_db
|
||||
|
||||
db = _get_db()
|
||||
if not db:
|
||||
_err("TradeDB 연결 실패")
|
||||
return
|
||||
cols = db.conn.execute("SHOW COLUMNS FROM active_trades").fetchall()
|
||||
col_names = [
|
||||
(c["Field"] if isinstance(c, dict) else c[0]) for c in (cols or [])
|
||||
]
|
||||
if "status" in col_names:
|
||||
rows = db.conn.execute(
|
||||
"SELECT strategy, COUNT(*) AS n FROM active_trades "
|
||||
"WHERE status=%s GROUP BY strategy",
|
||||
("HOLDING",),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = db.conn.execute(
|
||||
"SELECT strategy, COUNT(*) AS n FROM active_trades GROUP BY strategy"
|
||||
).fetchall()
|
||||
summary = []
|
||||
for r in rows or []:
|
||||
if isinstance(r, dict):
|
||||
summary.append(f"{r.get('strategy')}={r.get('n')}")
|
||||
else:
|
||||
summary.append(f"{r[0]}={r[1]}")
|
||||
_ok(f"active_trades HOLDING: {', '.join(summary) or '(없음)'}")
|
||||
except Exception as e:
|
||||
_err(f"active_trades 조회 실패: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
log.info("=== market_open_sim_smoke START ===")
|
||||
log.info("log=%s", LOG_PATH)
|
||||
# 실매 FORCE 오염 방지
|
||||
os.environ.pop("FORCE_MARKET_OPEN", None)
|
||||
os.environ.pop("FORCE_BUY_TEST", None)
|
||||
|
||||
try:
|
||||
fake_now = step_calendar()
|
||||
step_strategy_flags()
|
||||
step_market_hours(fake_now)
|
||||
step_engine_defaults()
|
||||
step_holdings_db()
|
||||
step_account_and_open_report()
|
||||
step_verify_three_paths()
|
||||
step_prev_day_backtest_smoke()
|
||||
except Exception as e:
|
||||
_err(f"치명: {e}")
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
log.info("=== SUMMARY ok=%d err=%d ===", len(oks), len(errors))
|
||||
for e in errors:
|
||||
log.info("ERR: %s", e)
|
||||
if errors:
|
||||
log.info("RESULT: FAIL")
|
||||
return 1
|
||||
log.info("RESULT: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
191
scripts/momentum_ratchet_ab_715.py
Normal file
191
scripts/momentum_ratchet_ab_715.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
이전 모멘텀 fine #1 파라미터 고정 × 래칫만 A/B (apply 없음).
|
||||
|
||||
기준 JSON: optuna_momentum_fine_20260716_014654.json (best ~+83k, 래칫 축 없음)
|
||||
기간: 2026-07-15 / 포트 120만 / orderbook off
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP", "1")
|
||||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP_BARS", "700")
|
||||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_SLEEP_SEC", "0.25")
|
||||
|
||||
from kis_trader.backtest.optuna_common import announce_optuna_json_path
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
from kis_trader.utils.env import get_env_from_db
|
||||
|
||||
# 래칫 후보: OFF + 실매 + fine 격자 대표 + 최근 Optuna 선호
|
||||
RATCHET_CASES = [
|
||||
("OFF", ""),
|
||||
("LIVE", "5:2,10:1.5"),
|
||||
("OPTUNA_TOP", "2:1.5,5:1"),
|
||||
("MID", "2:1,5:0.8,8:0.6"),
|
||||
]
|
||||
|
||||
BASE_JSON = os.path.join(
|
||||
ROOT,
|
||||
"kis_trader/backtest/results/optuna_momentum_fine_20260716_014654.json",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(BASE_JSON, encoding="utf-8") as f:
|
||||
src = json.load(f)
|
||||
base_combo = dict(src["results"][0]["params"])
|
||||
start = str(src.get("start") or "2026-07-15")
|
||||
end = str(src.get("end") or start)
|
||||
|
||||
print("=" * 72, flush=True)
|
||||
print("모멘텀 래칫 A/B | 이전 fine#1 고정 | apply 없음", flush=True)
|
||||
print(f"기준 JSON: {BASE_JSON}", flush=True)
|
||||
print(f"기간: {start} ~ {end}", flush=True)
|
||||
print(f"고정 params: {json.dumps(base_combo, ensure_ascii=False)}", flush=True)
|
||||
print(
|
||||
f"DB MOMENTUM_RATCHET_TIERS(참고): {get_env_from_db('MOMENTUM_RATCHET_TIERS', '')!r}",
|
||||
flush=True,
|
||||
)
|
||||
print("=" * 72, flush=True)
|
||||
|
||||
t0 = time.time()
|
||||
ctx = prepare_momentum_search_context(
|
||||
start,
|
||||
end,
|
||||
"fine",
|
||||
orderbook_filter="off",
|
||||
)
|
||||
if ctx is None:
|
||||
print("❌ context 준비 실패", flush=True)
|
||||
return 1
|
||||
print(
|
||||
f"✅ context OK | {time.time() - t0:.1f}s | "
|
||||
f"slot={ctx.slot_money:,.0f} max={ctx.max_stocks} budget={ctx.total_budget_krw:,.0f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
rows = []
|
||||
for label, ratchet in RATCHET_CASES:
|
||||
combo = dict(base_combo)
|
||||
combo["ratchet_tiers"] = ratchet
|
||||
print("-" * 72, flush=True)
|
||||
print(f"▶ 케이스 {label} | ratchet_tiers={ratchet!r}", flush=True)
|
||||
t1 = time.time()
|
||||
result = evaluate_momentum_param_combo(
|
||||
combo,
|
||||
base_fixed=ctx.base_fixed,
|
||||
grid_keys=list(ctx.grid_keys) + ["ratchet_tiers"],
|
||||
codes_candles=ctx.codes_candles,
|
||||
min_trades=1,
|
||||
min_win_rate=0.0,
|
||||
min_pf=0.0,
|
||||
universe_by_slot=ctx.universe_by_slot,
|
||||
slot_money=ctx.slot_money,
|
||||
max_stocks=ctx.max_stocks,
|
||||
total_budget_krw=ctx.total_budget_krw,
|
||||
fee_rate=ctx.fee_rate,
|
||||
sell_tax=ctx.sell_tax,
|
||||
period_days=ctx.period_days,
|
||||
cache_holder=ctx.cache_holder,
|
||||
ticks_by_code=ctx.ticks_by_code,
|
||||
orderbook_by_code=ctx.orderbook_by_code,
|
||||
program_by_code=ctx.program_by_code,
|
||||
log_verdict_by_code=ctx.log_verdict_by_code,
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
)
|
||||
elapsed = time.time() - t1
|
||||
if result is None:
|
||||
row = {
|
||||
"label": label,
|
||||
"ratchet_tiers": ratchet,
|
||||
"ok": False,
|
||||
"elapsed_sec": round(elapsed, 2),
|
||||
"note": "evaluate None",
|
||||
}
|
||||
print(f" ❌ None ({elapsed:.1f}s)", flush=True)
|
||||
else:
|
||||
row = {
|
||||
"label": label,
|
||||
"ratchet_tiers": ratchet,
|
||||
"ok": True,
|
||||
"elapsed_sec": round(elapsed, 2),
|
||||
"total_pnl": float(result.get("total_pnl") or 0),
|
||||
"total_trades": int(result.get("total_trades") or 0),
|
||||
"win_rate": float(result.get("win_rate") or 0),
|
||||
"pf": float(result.get("pf") or 0) if result.get("pf") is not None else None,
|
||||
}
|
||||
print(
|
||||
f" ✅ pnl={row['total_pnl']:,.0f} | trades={row['total_trades']} | "
|
||||
f"wr={row['win_rate']:.1f}% | pf={row['pf']} | {elapsed:.1f}s",
|
||||
flush=True,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
ok_rows = [r for r in rows if r.get("ok")]
|
||||
ok_rows.sort(key=lambda r: (-float(r["total_pnl"]), -int(r["total_trades"])))
|
||||
|
||||
print("=" * 72, flush=True)
|
||||
print("📊 A/B 결과 (PnL 내림차순)", flush=True)
|
||||
for i, r in enumerate(ok_rows, 1):
|
||||
print(
|
||||
f" {i}. [{r['label']}] ratchet={r['ratchet_tiers']!r} | "
|
||||
f"pnl={r['total_pnl']:,.0f} | trades={r['total_trades']} | wr={r['win_rate']:.1f}%",
|
||||
flush=True,
|
||||
)
|
||||
if len(ok_rows) >= 2:
|
||||
best, worst = ok_rows[0], ok_rows[-1]
|
||||
print(
|
||||
f"Δ best−worst = {best['total_pnl'] - worst['total_pnl']:+,.0f}원 "
|
||||
f"({best['label']} vs {worst['label']})",
|
||||
flush=True,
|
||||
)
|
||||
off = next((r for r in ok_rows if r["label"] == "OFF"), None)
|
||||
live = next((r for r in ok_rows if r["label"] == "LIVE"), None)
|
||||
if off and live:
|
||||
print(
|
||||
f"Δ OFF−LIVE = {off['total_pnl'] - live['total_pnl']:+,.0f}원 "
|
||||
f"(OFF {off['total_pnl']:,.0f} / LIVE {live['total_pnl']:,.0f})",
|
||||
flush=True,
|
||||
)
|
||||
print("=" * 72, flush=True)
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_dir = os.path.join(ROOT, "kis_trader/backtest/results")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
out_path = os.path.join(out_dir, f"momentum_ratchet_ab_{ts}.json")
|
||||
out = {
|
||||
"kind": "momentum_ratchet_ab",
|
||||
"apply": False,
|
||||
"base_json": BASE_JSON,
|
||||
"base_pnl_reported": src["results"][0].get("total_pnl"),
|
||||
"base_params": base_combo,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"slot_money": int(ctx.slot_money),
|
||||
"max_stocks": int(ctx.max_stocks),
|
||||
"total_budget_krw": int(ctx.total_budget_krw),
|
||||
"cases": rows,
|
||||
"ranked": ok_rows,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
}
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2, ensure_ascii=False)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="momentum", mode="ratchet_ab", note="래칫 A/B 최종 JSON",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
113
scripts/smoke_candle_upsert_rollup.py
Normal file
113
scripts/smoke_candle_upsert_rollup.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
스모크: 1M→N분 완전버킷 롤업 + confirm/merge volume upsert.
|
||||
|
||||
근본원인(2026-07-16 샘표): 불완전 롤업 삽입 + 동일 candle_time append 중복
|
||||
→ RAM prior volume 왜곡 → 실매 vol 통과 / 백테 탈락.
|
||||
|
||||
실행:
|
||||
python3 -u scripts/smoke_candle_upsert_rollup.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from kis_trader.engine.candle_rollup import floor_candle_time_to_tf, rollup_1m_bars_to_tf
|
||||
from kis_trader.ws.kis_ws import CandleAggregator
|
||||
|
||||
|
||||
def bar(ct, o, h, l, c, v, src="ws"):
|
||||
return {
|
||||
"candle_time": ct,
|
||||
"open": o,
|
||||
"high": h,
|
||||
"low": l,
|
||||
"close": c,
|
||||
"volume": v,
|
||||
"source": src,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
assert floor_candle_time_to_tf("202607160912", 3) == "202607160912"
|
||||
assert floor_candle_time_to_tf("202607160913", 3) == "202607160912"
|
||||
assert floor_candle_time_to_tf("202607160914", 3) == "202607160912"
|
||||
|
||||
partial = [
|
||||
bar("202607160912", 100, 101, 99, 100, 100),
|
||||
bar("202607160913", 100, 102, 99, 101, 200),
|
||||
]
|
||||
assert rollup_1m_bars_to_tf(partial, 3) == []
|
||||
|
||||
full = partial + [bar("202607160914", 101, 110, 100, 105, 4226)]
|
||||
rolled = rollup_1m_bars_to_tf(full, 3)
|
||||
assert len(rolled) == 1
|
||||
assert rolled[0]["candle_time"] == "202607160912"
|
||||
assert rolled[0]["volume"] == 100 + 200 + 4226
|
||||
|
||||
more = full + [
|
||||
bar("202607160915", 105, 106, 104, 105, 50),
|
||||
bar("202607160916", 105, 107, 104, 106, 60),
|
||||
]
|
||||
assert len(rollup_1m_bars_to_tf(more, 3)) == 1
|
||||
|
||||
agg = CandleAggregator(db=None, timeframes=[1, 3])
|
||||
code = "007540"
|
||||
assert agg.merge_confirmed_bars(
|
||||
code, 3,
|
||||
[bar("202607160912", 43000, 44000, 42000, 43500, 515, "rollup_1m")],
|
||||
log_tag="smoke_partial",
|
||||
) == 1
|
||||
assert agg.merge_confirmed_bars(
|
||||
code, 3,
|
||||
[bar("202607160912", 43000, 44500, 42000, 43800, 4526, "rest")],
|
||||
log_tag="smoke_full",
|
||||
) == 1
|
||||
buf = agg._confirmed[(code, 3)]
|
||||
assert len(buf) == 1 and buf[0]["volume"] == 4526
|
||||
assert agg.merge_confirmed_bars(
|
||||
code, 3,
|
||||
[bar("202607160912", 43000, 44000, 42000, 43700, 100, "ws")],
|
||||
log_tag="smoke_small",
|
||||
) == 0
|
||||
assert buf[0]["volume"] == 4526
|
||||
|
||||
agg2 = CandleAggregator(db=None, timeframes=[3])
|
||||
key = (code, 3)
|
||||
agg2.merge_confirmed_bars(
|
||||
code, 3,
|
||||
[bar("202607160912", 43000, 44000, 42000, 43500, 515, "rollup_1m")],
|
||||
)
|
||||
with agg2._lock:
|
||||
confirmed = agg2._confirm_current_bucket(key, {
|
||||
"candle_time": "202607160912",
|
||||
"open": 43000,
|
||||
"high": 44200,
|
||||
"low": 42000,
|
||||
"close": 43600,
|
||||
"volume": 800,
|
||||
"source": "ws",
|
||||
})
|
||||
assert len(agg2._confirmed[key]) == 1
|
||||
assert confirmed["volume"] == 800
|
||||
with agg2._lock:
|
||||
agg2._confirm_current_bucket(key, {
|
||||
"candle_time": "202607160912",
|
||||
"open": 43000,
|
||||
"high": 44100,
|
||||
"low": 42000,
|
||||
"close": 43400,
|
||||
"volume": 100,
|
||||
"source": "ws",
|
||||
})
|
||||
assert agg2._confirmed[key][0]["volume"] == 800
|
||||
|
||||
print("SMOKE_OK candle_upsert_rollup")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
158
scripts/tail_live_bt_forensics.py
Normal file
158
scripts/tail_live_bt_forensics.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
꼬리 실매 vs 백테 건별 forensics (C).
|
||||
|
||||
실매 trade_history(SHORT) 각 건에 대해:
|
||||
유니버스 IN/OUT · 재편입 · 당일봉 entry_i · 웜업 후 align 신호 · 백테 체결 여부
|
||||
|
||||
사용:
|
||||
python3 -u scripts/tail_live_bt_forensics.py --date 2026-07-16
|
||||
nohup python3 -u scripts/tail_live_bt_forensics.py --date 2026-07-16 \
|
||||
> logs/tail_live_bt_forensics_20260716.log 2>&1 &
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--date", default="2026-07-16", help="YYYY-MM-DD")
|
||||
args = ap.parse_args()
|
||||
day = args.date.replace("-", "")
|
||||
day_dash = f"{day[:4]}-{day[4:6]}-{day[6:8]}"
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.engine import tail_engine as te
|
||||
from kis_trader.engine.tail_engine import (
|
||||
_eval_live_align_lookback,
|
||||
_last_closed_bar_index,
|
||||
_universe_enter_minutes,
|
||||
)
|
||||
from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.backtest.universe_timeline import build_universe_timeline
|
||||
|
||||
db = TradeDB()
|
||||
live = db.conn.execute(
|
||||
"SELECT code, name, buy_date, buy_price, sell_date, realized_pnl "
|
||||
"FROM trade_history WHERE strategy=%s AND buy_date LIKE %s "
|
||||
"ORDER BY buy_date",
|
||||
("SHORT", f"{day_dash}%"),
|
||||
).fetchall()
|
||||
print(f"=== 꼬리 forensics {day_dash} live={len(live)} ===")
|
||||
|
||||
base = te.get_tail_defaults_from_db(db)
|
||||
universe, src, n_slots, _ = tbc.resolve_tail_universe(
|
||||
day, day, use_saved_history=True, strategy_id="SHORT",
|
||||
)
|
||||
tl = build_universe_timeline(
|
||||
strategy_id="SHORT", start_ymd=day, end_ymd=day,
|
||||
debounce_sec=0, strict=False,
|
||||
)
|
||||
start_key, end_key = day + "0000", day + "2359"
|
||||
candles_by_code, _, _ = tbc.load_tail_candles_by_code(
|
||||
db, start_key, end_key, int(base.get("timeframe") or 3),
|
||||
rsi_period=int(base.get("rsi_period") or 14),
|
||||
)
|
||||
# REST 웜업 (유니버스 교집합)
|
||||
tbc.inject_tail_rest_warmup_memory(
|
||||
candles_by_code, start_key,
|
||||
timeframe=int(base.get("timeframe") or 3),
|
||||
universe_by_slot=universe,
|
||||
)
|
||||
|
||||
port = tbc.resolve_tail_portfolio_params(
|
||||
dict(db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone() or {}),
|
||||
base,
|
||||
)
|
||||
row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone()
|
||||
fee, tax, _ = tbc.fee_and_slot_from_env_row(dict(row) if row else None)
|
||||
params = dict(base)
|
||||
tbc.merge_tail_portfolio_into_params(params, port)
|
||||
meta = {"db": db, "start_key": start_key, "end_key": end_key}
|
||||
bt_trades = tbc.run_tail_backtest_web_aligned(
|
||||
candles_by_code, params, universe,
|
||||
slot_money=float(port["slot_money"]),
|
||||
fee_rate=fee, sell_tax=tax,
|
||||
total_budget_krw=float(port["total_budget_krw"]),
|
||||
meta_out=meta,
|
||||
)
|
||||
bt_by_code = {}
|
||||
for t in bt_trades:
|
||||
bt_by_code.setdefault(str(t.get("code")), []).append(t)
|
||||
|
||||
enter_mins = _universe_enter_minutes(universe, tl, None)
|
||||
print(f"universe src={src} slots={n_slots} enter_minutes={len(enter_mins)}")
|
||||
print(f"warmup bars target={tbc.tail_backtest_candle_warmup_bars()} "
|
||||
f"rest={meta.get('skip_stats', {}).get('rest_warmup')}")
|
||||
print(f"BT trades={len(bt_trades)} pnl={sum(int(t.get('pnl') or 0) for t in bt_trades)}")
|
||||
print()
|
||||
|
||||
for r in live:
|
||||
code = str(r["code"])
|
||||
buy_ts = str(r["buy_date"])
|
||||
buy_hm = buy_ts[11:16].replace(":", "")
|
||||
t12 = day + buy_hm
|
||||
name = r.get("name") or code
|
||||
print(f"── {code} {name} live {buy_ts} @{int(r['buy_price'])} pnl={r['realized_pnl']}")
|
||||
|
||||
# transitions that day
|
||||
prev = False
|
||||
trans = []
|
||||
for et_row in db.conn.execute(
|
||||
"""SELECT event_time, MAX(code=%s) has_me
|
||||
FROM target_candidates_history
|
||||
WHERE strategy_id=%s AND event_time LIKE %s
|
||||
GROUP BY event_time ORDER BY event_time""",
|
||||
(code, "SHORT", f"{day_dash}%"),
|
||||
).fetchall():
|
||||
has = bool(et_row["has_me"])
|
||||
if has != prev:
|
||||
trans.append((str(et_row["event_time"]), "IN" if has else "OUT"))
|
||||
prev = has
|
||||
print(f" transitions: {trans[:8]}{'...' if len(trans) > 8 else ''}")
|
||||
|
||||
in_at_buy = False
|
||||
if tl is not None:
|
||||
codes = tl.codes_at(t12 + "00") or []
|
||||
in_at_buy = code in codes
|
||||
print(f" universe@buy {t12}: {'IN' if in_at_buy else 'OUT'}")
|
||||
|
||||
bars = candles_by_code.get(code) or []
|
||||
n_prev = sum(1 for c in bars if str(c.get("candle_time") or "")[:8] < day)
|
||||
ei = _last_closed_bar_index(bars, t12, int(base.get("timeframe") or 3))
|
||||
print(f" candles n={len(bars)} prev_day={n_prev} entry_i@buy={ei}",
|
||||
f"bar={bars[ei]['candle_time'] if ei >= 0 else None}")
|
||||
|
||||
st = {"daily_cnt": 0, "last_exit_dt": None, "daily_pnl_krw": 0.0}
|
||||
if ei >= 19:
|
||||
rej, msg, sig = _eval_live_align_lookback(
|
||||
bars, ei, params, st,
|
||||
lookback=max(1, int(params.get("live_signal_lookback_bars") or 1)),
|
||||
)
|
||||
print(f" align: reject={rej} msg={(msg or '')[:70]} "
|
||||
f"sig={bool(sig)} px={sig.get('entry_price') if sig else None}")
|
||||
else:
|
||||
print(f" align: SKIP entry_i={ei} < 19 (웜업 부족)")
|
||||
|
||||
hits = bt_by_code.get(code) or []
|
||||
if hits:
|
||||
for h in hits:
|
||||
print(f" BT hit: {h.get('entry_time')} @{h.get('entry')} "
|
||||
f"→ {h.get('exit_time')} pnl={h.get('pnl')}")
|
||||
else:
|
||||
print(" BT hit: NONE")
|
||||
print()
|
||||
|
||||
db.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
126
scripts/tail_symbol_gate_verify_20260709.py
Normal file
126
scripts/tail_symbol_gate_verify_20260709.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""7/9 SHORT 종목일일손익게이트 백테 검증 — Case A(edge=2000) vs B(edge=0)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from database import TradeDB
|
||||
from kis_trader.engine import tail_engine as te
|
||||
from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.engine.tail_tick_replay import tail_backtest_wants_tick_replay
|
||||
|
||||
|
||||
def _summarize(trades, label: str) -> dict:
|
||||
total = len(trades)
|
||||
wins = [t for t in trades if float(t.get("pnl") or 0) > 0]
|
||||
pnl = sum(float(t.get("pnl") or 0) for t in trades)
|
||||
by_code: dict = defaultdict(list)
|
||||
for t in trades:
|
||||
by_code[t.get("code")].append(t)
|
||||
multi = sum(1 for v in by_code.values() if len(v) > 1)
|
||||
wr = (len(wins) / total * 100) if total else 0.0
|
||||
print(
|
||||
f" trades={total} wins={len(wins)} WR={wr:.1f}% "
|
||||
f"pnl={pnl:+,.0f} multi_codes={multi}",
|
||||
flush=True,
|
||||
)
|
||||
for code, ts in sorted(by_code.items()):
|
||||
cum = 0.0
|
||||
parts = []
|
||||
for t in ts:
|
||||
cum += float(t.get("pnl") or 0)
|
||||
hm = str(t.get("entry_time") or t.get("candle_time") or "")[8:12]
|
||||
parts.append(
|
||||
f"{hm}:{t.get('pnl', 0):+.0f}({t.get('exit_reason', '')})→cum{cum:+.0f}"
|
||||
)
|
||||
if len(ts) > 1:
|
||||
print(f" [{code}] " + " | ".join(parts), flush=True)
|
||||
return {"label": label, "trades": total, "pnl": pnl, "multi": multi}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("=== 7/9 SHORT 게이트 백테 검증 (skip_hts=DB) ===", flush=True)
|
||||
db = TradeDB()
|
||||
start, end = "2026-07-09", "2026-07-09"
|
||||
start_key = start.replace("-", "") + "0000"
|
||||
end_key = end.replace("-", "") + "2359"
|
||||
start_ymd, end_ymd = start_key[:8], end_key[:8]
|
||||
|
||||
try:
|
||||
base = te.get_tail_defaults_from_db(db)
|
||||
skip_hts = bool(base.get("skip_hts_scan_dupes"))
|
||||
print(f"skip_hts_scan_dupes(DB)={skip_hts}", flush=True)
|
||||
|
||||
universe, src, n_slots, _ = tbc.resolve_tail_universe(
|
||||
start_ymd, end_ymd, use_saved_history=True, strategy_id="SHORT",
|
||||
)
|
||||
tf = int(base.get("timeframe") or 3)
|
||||
rsi = int(base.get("rsi_period") or 14)
|
||||
candles_by_code, _, _ = tbc.load_tail_candles_by_code(
|
||||
db, start_key, end_key, tf, rsi_period=rsi,
|
||||
)
|
||||
codes = len(candles_by_code)
|
||||
bars = sum(len(v) for v in candles_by_code.values())
|
||||
slot = float(base.get("slot_money") or 300000)
|
||||
budget = float(base.get("total_budget_krw") or slot * int(base.get("max_stocks") or 4))
|
||||
print(
|
||||
f"[로드] universe={src} slots={n_slots} codes={codes} bars={bars} "
|
||||
f"slot={slot} budget={budget}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
fee = float(base.get("fee_rate") or 0.00015)
|
||||
tax = float(base.get("sell_tax") or 0.0023)
|
||||
meta = {"db": db, "start_key": start_key, "end_key": end_key}
|
||||
use_tick = tail_backtest_wants_tick_replay(base)
|
||||
print(f"[로드] tick_replay={use_tick}", flush=True)
|
||||
|
||||
cases = [
|
||||
("A GATE_ON edge=2000", 30000.0, 1.5, 2000.0),
|
||||
("B GATE_ON edge=0", 30000.0, 1.5, 0.0),
|
||||
]
|
||||
results = []
|
||||
for label, krw, pct, edge in cases:
|
||||
params = dict(base)
|
||||
params["symbol_daily_loss_limit_krw"] = krw
|
||||
params["symbol_daily_loss_limit_pct"] = pct
|
||||
params["reentry_min_edge_krw"] = edge
|
||||
params["skip_hts_scan_dupes"] = skip_hts
|
||||
print(
|
||||
f"\n--- {label}: krw={krw} pct={pct} edge={edge} ---",
|
||||
flush=True,
|
||||
)
|
||||
trades = tbc.run_tail_backtest_web_aligned(
|
||||
candles_by_code,
|
||||
params,
|
||||
universe,
|
||||
slot_money=slot,
|
||||
fee_rate=fee,
|
||||
sell_tax=tax,
|
||||
total_budget_krw=budget,
|
||||
meta_out=meta,
|
||||
)
|
||||
results.append(_summarize(trades, label))
|
||||
|
||||
print("\n======== SUMMARY ========", flush=True)
|
||||
for r in results:
|
||||
print(f" {r['label']}: trades={r['trades']} pnl={r['pnl']:+,.0f} multi={r['multi']}", flush=True)
|
||||
print("\n[LIVE 7/9] trades=3(신규2) pnl=-7,053 (376980 전일포지션 포함)", flush=True)
|
||||
print("✅ VERIFY DONE", flush=True)
|
||||
return 0
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user