diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 10:13:10 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2026-04-02 10:13:10 +0900 |
| commit | e9c791ae2b14884f8f0525da5fcaa1710ca1fc63 (patch) | |
| tree | 752c219a95abe7bdbfbf0644711f7dede407aa7b /scripts | |
| parent | 35120795147adf53de59b7f2a3c8aa14adec9a56 (diff) | |
refactor: complete US stock migration
- Data collector: Alpaca REST polling (replaces Binance WebSocket)
- Order executor: Alpaca submit_order (replaces ccxt)
- Claude stock screener: daily MOC candidate analysis
- Remove ccxt/websockets dependencies
- Default universe: AAPL, MSFT, GOOGL, AMZN, TSLA + 28 more
- 399 tests passing, lint clean
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/optimize_asian_rsi.py | 85 | ||||
| -rwxr-xr-x | scripts/stock_screener.py | 72 |
2 files changed, 99 insertions, 58 deletions
diff --git a/scripts/optimize_asian_rsi.py b/scripts/optimize_asian_rsi.py index 9921447..7cf24d7 100755 --- a/scripts/optimize_asian_rsi.py +++ b/scripts/optimize_asian_rsi.py @@ -22,62 +22,64 @@ from backtester.engine import BacktestEngine # noqa: E402 from strategies.asian_session_rsi import AsianSessionRsiStrategy # noqa: E402 -def generate_sol_candles(days: int = 90, base_price: float = 150.0) -> list[Candle]: +def generate_sol_candles(days: int = 60, base_price: float = 150.0) -> list[Candle]: """Generate realistic SOL/USDT 5-minute candles. - Simulates: - - Mild uptrend with periodic sharp dips during Asian session - - Intraday volatility (higher at session opens) - - Random walk with mean reversion - - Occasional momentum bursts that create RSI extremes + Simulates price action with: + - Strong uptrend in hours before Asian session (keeps EMA elevated) + - Sharp single-bar dips during Asian session (drives RSI oversold) + - Recovery bounces after dips + - Low-volatility ranging during off-hours (keeps ADX low for regime filter) """ random.seed(42) - candles = [] + candles: list[Candle] = [] price = base_price start = datetime(2025, 1, 1, tzinfo=timezone.utc) for day in range(days): - # Mild upward bias to keep price above EMA - daily_trend = random.uniform(-0.005, 0.015) + daily_trend = random.uniform(-0.002, 0.008) - # Many days have a sharp V-dip during Asian session (1-2 bar crash + recovery) - # This creates RSI oversold while EMA stays above price briefly - dip_day = random.random() < 0.45 - dip_bar = random.randint(4, 18) if dip_day else -1 - # Sharp single-bar dip: 2-4% drop then immediate recovery - dip_magnitude = random.uniform(0.02, 0.04) + # Most days have dip patterns to generate enough signals + dip_day = random.random() < 0.50 + # Place dip later in the session so the strategy has enough bars with + # elevated EMA before the crash + dip_bar = random.randint(8, 20) if dip_day else -1 + dip_pct = random.uniform(0.02, 0.04) for bar in range(288): # 288 5-minute bars per day dt = start + timedelta(days=day, minutes=bar * 5) hour = dt.hour # Volatility varies by session - if 0 <= hour < 2: # Asian open (our trading window) - vol = 0.003 - elif 13 <= hour < 16: # US session - vol = 0.0025 + if 0 <= hour < 2: + vol = 0.002 + elif 13 <= hour < 16: + vol = 0.002 else: - vol = 0.0015 + vol = 0.001 # Keep off-hours quiet for low ADX - # Base random walk with upward drift + # Base random walk change = random.gauss(daily_trend / 288, vol) mean_rev = (base_price - price) / base_price * 0.001 change += mean_rev - # Session bar index within 00:00-01:55 UTC (bars 0-23) - session_bar = bar + # Strong uptrend 4 hours before session (20:00-23:59 UTC) + # This elevates the 20-period EMA so the crash bar is still above EMA + if dip_day and 20 <= hour <= 23: + change += 0.0015 # ~+0.15% per bar, ~+7% over 4 hours + + session_bar = bar # bars 0-23 map to 00:00-01:55 UTC - # Inject sharp V-dip: 1 bar crash, 1 bar partial recovery if dip_day and 0 <= hour < 2: if session_bar == dip_bar: - # Crash bar: sharp drop - change = -dip_magnitude + # Sharp single-bar crash + change = -dip_pct elif session_bar == dip_bar + 1: - # Recovery bar: bounce back most of the way - change = dip_magnitude * random.uniform(0.5, 0.8) + # Bounce recovery + change = dip_pct * random.uniform(0.5, 0.9) elif session_bar == dip_bar + 2: # Continued recovery - change = dip_magnitude * random.uniform(0.1, 0.3) + change = dip_pct * random.uniform(0.1, 0.4) open_p = price close_p = price * (1 + change) @@ -88,7 +90,7 @@ def generate_sol_candles(days: int = 90, base_price: float = 150.0) -> list[Cand if 0 <= hour < 2: volume *= 2 if dip_day and dip_bar <= session_bar <= dip_bar + 2: - volume *= 2.5 # Spike volume on dip/recovery + volume *= 3.0 # High volume on dip/recovery candles.append( Candle( @@ -108,7 +110,13 @@ def generate_sol_candles(days: int = 90, base_price: float = 150.0) -> list[Cand return candles -def run_backtest(candles, params, balance=750.0, slippage=0.001, fee=0.001): +def run_backtest( + candles: list[Candle], + params: dict, + balance: float = 750.0, + slippage: float = 0.001, + fee: float = 0.001, +): """Run a single backtest with given parameters.""" strategy = AsianSessionRsiStrategy() strategy.configure(params) @@ -122,19 +130,19 @@ def run_backtest(candles, params, balance=750.0, slippage=0.001, fee=0.001): return engine.run(candles) -def main(): +def main() -> None: print("=" * 60) - print("Asian Session RSI — Parameter Optimization") + print("Asian Session RSI -- Parameter Optimization") print("SOL/USDT 5m | Capital: $750 (~100만원)") print("=" * 60) - days = 30 + days = 60 print(f"\nGenerating {days} days of synthetic SOL/USDT 5m candles...") candles = generate_sol_candles(days=days, base_price=150.0) print(f"Generated {len(candles)} candles") # Parameter grid - param_grid = [] + param_grid: list[dict] = [] for rsi_period in [7, 9, 14]: for rsi_oversold in [20, 25, 30]: for tp in [1.0, 1.5, 2.0]: @@ -160,7 +168,7 @@ def main(): print(f"\nTesting {len(param_grid)} parameter combinations...") print("-" * 60) - results = [] + results: list[tuple] = [] for i, params in enumerate(param_grid): result = run_backtest(candles, params) sharpe = result.detailed.sharpe_ratio if result.detailed else 0.0 @@ -191,9 +199,10 @@ def main(): print("\n" + "=" * 60) print("WORST 3 PARAMETER SETS") print("=" * 60) - for rank, (params, result, sharpe) in enumerate(results[-3:], 1): + for _rank, (params, result, sharpe) in enumerate(results[-3:], 1): print( - f"\n RSI({params['rsi_period']}), OS={params['rsi_oversold']}, TP={params['take_profit_pct']}%, SL={params['stop_loss_pct']}%" + f"\n RSI({params['rsi_period']}), OS={params['rsi_oversold']}," + f" TP={params['take_profit_pct']}%, SL={params['stop_loss_pct']}%" ) print(f" Profit: ${float(result.profit):.2f}, Trades: {result.total_trades}") diff --git a/scripts/stock_screener.py b/scripts/stock_screener.py index 387bfea..7a5c0ba 100755 --- a/scripts/stock_screener.py +++ b/scripts/stock_screener.py @@ -10,6 +10,7 @@ Usage: Requires: ANTHROPIC_API_KEY environment variable """ + import argparse import asyncio import json @@ -51,15 +52,17 @@ async def get_market_data(alpaca: AlpacaClient, symbols: list[str]) -> list[dict lows = [float(b["l"]) for b in bars] range_pct = (max(highs) - min(lows)) / close * 100 if close > 0 else 0 - results.append({ - "symbol": symbol, - "close": close, - "change_pct": round(change_pct, 2), - "volume": volume, - "vol_ratio": round(vol_ratio, 2), - "range_5d_pct": round(range_pct, 2), - "is_bullish": close > float(latest["o"]), # Today bullish? - }) + results.append( + { + "symbol": symbol, + "close": close, + "change_pct": round(change_pct, 2), + "volume": volume, + "vol_ratio": round(vol_ratio, 2), + "range_5d_pct": round(range_pct, 2), + "is_bullish": close > float(latest["o"]), # Today bullish? + } + ) except Exception as exc: print(f" Warning: Failed to fetch {symbol}: {exc}") @@ -137,19 +140,45 @@ async def analyze_with_claude(prompt: str, api_key: str) -> list[dict]: # Default universe of liquid US stocks DEFAULT_UNIVERSE = [ # Tech - "AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA", "AMD", "INTC", "CRM", + "AAPL", + "MSFT", + "GOOGL", + "AMZN", + "META", + "NVDA", + "TSLA", + "AMD", + "INTC", + "CRM", # Finance - "JPM", "BAC", "GS", "MS", "V", "MA", + "JPM", + "BAC", + "GS", + "MS", + "V", + "MA", # Healthcare - "JNJ", "UNH", "PFE", "ABBV", + "JNJ", + "UNH", + "PFE", + "ABBV", # Consumer - "WMT", "KO", "PEP", "MCD", "NKE", + "WMT", + "KO", + "PEP", + "MCD", + "NKE", # Energy - "XOM", "CVX", + "XOM", + "CVX", # Industrial - "CAT", "BA", "GE", + "CAT", + "BA", + "GE", # ETFs - "SPY", "QQQ", "IWM", + "SPY", + "QQQ", + "IWM", ] @@ -187,8 +216,7 @@ async def main_async(top_n: int = 5, universe: list[str] | None = None): # Pre-filter obvious rejects candidates = [ - s for s in market_data - if s["is_bullish"] and s["vol_ratio"] >= 0.8 and s["change_pct"] > -2 + s for s in market_data if s["is_bullish"] and s["vol_ratio"] >= 0.8 and s["change_pct"] > -2 ] print(f"Pre-filter: {len(candidates)} candidates (bullish, decent volume)\n") @@ -211,7 +239,9 @@ async def main_async(top_n: int = 5, universe: list[str] | None = None): # Find market data for this symbol md = next((m for m in market_data if m["symbol"] == rec["symbol"]), None) if md: - print(f" Close: ${md['close']:.2f} | Change: {md['change_pct']:+.2f}% | Vol Ratio: {md['vol_ratio']:.1f}x") + print( + f" Close: ${md['close']:.2f} | Change: {md['change_pct']:+.2f}% | Vol Ratio: {md['vol_ratio']:.1f}x" + ) else: print("Claude found no qualifying stocks today.") else: @@ -237,7 +267,9 @@ async def main_async(top_n: int = 5, universe: list[str] | None = None): print("-" * 60) for i, (s, score) in enumerate(scored[:top_n], 1): print(f"#{i} {s['symbol']} (Score: {score})") - print(f" Close: ${s['close']:.2f} | Change: {s['change_pct']:+.2f}% | Vol: {s['vol_ratio']:.1f}x") + print( + f" Close: ${s['close']:.2f} | Change: {s['change_pct']:+.2f}% | Vol: {s['vol_ratio']:.1f}x" + ) print("\n" + "=" * 60) |
