Add demo trading executor and analysis scheduling (plan 3 C4)
- Executor reconciles each symbol to its desired orders: journaled ap{id}
client ids, reduce-only quantity stop placed before entries, make-before-
break stop moves, breakeven after TP1, orphan cancel, -2021 market close
- Plan lifecycle: proposed plans are adopted only if the position is
unchanged; holding-mode plans may only tighten the stop
- Program rules: time stop, short-term invalidation close, long-term 4h
review, reevaluate conditions, takeover of unmanaged positions
- Trigger and routine analysis scheduling with a 10 min gap, daily cap and
bounded retries
- Plan endpoint and plan status, fills and countdowns on the trading page
This commit is contained in:
1 parent
78ca4422af
commit
1fcf41952c
12 files changed
+1192
-67
No files matched your search
@@ -0,0 +1,68 @@
|
||||
"""自动分析的排队:触发式(成交、止盈、平仓、到期、条件满足)和例行。只在交易中排队,手动分析走 API"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from alphapulse.ai.tools import LOCAL_TZ
|
||||
from alphapulse.core.db import SessionLocal
|
||||
from alphapulse.models import TradingAccount, TradingAnalysis, TradingEvent, TradingSymbol
|
||||
from alphapulse.trading.plan import DAILY_ANALYSIS_LIMIT, MIN_ANALYSIS_GAP, next_routine_at
|
||||
|
||||
# 自动分析失败后的重试:间隔、连续失败上限
|
||||
RETRY_AFTER = timedelta(minutes=5)
|
||||
MAX_CONSECUTIVE_FAILURES = 3
|
||||
|
||||
|
||||
async def daily_limit_reached(session: AsyncSession) -> bool:
|
||||
today = datetime.now(LOCAL_TZ).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
count = await session.scalar(select(func.count()).where(TradingAnalysis.started_at >= today))
|
||||
if count < DAILY_ANALYSIS_LIMIT:
|
||||
return False
|
||||
noticed = await session.scalar(select(func.count()).where(TradingEvent.kind == "daily_limit", TradingEvent.created_at >= today))
|
||||
if not noticed:
|
||||
session.add(TradingEvent(kind="daily_limit", level="warn",
|
||||
message=f"今日自动分析已达 {DAILY_ANALYSIS_LIMIT} 次上限,之后只执行程序规则(止损、止盈、到期平仓等)"))
|
||||
return True
|
||||
|
||||
|
||||
async def request_analysis(session: AsyncSession, symbol: str, reason: str) -> None:
|
||||
"""排队一次自动分析:多个原因合并;距上次分析不足最短间隔时顺延。调用方负责 commit"""
|
||||
account = await session.get(TradingAccount, 1)
|
||||
target = await session.get(TradingSymbol, symbol)
|
||||
if account is None or not account.running or target is None or await daily_limit_reached(session):
|
||||
return
|
||||
now = datetime.now(UTC)
|
||||
last_started = await session.scalar(select(func.max(TradingAnalysis.started_at)).where(TradingAnalysis.symbol == symbol))
|
||||
at = max(now, last_started + MIN_ANALYSIS_GAP) if last_started else now
|
||||
target.analysis_requested_at = min(target.analysis_requested_at, at) if target.analysis_requested_at else at
|
||||
if reason not in target.analysis_reasons:
|
||||
# JSONB 列要整体赋值才会被识别为修改
|
||||
target.analysis_reasons = [*target.analysis_reasons, reason]
|
||||
|
||||
|
||||
async def routine_requests() -> None:
|
||||
"""交易中:到例行时间的币排队例行分析;自动分析失败的隔一段时间重试"""
|
||||
async with SessionLocal() as session:
|
||||
account = await session.get(TradingAccount, 1)
|
||||
if account is None or not account.running:
|
||||
return
|
||||
now = datetime.now(UTC)
|
||||
for target in (await session.scalars(select(TradingSymbol))).all():
|
||||
if target.analysis_requested_at is not None:
|
||||
continue
|
||||
recent = (await session.scalars(
|
||||
select(TradingAnalysis).where(TradingAnalysis.symbol == target.symbol)
|
||||
.order_by(TradingAnalysis.id.desc()).limit(MAX_CONSECUTIVE_FAILURES)
|
||||
)).all()
|
||||
if recent and recent[0].status == "running":
|
||||
continue
|
||||
if not recent or now >= next_routine_at(recent[0].started_at, target.angle):
|
||||
await request_analysis(session, target.symbol, "例行分析")
|
||||
continue
|
||||
last = recent[0]
|
||||
all_failed = len(recent) == MAX_CONSECUTIVE_FAILURES and all(a.status == "failed" for a in recent)
|
||||
if last.status == "failed" and last.reasons != ["手动分析"] and not all_failed and now >= last.finished_at + RETRY_AFTER:
|
||||
await request_analysis(session, target.symbol, f"重试:{';'.join(last.reasons)}")
|
||||
await session.commit()
|
||||
Reference in new issue
Block a user