This commit is contained in:
k
2026-04-21 19:33:24 +08:00
parent 9a72566753
commit 3268080401
26 changed files with 1816 additions and 318 deletions
+130
View File
@@ -0,0 +1,130 @@
"""
Simulate ~30 days of bot trading history for a given wallet, using real historical
posts + the measured price_impact_{m5,m15,m1h} fields. Creates BotTrade rows as if
the bot had been running with default settings (size=$20, lev=3x, tp=2%, sl=1.5%).
Usage:
python simulate_trades.py <wallet_address>
Safe to re-run: skips posts already linked to a trade for the given wallet.
"""
import asyncio
import sys
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple
from sqlalchemy import select
from app.database import AsyncSessionLocal
from app.models import BotTrade, Post
SIZE_USD = 20.0
LEVERAGE = 3
TP_PCT = 2.0 # close at +2% price move in position direction
SL_PCT = 1.5 # close at -1.5%
MIN_CONFIDENCE = 80
DAYS_BACK = 30
def simulate_exit(side: str, m5: Optional[float], m15: Optional[float], m1h: Optional[float]) -> Tuple[float, int, str]:
"""
Walk the three sampled points; exit at the first TP/SL crossing, else at m1h close.
Returns (signed_pct, hold_seconds, reason).
signed_pct = price move in position's favour (positive means profit, in raw %).
"""
samples = [(300, m5), (900, m15), (3600, m1h)]
for secs, pct in samples:
if pct is None:
continue
signed = pct if side == 'long' else -pct
if signed >= TP_PCT:
return TP_PCT, secs, 'take_profit'
if signed <= -SL_PCT:
return -SL_PCT, secs, 'stop_loss'
# No TP/SL hit — exit at whichever latest sample we have
final = m1h if m1h is not None else (m15 if m15 is not None else (m5 or 0.0))
signed = final if side == 'long' else -final
hold = 3600 if m1h is not None else (900 if m15 is not None else 300)
return signed, hold, 'max_hold'
async def main(wallet: str) -> None:
wallet = wallet.lower()
cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=DAYS_BACK)
async with AsyncSessionLocal() as db:
# Get candidate posts
result = await db.execute(
select(Post)
.where(Post.published_at >= cutoff)
.where(Post.relevant == True) # noqa: E712
.where(Post.ai_confidence >= MIN_CONFIDENCE)
.where(Post.signal.in_(('buy', 'short')))
.where(Post.price_at_post.is_not(None))
.order_by(Post.published_at.asc())
)
posts = result.scalars().all()
# Skip posts already simulated for this wallet
existing = await db.execute(
select(BotTrade.trigger_post_id).where(BotTrade.wallet_address == wallet)
)
seen = {pid for (pid,) in existing.all() if pid is not None}
created = 0
total_pnl = 0.0
wins = 0
for p in posts:
if p.id in seen:
continue
asset = p.price_impact_asset or 'BTC'
side = 'long' if p.signal == 'buy' else 'short'
entry = p.price_at_post
if not entry:
continue
signed_pct, hold_secs, reason = simulate_exit(
side, p.price_impact_m5, p.price_impact_m15, p.price_impact_m1h
)
# exit_price reconstructed from signed_pct relative to side direction
raw_pct = signed_pct if side == 'long' else -signed_pct
exit_price = entry * (1 + raw_pct / 100.0)
pnl_usd = round(SIZE_USD * (signed_pct / 100.0) * LEVERAGE, 2)
opened = p.published_at
closed = opened + timedelta(seconds=hold_secs)
trade = BotTrade(
asset=asset,
side=side,
entry_price=round(entry, 2),
exit_price=round(exit_price, 2),
pnl_usd=pnl_usd,
hold_seconds=hold_secs,
trigger_post_id=p.id,
wallet_address=wallet,
opened_at=opened,
closed_at=closed,
hl_order_id=f'sim-{p.id}',
)
db.add(trade)
created += 1
total_pnl += pnl_usd
if pnl_usd > 0:
wins += 1
await db.commit()
wr = (wins / created * 100) if created else 0
print(f"✓ Simulated {created} trades for {wallet}")
print(f" Total PnL: ${total_pnl:+.2f}")
print(f" Win rate: {wr:.1f}% ({wins}/{created})")
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: python simulate_trades.py <wallet_address>")
sys.exit(1)
asyncio.run(main(sys.argv[1]))