refactor: reorganize project structure into tests/, data/, logs/
- Move all backtest/simulation scripts to tests/ - Add sys.path.insert to each script for correct import resolution - Move pkl cache files to data/ (git-ignored) - Move log files to logs/ (git-ignored) - Update main.py: trading.log path → logs/trading.log - Add ecosystem.config.js: pm2 log paths → logs/pm2*.log - Update .gitignore: ignore data/ and logs/ instead of *.pkl/*.log - core/fng.py: increase cache TTL 3600→86400s (API updates daily at KST 09:00) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
189
tests/momentum_cmp.py
Normal file
189
tests/momentum_cmp.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""모멘텀 필터 유/무 비교 시뮬레이션.
|
||||
|
||||
A안: 추세(2h +5%) + 15분 워치리스트 (모멘텀 없음)
|
||||
B안: 추세(2h +5%) + 모멘텀 + 15분 워치리스트 (현행)
|
||||
"""
|
||||
|
||||
import os as _os, sys as _sys
|
||||
_sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))
|
||||
|
||||
import os, time
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
import oracledb
|
||||
import pyupbit
|
||||
|
||||
STOP_LOSS_PCT = float(os.getenv("STOP_LOSS_PCT", "1.5")) / 100
|
||||
TIME_STOP_HOURS = int(os.getenv("TIME_STOP_HOURS", "8"))
|
||||
TIME_STOP_MIN_PCT = float(os.getenv("TIME_STOP_MIN_GAIN_PCT", "3")) / 100
|
||||
TREND_MIN_PCT = 5.0
|
||||
MA_PERIOD = 20
|
||||
LOCAL_VOL_HOURS = 5
|
||||
VOL_MULT = float(os.getenv("VOLUME_MULTIPLIER", "2.0"))
|
||||
CONFIRM_MINUTES = 15
|
||||
FEE = 0.0005
|
||||
|
||||
_daily_cache = {}
|
||||
_hourly_cache = {}
|
||||
|
||||
def get_conn():
|
||||
return oracledb.connect(
|
||||
user=os.getenv('ORACLE_USER'), password=os.getenv('ORACLE_PASSWORD'),
|
||||
dsn=os.getenv('ORACLE_DSN'), config_dir=os.getenv('ORACLE_WALLET'))
|
||||
|
||||
def load_prices(cur, ticker, from_dt):
|
||||
cur.execute("""SELECT price, recorded_at FROM price_history
|
||||
WHERE ticker=:t AND recorded_at>=:f ORDER BY recorded_at""", t=ticker, f=from_dt)
|
||||
return cur.fetchall()
|
||||
|
||||
def get_ma20(ticker, dt):
|
||||
key = (ticker, dt.strftime("%Y-%m-%d"))
|
||||
if key not in _daily_cache:
|
||||
try:
|
||||
df = pyupbit.get_ohlcv(ticker, interval="day", count=MA_PERIOD+2,
|
||||
to=dt.strftime("%Y-%m-%d 09:00:00"))
|
||||
_daily_cache[key] = df
|
||||
time.sleep(0.1)
|
||||
except:
|
||||
_daily_cache[key] = None
|
||||
df = _daily_cache[key]
|
||||
if df is None or len(df) < MA_PERIOD:
|
||||
return None
|
||||
return df["close"].iloc[-MA_PERIOD:].mean()
|
||||
|
||||
def get_vol_ratio(ticker, dt):
|
||||
key = (ticker, dt.strftime("%Y-%m-%d %H"))
|
||||
if key not in _hourly_cache:
|
||||
try:
|
||||
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=LOCAL_VOL_HOURS+3,
|
||||
to=dt.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
_hourly_cache[key] = df
|
||||
time.sleep(0.1)
|
||||
except:
|
||||
_hourly_cache[key] = None
|
||||
df = _hourly_cache[key]
|
||||
if df is None or len(df) < LOCAL_VOL_HOURS+1:
|
||||
return 0.0
|
||||
rv = df["volume"].iloc[-2]
|
||||
la = df["volume"].iloc[-(LOCAL_VOL_HOURS+1):-2].mean()
|
||||
return rv/la if la > 0 else 0.0
|
||||
|
||||
def check_trend(prices, idx):
|
||||
lb = 12 # 2h = 12 * 10min
|
||||
if idx < lb: return False
|
||||
curr, past = prices[idx][0], prices[idx-lb][0]
|
||||
return past > 0 and (curr-past)/past*100 >= TREND_MIN_PCT
|
||||
|
||||
def check_momentum(ticker, price, dt):
|
||||
ma = get_ma20(ticker, dt)
|
||||
if ma is None or price <= ma: return False
|
||||
return get_vol_ratio(ticker, dt) >= VOL_MULT
|
||||
|
||||
def simulate_pos(prices, buy_idx, buy_price):
|
||||
buy_dt = prices[buy_idx][1]
|
||||
peak = buy_price
|
||||
for price, ts in prices[buy_idx+1:]:
|
||||
if price > peak: peak = price
|
||||
elapsed_h = (ts - buy_dt).total_seconds() / 3600
|
||||
pnl = (price - buy_price) / buy_price
|
||||
if (peak - price) / peak >= STOP_LOSS_PCT:
|
||||
net = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return net > 0, price, ts, f"트레일링({pnl*100:+.1f}%)", net
|
||||
if elapsed_h >= TIME_STOP_HOURS and pnl < TIME_STOP_MIN_PCT:
|
||||
net = (price*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return net > 0, price, ts, f"타임스탑", net
|
||||
lp, lt = prices[-1]
|
||||
net = (lp*(1-FEE) - buy_price*(1+FEE)) / (buy_price*(1+FEE)) * 100
|
||||
return net > 0, lp, lt, "데이터종료", net
|
||||
|
||||
def run_scenario(prices, ticker, use_momentum, label):
|
||||
wins = losses = 0
|
||||
total_pnl = 0.0
|
||||
watchlist_dt = None
|
||||
in_pos = False
|
||||
buy_idx = buy_price = None
|
||||
idx = 0
|
||||
trades = []
|
||||
|
||||
while idx < len(prices):
|
||||
price, dt = prices[idx]
|
||||
|
||||
if in_pos:
|
||||
is_win, sp, sdt, reason, pnl = simulate_pos(prices, buy_idx, buy_price)
|
||||
next_idx = next((i for i,(_, ts) in enumerate(prices) if ts > sdt), len(prices))
|
||||
if is_win: wins += 1
|
||||
else: losses += 1
|
||||
total_pnl += pnl
|
||||
trades.append((is_win, buy_price, sp, pnl, dt, sdt, reason))
|
||||
in_pos = False
|
||||
watchlist_dt = None
|
||||
idx = next_idx
|
||||
continue
|
||||
|
||||
trend_ok = check_trend(prices, idx)
|
||||
mom_ok = check_momentum(ticker, price, dt) if (use_momentum and trend_ok) else True
|
||||
|
||||
if trend_ok and mom_ok:
|
||||
if watchlist_dt is None:
|
||||
watchlist_dt = dt
|
||||
elif (dt - watchlist_dt).total_seconds() >= CONFIRM_MINUTES * 60:
|
||||
in_pos = True
|
||||
buy_idx = idx
|
||||
buy_price = price
|
||||
watchlist_dt = None
|
||||
else:
|
||||
watchlist_dt = None
|
||||
|
||||
idx += 1
|
||||
|
||||
total = wins + losses
|
||||
wr = wins/total*100 if total else 0
|
||||
return {'label': label, 'total': total, 'wins': wins, 'losses': losses,
|
||||
'wr': wr, 'pnl': total_pnl, 'trades': trades}
|
||||
|
||||
def print_result(r):
|
||||
print(f"\n [{r['label']}]")
|
||||
print(f" 총 {r['total']}건 | 승률={r['wr']:.0f}% ({r['wins']}승 {r['losses']}패) | 누적={r['pnl']:+.2f}%")
|
||||
for i, (iw, bp, sp, pnl, bdt, sdt, reason) in enumerate(r['trades'], 1):
|
||||
mark = "✅" if iw else "❌"
|
||||
print(f" #{i}: {bp:.4f}→{sp:.4f}원 | {mark} {pnl:+.2f}% | {reason}"
|
||||
f" ({bdt.strftime('%m-%d %H:%M')}→{sdt.strftime('%m-%d %H:%M')})")
|
||||
|
||||
def main():
|
||||
conn = get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT MAX(recorded_at) FROM price_history")
|
||||
end_dt = cur.fetchone()[0]
|
||||
|
||||
print("=" * 62)
|
||||
print("모멘텀 필터 유/무 비교 (WF차단 발동 이후 전 기간)")
|
||||
print("A안: 추세+워치리스트만 B안: 추세+모멘텀+워치리스트(현행)")
|
||||
print("=" * 62)
|
||||
|
||||
summary = []
|
||||
for ticker in ['KRW-DKA', 'KRW-LAYER', 'KRW-SIGN']:
|
||||
cur.execute("SELECT traded_at FROM trade_results WHERE ticker=:t ORDER BY traded_at", t=ticker)
|
||||
rows = cur.fetchall()
|
||||
wf_dt = rows[4][0]
|
||||
prices = load_prices(cur, ticker, wf_dt)
|
||||
|
||||
print(f"\n{'─'*62}")
|
||||
print(f"[{ticker}] WF차단: {wf_dt.strftime('%m-%d %H:%M')} 데이터: {len(prices)}틱")
|
||||
|
||||
rA = run_scenario(prices, ticker, use_momentum=False, label="A: 추세+워치만")
|
||||
rB = run_scenario(prices, ticker, use_momentum=True, label="B: 추세+모멘텀+워치(현행)")
|
||||
print_result(rA)
|
||||
print_result(rB)
|
||||
summary.append((ticker, rA, rB))
|
||||
|
||||
print(f"\n{'='*62}")
|
||||
print(f"{'종목':<12} {'A안 거래':>6} {'A안 승률':>8} {'A안 PnL':>10} │ {'B안 거래':>6} {'B안 승률':>8} {'B안 PnL':>10}")
|
||||
print(f"{'─'*62}")
|
||||
for ticker, rA, rB in summary:
|
||||
print(f"{ticker:<12} {rA['total']:>6}건 {rA['wr']:>6.0f}% {rA['pnl']:>+9.2f}% │"
|
||||
f" {rB['total']:>6}건 {rB['wr']:>6.0f}% {rB['pnl']:>+9.2f}%")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user