5fb1d52026
Big-picture changes since b941223:
KOL pipeline (new) — Substack/podcast/blog RSS → AI ticker extraction →
on-chain wallet diff → talks-vs-trades divergence detection. Daily polls,
19 feeds, divergence emits Post + Telegram fan-out.
Telegram push (new) — walletless free tier + wallet-linked Pro upgrade,
in-bot preference commands (/trump /btc /funding /kol /conf /quiet),
signed-envelope API for dashboard. Disconnect-wallet keeps free
subscription.
BTC funding-rate reversal scanner (new) — hourly cron, 30d cumulative
funding threshold + mean-revert + 7d price confirm, emits via
/api/signals/ingest. BTC bottom-reversal scanner promoted to System 2.
WS broadcast rewrite — per-client send timeout + parallel fan-out
(asyncio.gather). Fixes "Binance WS no close frame" reconnect storms +
APScheduler 11-min job misses, both caused by one slow client stalling
the kline loop.
Error visibility — three silent-error sites (trumpstruth/truth_social
fetchers, funding_reversal scanner) now include exception type name so
httpx ConnectError-style empty-message errors stop logging blank lines.
Telegram bot loop now classifies ReadTimeout vs network vs unknown +
logger.exception for the unknown bucket.
Security hygiene — trumpsignal.db untracked from git (held subscriber
wallets + encrypted HL keys + 22 bot trades); .gitignore now blocks
*.db/.next/backups. CORS only allows FRONTEND_URL in production.
New ops scripts —
- scripts/preflight.py: env/DB/Telegram/AI auth verification gate
- scripts/backup_db.sh: cron-friendly daily DB backup (SQLite + Postgres)
- scripts/seed_kol_wallets.py: idempotent KOL on-chain wallet seeder
15 new Alembic migrations (007-021) covering convex strategy fields,
phase-1 safety, two-system frozen exits, invalidation prices, dynamic
SYS2 leverage, staged de-risk + pyramiding, peak gain tracking, risk
mode, auto-trade + grow flags, KOL module, KOL on-chain, KOL divergence,
Telegram bindings + walletless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
224 lines
7.6 KiB
Python
224 lines
7.6 KiB
Python
"""
|
|
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)
|