Record news signals forward for a no-trade edge test

The trader labels crypto news published after 2026-10-16 15:00 with
the configured model (category and BTC direction) and, four hours
later, backfills BTC/ETH 1m opens at publish, +5m, +1h and +4h from
the klines table. A drawer on the trading page shows the cumulative
returns of trading the labelled direction, after fees. Nothing is
traded; this only collects an out-of-sample record.
This commit is contained in:
eezd committed 2026-10-16 14:47:06 +08:00
1 parent 0397e30870
commit 23a9a5175e
11 files changed
+537 -7

No files matched your search

+60 -1
View File
@@ -17,11 +17,13 @@ from alphapulse.api.market import call_binance, client as market_client
from alphapulse.core.auth import require_admin
from alphapulse.core.db import get_session
from alphapulse.core.redis import KEY_PREFIX, redis
from alphapulse.models import AiProvider, TradingAccount, TradingAnalysis, TradingEvent, TradingFill, TradingOrder, TradingSymbol
from alphapulse.models import AiProvider, NewsItem, NewsSignal, TradingAccount, TradingAnalysis, TradingEvent, TradingFill, TradingOrder, TradingSymbol
from alphapulse.trading.client import OWN_CLIENT_ID, ExchangeError, public_get
from alphapulse.trading.executor import MANUAL_ACTION_KEY
from alphapulse.trading.news_signals import FORWARD_START, SYMBOLS as SIGNAL_SYMBOLS, news_text
from alphapulse.trading.positions import build_positions
from alphapulse.trading.plan import DAILY_ANALYSIS_LIMIT
from alphapulse.trading.signal_stats import Signal, summarize
from alphapulse.trading.snapshot import read_snapshot
router = APIRouter(prefix="/api/trading", tags=["trading"], dependencies=[Depends(require_admin)])
@@ -179,6 +181,37 @@ class PositionHistoryPage(BaseModel):
items: list[PositionHistoryItem]
class NewsSignalStat(BaseModel):
category: str
symbol: str
horizon: str
n: int
mean_bp: float | None
t: float | None
# 扣除来回手续费后的平均收益
net_bp: float | None
win_rate: float | None
class NewsSignalItem(BaseModel):
news_id: int
published_at: datetime
source_code: str
title: str
category: str
direction: str
# 进场后的原始涨跌(bp,未按方向取正负):{symbol: {"1h": ..., "4h": ...}};未满 4 小时为空
returns: dict[str, dict[str, float]] | None
class NewsSignalsOut(BaseModel):
start: datetime
labeled: int
filled: int
stats: list[NewsSignalStat]
recent: list[NewsSignalItem]
async def get_account(session: AsyncSession) -> TradingAccount:
return await session.get(TradingAccount, 1) or TradingAccount(id=1)
@@ -454,6 +487,32 @@ async def list_position_history_symbols(session: Annotated[AsyncSession, Depends
return list(await session.scalars(select(TradingFill.symbol).distinct().order_by(TradingFill.symbol)))
@router.get("/news-signals")
async def list_news_signals(session: Annotated[AsyncSession, Depends(get_session)]) -> NewsSignalsOut:
"""新闻信号前向记录:按标注方向交易的累计统计和最近 50 条"""
rows = (await session.execute(
select(NewsSignal, NewsItem.published_at, NewsItem.source_code, news_text())
.join(NewsItem, NewsItem.id == NewsSignal.news_id).order_by(NewsItem.published_at)
)).all()
filled = [Signal(published_at, s.category, s.direction, s.prices) for s, published_at, _, _ in rows if s.prices]
horizons = ("1h", "4h")
def returns(prices: dict | None) -> dict[str, dict[str, float]] | None:
if prices is None:
return None
return {sym: {h: (float(p[h]) / float(p["entry"]) - 1) * 1e4 for h in horizons} for sym, p in prices.items()}
return NewsSignalsOut(
start=FORWARD_START, labeled=len(rows), filled=len(filled),
stats=[NewsSignalStat(**r) for r in summarize(filled, SIGNAL_SYMBOLS, horizons)],
recent=[
NewsSignalItem(news_id=s.news_id, published_at=published_at, source_code=source, title=title,
category=s.category, direction=s.direction, returns=returns(s.prices))
for s, published_at, source, title in reversed(rows[-50:])
],
)
@router.post("/symbols/{symbol}/analyze", status_code=status.HTTP_202_ACCEPTED)
async def request_analysis(symbol: str, session: Annotated[AsyncSession, Depends(get_session)]) -> None:
"""排队一次手动分析:该币停止交易时为试运行,计划只记录、不下单"""