""" Dev-only endpoint: manually insert a fake post to test the full pipeline. """ import asyncio from datetime import datetime, timezone from fastapi import APIRouter, BackgroundTasks, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db, AsyncSessionLocal from app.models import Post, iso_utc from app.ws.manager import manager import hashlib, time router = APIRouter() @router.post("/dev/fake-post") async def insert_fake_post( text: str = "BITCOIN is the future of money! We will make America the greatest crypto capital EVER!", sentiment: str = "bullish", db: AsyncSession = Depends(get_db), ): external_id = hashlib.md5(f"fake-{time.time()}".encode()).hexdigest() post = Post( external_id=external_id, text=text, source="truth", published_at=datetime.now(timezone.utc).replace(tzinfo=None), sentiment=sentiment, ai_confidence=88, relevant=True, price_impact_asset="BTC", price_impact_m5=1.2, price_impact_m15=2.4, price_impact_m1h=3.1, price_at_post=94230.0, ) db.add(post) await db.commit() await db.refresh(post) await manager.broadcast({ "type": "new_post", "post": { "id": post.id, "text": post.text, "source": post.source, "published_at": iso_utc(post.published_at), "sentiment": post.sentiment, "ai_confidence": post.ai_confidence, "relevant": post.relevant, "price_impact": { "asset": "BTC", "m5": 1.2, "m15": 2.4, "m1h": 3.1, "price_at_post": 94230.0 }, }, }) return {"id": post.id, "text": post.text[:80]} @router.post("/dev/fake-signal") async def inject_fake_signal( symbol: str = "ETHUSDT", close: float = 1850.42, tbr: float = 0.72, vol_mult: float = 3.1, bb_pct: float = 8.5, btc_trend: str = "↑ uptrend", ): """Inject a synthetic funding_signal for end-to-end testing.""" from datetime import datetime, timezone from app.services import funding_signal as fs now = datetime.now(timezone.utc) alert = { "type": "funding_signal", "symbol": symbol, "time": now.isoformat(), "close": close, "tbr": tbr, "vol_mult": vol_mult, "bb_pct": bb_pct, "bb_upper": round(close * 1.002, 4), "btc_trend": btc_trend, "enabled": fs.is_enabled(), } fs._recent_signals.append(alert) if fs.is_enabled(): await manager.broadcast(alert) return {"status": "broadcast", "alert": alert} else: return {"status": "recorded_only (monitor OFF)", "alert": alert} @router.post("/dev/reanalyze") async def trigger_reanalyze( background_tasks: BackgroundTasks, limit: int = 500, dry_run: bool = False, delay_secs: float = 0.5, legacy_signals: bool = False, model: str = "", ): """ Batch re-run AI analysis on unscored posts. model: override the AI model (default: ai_live_model=flash for speed). Runs in the background — poll GET /dev/reanalyze/status for progress. """ from app.services.reanalyze import reanalyze_unscored, get_state from app.config import settings as _s state = get_state() if state["running"]: return {"status": "already_running", "state": state} effective_model = model or _s.ai_live_model # default to flash background_tasks.add_task( reanalyze_unscored, AsyncSessionLocal, limit=limit, dry_run=dry_run, delay_secs=delay_secs, legacy_signals=legacy_signals, model=effective_model, ) return { "status": "started", "limit": limit, "dry_run": dry_run, "delay_secs": delay_secs, "legacy_signals": legacy_signals, "model": effective_model, "message": f"Re-analyzing up to {limit} posts in background. Poll /api/dev/reanalyze/status.", } @router.get("/dev/reanalyze/status") async def reanalyze_status(): """Current progress of the background re-analyzer.""" from app.services.reanalyze import get_state return get_state() @router.post("/dev/backfill-prices") async def trigger_price_backfill(background_tasks: BackgroundTasks, asset: str = "BTC"): """触发历史帖子价格回溯(后台运行)""" from app.services.price_backfill import backfill_price_impact background_tasks.add_task(backfill_price_impact, AsyncSessionLocal, asset) return {"status": "started", "asset": asset, "message": "后台回溯中,约需 1-2 分钟"} # ─── Convex-strategy backtest ────────────────────────────────────────────── @router.post("/dev/backtest/post") async def backtest_one_post( post_id: int, stop_loss_pct: float = 1.5, trailing_stop_pct: float = 2.5, trailing_activate_at_pct: float = 5.0, take_profit_pct: float = 0.0, # 0 = treat as None (no fixed TP) max_hold_hours: int = 168, ): """Replay one post through the new convex-strategy exit rules. Pulls 1m candles from Binance for the post's hold window and simulates trailing-stop + SL + max-hold. Useful for sanity-checking parameters. Pass `take_profit_pct=0` to disable the fixed TP and run pure trailing. """ from app.services.backtest import backtest_post, BacktestParams params = BacktestParams( stop_loss_pct=stop_loss_pct, trailing_stop_pct=trailing_stop_pct or None, trailing_activate_at_pct=trailing_activate_at_pct or None, take_profit_pct=take_profit_pct or None, max_hold_hours=max_hold_hours, ) r = await backtest_post(post_id, params) if r is None: return {"status": "skipped", "post_id": post_id} return {"status": "ok", "result": r.to_dict()} @router.post("/dev/paper-mode") async def toggle_paper_mode( wallet: str, enabled: bool, db: AsyncSession = Depends(get_db), ): """Flip paper_mode for a subscription. Dev-only — no signature required. Paper mode: trades are simulated end-to-end (entry/exit from Binance, DB row with hl_order_id='paper') but no Hyperliquid call is made. """ from sqlalchemy import select from app.models import Subscription wallet = wallet.lower().strip() result = await db.execute(select(Subscription).where(Subscription.wallet_address == wallet)) sub = result.scalar_one_or_none() if sub is None: return {"status": "not_found", "wallet": wallet} sub.paper_mode = bool(enabled) await db.commit() return {"status": "ok", "wallet": wallet, "paper_mode": sub.paper_mode} @router.post("/dev/backtest/batch") async def backtest_batch_route( limit: int = 50, min_confidence: int = 80, stop_loss_pct: float = 1.5, trailing_stop_pct: float = 2.5, trailing_activate_at_pct: float = 5.0, take_profit_pct: float = 0.0, max_hold_hours: int = 168, ): """Batch backtest: every directional post with conf ≥ min_confidence. WARNING: synchronous — for 50 posts at ~10s each on Binance fetches this can take several minutes. Run with limit=5 first to sanity-check. """ from app.services.backtest import backtest_batch, BacktestParams params = BacktestParams( stop_loss_pct=stop_loss_pct, trailing_stop_pct=trailing_stop_pct or None, trailing_activate_at_pct=trailing_activate_at_pct or None, take_profit_pct=take_profit_pct or None, max_hold_hours=max_hold_hours, ) return await backtest_batch(limit=limit, min_confidence=min_confidence, params=params)