summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:13:10 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:13:10 +0900
commite9c791ae2b14884f8f0525da5fcaa1710ca1fc63 (patch)
tree752c219a95abe7bdbfbf0644711f7dede407aa7b /scripts
parent35120795147adf53de59b7f2a3c8aa14adec9a56 (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-xscripts/optimize_asian_rsi.py85
-rwxr-xr-xscripts/stock_screener.py72
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)