Add demo trading analyst with plan validation and sizing
The analyst prompts the model with trading rules, published skills, account sizing inputs and prefetched market data, exposes read-only tools plus submit_trade_plan, and validates and sizes the submitted plan in pure functions covered by pytest. Manual analyses run as dry runs while trading is stopped; the trading page shows the latest plan and an analysis detail drawer.
This commit is contained in:
1 parent
7536c9c5de
commit
78ca4422af
16 files changed
+1465
-151
No files matched your search
@@ -18,8 +18,9 @@ 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, TradingSymbol
|
||||
from alphapulse.trading.client import ExchangeError, public_get
|
||||
from alphapulse.trading.engine import DAILY_ANALYSIS_LIMIT, OWN_CLIENT_ID, STATUS_KEY
|
||||
from alphapulse.trading.client import OWN_CLIENT_ID, ExchangeError, public_get
|
||||
from alphapulse.trading.plan import DAILY_ANALYSIS_LIMIT
|
||||
from alphapulse.trading.snapshot import read_snapshot
|
||||
|
||||
router = APIRouter(prefix="/api/trading", tags=["trading"], dependencies=[Depends(require_admin)])
|
||||
|
||||
@@ -82,6 +83,36 @@ class SymbolPatch(BaseModel):
|
||||
angle: Angle
|
||||
|
||||
|
||||
class AnalysisOut(BaseModel):
|
||||
id: int
|
||||
symbol: str
|
||||
angle: str
|
||||
reasons: list[str]
|
||||
dry_run: bool
|
||||
status: str
|
||||
action: str | None
|
||||
decision: dict | None
|
||||
sizing: dict | None
|
||||
error: str | None
|
||||
model: str | None
|
||||
reasoning_effort: str | None
|
||||
usage: dict | None
|
||||
started_at: datetime
|
||||
finished_at: datetime | None
|
||||
plan_status: str | None
|
||||
valid_until: datetime | None
|
||||
first_fill_at: datetime | None
|
||||
close_deadline: datetime | None
|
||||
closed_at: datetime | None
|
||||
close_reason: str | None
|
||||
result: dict | None
|
||||
|
||||
|
||||
class AnalysisDetail(AnalysisOut):
|
||||
text: str | None
|
||||
transcript: list | None
|
||||
|
||||
|
||||
class EventOut(BaseModel):
|
||||
id: int
|
||||
created_at: datetime
|
||||
@@ -97,11 +128,6 @@ async def get_account(session: AsyncSession) -> TradingAccount:
|
||||
return await session.get(TradingAccount, 1) or TradingAccount(id=1, running=False)
|
||||
|
||||
|
||||
async def read_snapshot() -> dict | None:
|
||||
raw = await redis.get(STATUS_KEY)
|
||||
return json.loads(raw) if raw else None
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status(session: Annotated[AsyncSession, Depends(get_session)]) -> StatusOut:
|
||||
account = await get_account(session)
|
||||
@@ -292,3 +318,39 @@ async def list_events(
|
||||
if before:
|
||||
query = query.where(TradingEvent.id < before)
|
||||
return [EventOut.model_validate(e, from_attributes=True) for e in await session.scalars(query)]
|
||||
|
||||
|
||||
@router.post("/symbols/{symbol}/analyze", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def request_analysis(symbol: str, session: Annotated[AsyncSession, Depends(get_session)]) -> None:
|
||||
"""排队一次手动分析:引擎停止时为试运行,计划只记录、不下单"""
|
||||
s = await session.get(TradingSymbol, symbol)
|
||||
if s is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, f"{symbol} 不在交易列表中")
|
||||
if (await get_account(session)).capital_usdt is None:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "还没有设置交易资金")
|
||||
running = await session.scalar(select(func.count()).where(TradingAnalysis.symbol == symbol, TradingAnalysis.status == "running"))
|
||||
if running or s.analysis_requested_at is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, f"{symbol} 已在分析或排队中")
|
||||
s.analysis_requested_at = datetime.now(LOCAL_TZ)
|
||||
s.analysis_reasons = ["手动分析"]
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.get("/analyses")
|
||||
async def list_analyses(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
symbol: str | None = None,
|
||||
limit: Annotated[int, Query(ge=1, le=50)] = 10,
|
||||
) -> list[AnalysisOut]:
|
||||
query = select(TradingAnalysis).order_by(TradingAnalysis.id.desc()).limit(limit)
|
||||
if symbol:
|
||||
query = query.where(TradingAnalysis.symbol == symbol)
|
||||
return [AnalysisOut.model_validate(a, from_attributes=True) for a in await session.scalars(query)]
|
||||
|
||||
|
||||
@router.get("/analyses/{analysis_id}")
|
||||
async def get_analysis(analysis_id: int, session: Annotated[AsyncSession, Depends(get_session)]) -> AnalysisDetail:
|
||||
analysis = await session.get(TradingAnalysis, analysis_id)
|
||||
if analysis is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "分析不存在")
|
||||
return AnalysisDetail.model_validate(analysis, from_attributes=True)
|
||||
Reference in new issue
Block a user