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:
1 parent
0397e30870
commit
23a9a5175e
11 files changed
+537
-7
No files matched your search
@@ -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:
|
||||
"""排队一次手动分析:该币停止交易时为试运行,计划只记录、不下单"""
|
||||
|
||||
Reference in new issue
Block a user