커서가 망쳐놓은 듯
This commit is contained in:
210
backtest_scalping/holding_param_search.py
Normal file
210
backtest_scalping/holding_param_search.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
holding_param_search.py — 홀딩 전략 파라미터 자동 탐색 (종목별 Grid Search)
|
||||
=============================================================================
|
||||
실행:
|
||||
cd /home/hoon/kis_bot
|
||||
python3 backtest_scalping/holding_param_search.py
|
||||
|
||||
옵션:
|
||||
--code 종목코드 (미지정 시 관심종목 전체)
|
||||
--start 시작일 (기본: 2년 전)
|
||||
--end 종료일 (기본: 오늘)
|
||||
--mode coarse(기본) / fine / full
|
||||
--top 상위 N개 출력 (기본: 10)
|
||||
--apply 1위 파라미터를 DB에 자동 적용
|
||||
|
||||
예시:
|
||||
python3 backtest_scalping/holding_param_search.py --code 005930 --apply
|
||||
python3 backtest_scalping/holding_param_search.py --mode fine --apply
|
||||
"""
|
||||
|
||||
import sys, os, json, time, argparse
|
||||
from datetime import datetime, timedelta
|
||||
from itertools import product as iproduct
|
||||
|
||||
import logging
|
||||
logging.getLogger("TradeDB").setLevel(logging.WARNING)
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from database import TradeDB
|
||||
import holding_bot as hb
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 그리드 정의
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
GRIDS = {
|
||||
# 빠른 1차 탐색 (~270 조합)
|
||||
"coarse": {
|
||||
"rsi_buy1": [40, 35, 30],
|
||||
"rsi_buy2": [35, 30, 25],
|
||||
"rsi_buy3": [30, 25, 20],
|
||||
"take_profit_pct": [3.0, 4.0, 5.0, 6.0, 8.0],
|
||||
"stop_loss_pct": [7.0, 10.0, 15.0],
|
||||
},
|
||||
# 세밀 2차 탐색 (~2,400 조합)
|
||||
"fine": {
|
||||
"rsi_buy1": [45, 40, 35, 30],
|
||||
"rsi_buy2": [40, 35, 30, 25],
|
||||
"rsi_buy3": [35, 30, 25, 20],
|
||||
"take_profit_pct": [2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0],
|
||||
"stop_loss_pct": [5.0, 7.0, 10.0, 15.0, 20.0],
|
||||
"rsi_sell": [65, 70, 75],
|
||||
},
|
||||
# 전체 탐색 (~10,000+ 조합)
|
||||
"full": {
|
||||
"rsi_buy1": [50, 45, 40, 35, 30],
|
||||
"rsi_buy2": [45, 40, 35, 30, 25],
|
||||
"rsi_buy3": [40, 35, 30, 25, 20, 15],
|
||||
"take_profit_pct": [2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 12.0],
|
||||
"stop_loss_pct": [5.0, 7.0, 10.0, 15.0, 20.0],
|
||||
"rsi_sell": [60, 65, 70, 75, 80],
|
||||
"rsi_period": [7, 14, 21],
|
||||
},
|
||||
}
|
||||
|
||||
FIXED_DEFAULTS = {
|
||||
"rsi_sell": 70,
|
||||
"rsi_period": 14,
|
||||
"buy1_ratio": 30,
|
||||
"buy2_ratio": 30,
|
||||
"buy3_ratio": 40,
|
||||
"slot_money": 3_000_000,
|
||||
}
|
||||
|
||||
|
||||
def run_search(code: str, name: str, candles, mode: str,
|
||||
top_n: int, min_trades: int, apply_best: bool, db):
|
||||
grid = GRIDS[mode]
|
||||
keys = list(grid.keys())
|
||||
combos = list(iproduct(*[grid[k] for k in keys]))
|
||||
valid = [(zip(keys, v)) for v in combos]
|
||||
|
||||
results = []
|
||||
base_cfg = dict(hb.DEFAULT_STOCK_CONFIG)
|
||||
base_cfg.update(FIXED_DEFAULTS)
|
||||
|
||||
t0 = time.time()
|
||||
total = len(combos)
|
||||
print(f"\n[{code}] {name} | {mode.upper()} | 조합: {total:,}개")
|
||||
print("=" * 60)
|
||||
|
||||
for i, vals in enumerate(combos):
|
||||
cfg = dict(base_cfg)
|
||||
cfg.update(dict(zip(keys, vals)))
|
||||
# rsi_buy 순서 보정
|
||||
if cfg["rsi_buy1"] <= cfg["rsi_buy2"] or cfg["rsi_buy2"] <= cfg["rsi_buy3"]:
|
||||
continue
|
||||
|
||||
res = hb.run_backtest(candles, cfg)
|
||||
s = res.get("summary", {})
|
||||
if s.get("total_trades", 0) < min_trades:
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"params": {k: cfg[k] for k in keys},
|
||||
"total_pnl": s["total_pnl"],
|
||||
"win_rate": s["win_rate"],
|
||||
"total_trades": s["total_trades"],
|
||||
"pf": s["profit_factor"],
|
||||
"avg_hold": s["avg_hold_days"],
|
||||
"mdd": s["max_drawdown"],
|
||||
})
|
||||
|
||||
elapsed = time.time() - t0
|
||||
print(f"완료: {elapsed:.1f}초 | 유효: {len(results)}건")
|
||||
|
||||
if not results:
|
||||
print("⚠️ 유효 결과 없음. 기간을 늘리거나 min_trades를 낮추세요.")
|
||||
return
|
||||
|
||||
results.sort(key=lambda x: x["total_pnl"], reverse=True)
|
||||
|
||||
col_w = max(len(k) for k in keys) + 2
|
||||
hdr = " ".join(f"{k:>{col_w}}" for k in keys)
|
||||
print(f"\n{'='*60}")
|
||||
print(f" TOP {min(top_n, len(results))}")
|
||||
print(f"{'='*60}")
|
||||
print(f"{hdr} | {'손익':>12} {'승률':>6} {'거래':>5} {'PF':>5} {'보유(일)':>8}")
|
||||
print("-" * (len(hdr) + 50))
|
||||
for r in results[:top_n]:
|
||||
p = r["params"]
|
||||
row = " ".join(f"{p[k]:>{col_w}.4g}" for k in keys)
|
||||
print(f"{row} | {r['total_pnl']:>+12,.0f} {r['win_rate']:>5.1f}% "
|
||||
f"{r['total_trades']:>5} {r['pf']:>5.2f} {r['avg_hold']:>7.1f}일")
|
||||
|
||||
best = results[0]
|
||||
bp = best["params"]
|
||||
print(f"""
|
||||
╔═══════════════════════════════════════════╗
|
||||
║ 🏆 [{code}] {name[:10]} 최적 파라미터
|
||||
╠═══════════════════════════════════════════╣""")
|
||||
for k, v in bp.items():
|
||||
print(f"║ {k:<22s} : {v!s:>8} ║")
|
||||
print(f"""╠═══════════════════════════════════════════╣
|
||||
║ 총 손익 : {best['total_pnl']:>+12,.0f} 원 ║
|
||||
║ 승률 : {best['win_rate']:>6.1f}% ║
|
||||
║ 거래 : {best['total_trades']:>5} 건 ║
|
||||
║ PF : {best['pf']:>5.2f} ║
|
||||
║ 보유(일) : {best['avg_hold']:>5.1f} 일 ║
|
||||
║ MDD : {best['mdd']:>12,.0f} 원 ║
|
||||
╚═══════════════════════════════════════════╝""")
|
||||
|
||||
# 결과 저장
|
||||
out_dir = os.path.join(ROOT, "backtest_scalping", "results")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_path = os.path.join(out_dir, f"holding_{code}_{mode}_{ts}.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"code": code, "name": name, "mode": mode,
|
||||
"top": results[:top_n]}, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n💾 결과 저장: {out_path}")
|
||||
|
||||
if apply_best:
|
||||
hb.set_stock_config(db, code, name, bp)
|
||||
print(f"✅ DB 파라미터 적용 완료 ({code})")
|
||||
|
||||
|
||||
def main():
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
two_ago = (datetime.now() - timedelta(days=730)).strftime("%Y-%m-%d")
|
||||
|
||||
parser = argparse.ArgumentParser(description="홀딩 전략 파라미터 Grid Search")
|
||||
parser.add_argument("--code", default="", help="종목코드 (공백=관심종목 전체)")
|
||||
parser.add_argument("--start", default=two_ago, help="시작일")
|
||||
parser.add_argument("--end", default=today, help="종료일")
|
||||
parser.add_argument("--mode", default="coarse", choices=["coarse", "fine", "full"])
|
||||
parser.add_argument("--top", default=10, type=int)
|
||||
parser.add_argument("--min_trades", default=2, type=int)
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
db = TradeDB()
|
||||
hb.ensure_holding_tables(db)
|
||||
|
||||
items = hb.load_watchlist()
|
||||
if args.code:
|
||||
items = [i for i in items if i["code"] == args.code]
|
||||
if not items:
|
||||
print(f"종목을 찾을 수 없습니다: {args.code}")
|
||||
db.close()
|
||||
return
|
||||
|
||||
for item in items:
|
||||
code = item["code"]
|
||||
name = item["name"]
|
||||
candles = hb.get_stored_candles(db, code, args.start, args.end)
|
||||
if len(candles) < 20:
|
||||
print(f"⚠️ [{code}] {name}: 봉 부족 ({len(candles)}개) → 스킵. 먼저 캔들을 수집하세요.")
|
||||
print(f" 실행: python3 holding_bot.py fetch --start {args.start} --end {args.end}")
|
||||
continue
|
||||
run_search(code, name, candles, args.mode,
|
||||
args.top, args.min_trades, args.apply, db)
|
||||
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user