d6c802ef26
Batch of the pre-launch audit campaign (BUG-01…14 plus three new features): Pricing / TP-SL protection - Add app/services/hl_price_feed.py: supplemental HL allMids poller for HL-native assets (HYPE, PURR) not listed on Binance. Pumps price_store + tp_sl_monitor.on_price_tick so bot trades on these assets keep full stop-loss / take-profit / trailing protection instead of max-hold only. - Wire feed into main.py lifespan (startup task + graceful shutdown cancel). Telegram - Add format_trump_mention + PATH B in _dispatch: crypto-relevant Trump posts with no directional signal (relevant=True, signal=hold) now alert the public channel only (no per-subscriber noise). - Rate limiter (slowapi) on the API; assorted bot/digest fixes. KOL on-chain - seed_kol_wallets.py: KOL_FEEDS coverage cross-check; reversibly deactivate orphaned wallets (handle not in KOL_FEEDS → can never produce divergence) so the scanner stops burning cycles on them. Tests / misc - Fix brittle test_macro_ahr999_uses_same_formula_as_scanner: mock now uses realistic ms timestamps so the in-progress-day drop fires, matching the fetcher's bar count (was 0.3179 vs 0.3178 off-by-one). - Refresh stale notify_signal comment in truth_social.py. Frontend reduce-action type fix lives in the sibling repo. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
import logging
|
|
from typing import List
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import joinedload
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.models import BotTrade, iso_utc
|
|
from app.schemas import BotTrade as BotTradeSchema
|
|
from app.services.signed_request import verify_signed_request_any
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ACTION_VIEW_TRADES = "view_trades"
|
|
ACTION_VIEW_USER = "view_user"
|
|
|
|
|
|
def _trade_to_schema(trade: BotTrade) -> BotTradeSchema:
|
|
# Join against trigger_post to surface the source tag. When a trade was
|
|
# opened by an ingested signal (VCP scanner, user's module, etc.) the
|
|
# source reveals WHICH module produced it — critical for "is module X
|
|
# actually making money?" attribution analysis.
|
|
trigger_source = None
|
|
if trade.trigger_post is not None:
|
|
trigger_source = trade.trigger_post.source
|
|
# Paper trades are tagged via hl_order_id at open time; that's the only
|
|
# stable signal we have to distinguish them in aggregate views.
|
|
is_paper = (trade.hl_order_id == "paper")
|
|
return BotTradeSchema(
|
|
id=trade.id,
|
|
asset=trade.asset,
|
|
side=trade.side,
|
|
entry_price=trade.entry_price,
|
|
exit_price=trade.exit_price or 0.0,
|
|
pnl_usd=trade.pnl_usd or 0.0,
|
|
hold_seconds=trade.hold_seconds or 0,
|
|
trigger_post_id=trade.trigger_post_id or 0,
|
|
opened_at=iso_utc(trade.opened_at) or "",
|
|
closed_at=iso_utc(trade.closed_at) or "",
|
|
trigger_source=trigger_source,
|
|
is_paper=is_paper,
|
|
)
|
|
|
|
|
|
@router.get("/trades", response_model=List[BotTradeSchema])
|
|
async def get_trades(
|
|
wallet: str = Query(..., description="Wallet address (lower-cased internally)"),
|
|
ts: int = Query(..., description="Signed timestamp (ms)"),
|
|
sig: str = Query(..., description="EIP-191 signature"),
|
|
limit: int = Query(default=20, ge=1, le=100),
|
|
page: int = Query(default=1, ge=1),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
wallet = wallet.lower().strip()
|
|
verify_signed_request_any(
|
|
actions=[ACTION_VIEW_TRADES, ACTION_VIEW_USER],
|
|
wallet=wallet,
|
|
timestamp_ms=ts,
|
|
signature=sig,
|
|
body=None,
|
|
allow_replay=True,
|
|
)
|
|
offset = (page - 1) * limit
|
|
# joinedload pulls trigger_post in the same query — avoids N+1 lookups
|
|
# when serialising 100 trades.
|
|
result = await db.execute(
|
|
select(BotTrade)
|
|
.options(joinedload(BotTrade.trigger_post))
|
|
.where(BotTrade.wallet_address == wallet)
|
|
.where(BotTrade.closed_at.is_not(None))
|
|
.order_by(BotTrade.opened_at.desc())
|
|
.offset(offset)
|
|
.limit(limit)
|
|
)
|
|
trades = result.scalars().all()
|
|
return [_trade_to_schema(t) for t in trades]
|