chore: add $500 capital sweep scripts (conservative + moonshot)
scripts/sweep_500.py: Parameter grid for capital-preservation oriented configs. Risk filters: max DD <= 30%, max single loss <= 16% of capital. scripts/sweep_moonshot.py: Aggressive grid for one-trade-hits-big strategy. Looser DD ceiling (50%), prioritizes biggest single-trade upside. Both run on the local v4 dataset to inform initial subscription parameter choices for live trading. Re-run after v5 accumulates enough signals (~6 weeks) to recalibrate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Parameter sweep for a $500 starting account.
|
||||
|
||||
Finds (confidence_floor, leverage, tp%, sl%, margin_per_trade) combos that
|
||||
maximize expected return while keeping max drawdown bounded.
|
||||
|
||||
Uses the v4 buy/short signals already in the DB (sell excluded — semantic
|
||||
bug). For each signal we know `price_impact_m1h` = max favorable excursion
|
||||
in % over the next hour (already side-adjusted). Sim:
|
||||
• peak >= tp_pct → TP hit, exit at +tp
|
||||
• peak < 0 → SL hit (price went against us — pessimistic but safe)
|
||||
• else → exit at peak/2 (held until window close, conservative)
|
||||
• fees: 9 bps round-trip on notional
|
||||
|
||||
Metrics computed per combo:
|
||||
total_pnl_usd total profit on the $500 over the sample period
|
||||
max_dd_usd biggest peak-to-trough drawdown in $
|
||||
max_dd_pct same as % of starting capital
|
||||
sharpe_ish mean / std of per-trade PnL (rough Sharpe proxy)
|
||||
worst_trade biggest single-trade loss in $
|
||||
n_trades how many trades fired in this combo
|
||||
"""
|
||||
import sqlite3
|
||||
import statistics
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
|
||||
DB = Path(__file__).resolve().parents[1] / "trumpsignal.db"
|
||||
CAPITAL = 500.0
|
||||
FEE_BPS_RT = 0.0009
|
||||
|
||||
|
||||
def fetch_signals(conf_floor: int):
|
||||
con = sqlite3.connect(DB)
|
||||
con.row_factory = sqlite3.Row
|
||||
rows = con.execute("""
|
||||
SELECT id, signal, ai_confidence, price_impact_m1h, published_at
|
||||
FROM posts
|
||||
WHERE signal IN ('buy', 'short')
|
||||
AND price_at_post IS NOT NULL
|
||||
AND price_impact_m1h IS NOT NULL
|
||||
AND ai_confidence >= ?
|
||||
ORDER BY published_at
|
||||
""", (conf_floor,)).fetchall()
|
||||
con.close()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def simulate(signals, margin: float, leverage: int, tp_pct: float, sl_pct: float):
|
||||
"""One sim of all signals with one parameter set. Returns trade-by-trade
|
||||
PnLs in $ (positive = win, negative = loss)."""
|
||||
notional = margin * leverage
|
||||
pnls: list[float] = []
|
||||
for s in signals:
|
||||
peak = s["price_impact_m1h"] # already side-adjusted, %
|
||||
if peak >= tp_pct:
|
||||
gross = tp_pct
|
||||
elif peak < 0:
|
||||
gross = -sl_pct # pessimistic: assume SL hit
|
||||
else:
|
||||
gross = peak / 2 # conservative midpoint
|
||||
# Apply fees on notional
|
||||
net_pct = gross / 100 - FEE_BPS_RT
|
||||
# Convert to $ on this single trade
|
||||
pnls.append(notional * net_pct)
|
||||
return pnls
|
||||
|
||||
|
||||
def metrics(pnls: list[float]) -> dict:
|
||||
n = len(pnls)
|
||||
if n == 0:
|
||||
return {"n": 0, "total": 0.0, "max_dd": 0.0, "max_dd_pct": 0.0,
|
||||
"sharpe": 0.0, "worst": 0.0, "win_rate": 0.0}
|
||||
total = sum(pnls)
|
||||
wins = sum(1 for p in pnls if p > 0)
|
||||
# Running equity for drawdown
|
||||
equity = CAPITAL
|
||||
peak = equity
|
||||
max_dd = 0.0
|
||||
for p in pnls:
|
||||
equity += p
|
||||
if equity > peak:
|
||||
peak = equity
|
||||
dd = peak - equity
|
||||
if dd > max_dd:
|
||||
max_dd = dd
|
||||
sharpe = (statistics.mean(pnls) / statistics.stdev(pnls)
|
||||
if n > 1 and statistics.stdev(pnls) > 0 else 0.0)
|
||||
return {
|
||||
"n": n,
|
||||
"total": round(total, 2),
|
||||
"max_dd": round(max_dd, 2),
|
||||
"max_dd_pct": round(max_dd / CAPITAL * 100, 1),
|
||||
"sharpe": round(sharpe, 2),
|
||||
"worst": round(min(pnls), 2),
|
||||
"win_rate": round(wins / n * 100, 1),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print(f"$500 PARAMETER SWEEP")
|
||||
print("=" * 90)
|
||||
print(f"Sample: v4 buy/short signals from local DB, peak-MFE-based realistic sim")
|
||||
print(f"Sim model: TP if peak≥TP, SL if peak<0, else peak/2 (conservative)")
|
||||
print()
|
||||
|
||||
# Sweep grid
|
||||
confs = [60, 70, 75, 80, 85]
|
||||
margins = [25, 50, 100] # $ per trade
|
||||
levs = [5, 10, 15, 20]
|
||||
tps = [0.5, 1.0, 1.5, 2.0, 3.0] # %
|
||||
sls = [0.5, 1.0, 1.5, 2.0] # %
|
||||
|
||||
results = []
|
||||
for conf in confs:
|
||||
sigs = fetch_signals(conf)
|
||||
if not sigs:
|
||||
continue
|
||||
for margin, lev, tp, sl in product(margins, levs, tps, sls):
|
||||
# Skip degenerate combos (tp<sl with low conviction signals)
|
||||
if margin * lev > CAPITAL * 5: # cap notional at 5× capital total
|
||||
continue
|
||||
pnls = simulate(sigs, margin, lev, tp, sl)
|
||||
m = metrics(pnls)
|
||||
# ROI on the trading capital (not notional)
|
||||
m.update({
|
||||
"conf": conf, "margin": margin, "lev": lev,
|
||||
"tp": tp, "sl": sl,
|
||||
"roi_pct": round(m["total"] / CAPITAL * 100, 1),
|
||||
"max_loss_per_trade": round(margin * lev * (sl/100 + FEE_BPS_RT), 2),
|
||||
})
|
||||
results.append(m)
|
||||
|
||||
# Filter to "realistic risk profile" first
|
||||
# Worst single trade <= $80 (16% of capital)
|
||||
# Max drawdown <= $150 (30% of capital)
|
||||
# At least 5 trades
|
||||
candidates = [
|
||||
r for r in results
|
||||
if r["max_loss_per_trade"] <= 80
|
||||
and r["max_dd"] <= 150
|
||||
and r["n"] >= 5
|
||||
and r["total"] > 0 # actually profitable
|
||||
]
|
||||
|
||||
print(f"Combos tested: {len(results)}")
|
||||
print(f"Profitable + safe: {len(candidates)}")
|
||||
print()
|
||||
|
||||
# Top 10 by total PnL
|
||||
print("=" * 90)
|
||||
print("TOP 10 BY TOTAL PROFIT (with risk filters applied)")
|
||||
print("=" * 90)
|
||||
print(f"{'conf':>4} {'margin':>6} {'lev':>3} {'TP%':>4} {'SL%':>4} "
|
||||
f"{'n':>3} {'win%':>5} {'total$':>8} {'ROI%':>6} "
|
||||
f"{'maxDD$':>7} {'DD%':>5} {'maxLoss$':>8} {'sharpe':>6}")
|
||||
for r in sorted(candidates, key=lambda x: -x["total"])[:10]:
|
||||
print(f"{r['conf']:>4} ${r['margin']:>5} {r['lev']:>3}× "
|
||||
f"{r['tp']:>4.1f} {r['sl']:>4.1f} "
|
||||
f"{r['n']:>3} {r['win_rate']:>4.1f}% "
|
||||
f"${r['total']:>+6.2f} {r['roi_pct']:>+5.1f}% "
|
||||
f"${r['max_dd']:>5.2f} {r['max_dd_pct']:>4.1f}% "
|
||||
f"${r['max_loss_per_trade']:>6.2f} {r['sharpe']:>+5.2f}")
|
||||
|
||||
print()
|
||||
print("=" * 90)
|
||||
print("TOP 5 BY SHARPE (risk-adjusted return)")
|
||||
print("=" * 90)
|
||||
print(f"{'conf':>4} {'margin':>6} {'lev':>3} {'TP%':>4} {'SL%':>4} "
|
||||
f"{'n':>3} {'win%':>5} {'total$':>8} {'sharpe':>6} {'maxDD%':>6}")
|
||||
for r in sorted(candidates, key=lambda x: -x["sharpe"])[:5]:
|
||||
print(f"{r['conf']:>4} ${r['margin']:>5} {r['lev']:>3}× "
|
||||
f"{r['tp']:>4.1f} {r['sl']:>4.1f} "
|
||||
f"{r['n']:>3} {r['win_rate']:>4.1f}% "
|
||||
f"${r['total']:>+6.2f} {r['sharpe']:>+5.2f} "
|
||||
f"{r['max_dd_pct']:>5.1f}%")
|
||||
|
||||
print()
|
||||
print("=" * 90)
|
||||
print("LOWEST DRAWDOWN COMBOS (conservative, prioritize survival)")
|
||||
print("=" * 90)
|
||||
print(f"{'conf':>4} {'margin':>6} {'lev':>3} {'TP%':>4} {'SL%':>4} "
|
||||
f"{'n':>3} {'total$':>8} {'maxDD%':>6} {'sharpe':>6}")
|
||||
for r in sorted(candidates, key=lambda x: (x["max_dd_pct"], -x["total"]))[:5]:
|
||||
print(f"{r['conf']:>4} ${r['margin']:>5} {r['lev']:>3}× "
|
||||
f"{r['tp']:>4.1f} {r['sl']:>4.1f} "
|
||||
f"{r['n']:>3} ${r['total']:>+6.2f} "
|
||||
f"{r['max_dd_pct']:>5.1f}% {r['sharpe']:>+5.2f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user