Files
AlphaPulse/backend/src/alphapulse/trading/engine.py
T
eezd 1fcf41952c 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
2026-10-15 11:07:00 +08:00

83 lines
3.1 KiB
Python

"""模拟盘交易进程:同步账户快照、执行器对账、调度分析。
用法:python -m alphapulse.trading.engine
"""
import asyncio
import logging
from datetime import UTC, datetime
from sqlalchemy import select, update
from alphapulse.core.db import SessionLocal
from alphapulse.models import TradingAnalysis, TradingSymbol
from alphapulse.trading.analyst import run_analysis
from alphapulse.trading.executor import executor_loop
from alphapulse.trading.schedule import routine_requests
from alphapulse.trading.snapshot import snapshot_loop
logger = logging.getLogger("alphapulse.trading.engine")
ANALYSIS_POLL_SECONDS = 5
# 模型调用耗时长,并发过多会撞上游限流
MAX_CONCURRENT_ANALYSES = 2
async def fail_interrupted() -> None:
"""上次进程退出时还在进行的分析不会再有结果"""
async with SessionLocal() as session:
await session.execute(
update(TradingAnalysis).where(TradingAnalysis.status == "running")
.values(status="failed", error="引擎重启,分析中断", finished_at=datetime.now(UTC))
)
await session.commit()
async def claim_requests(limit: int, busy: set[str]) -> list[tuple[str, list[str]]]:
"""取出到期的分析请求并清空,同一请求里的多个触发原因合并为一次分析"""
async with SessionLocal() as session:
symbols = (await session.scalars(
select(TradingSymbol)
.where(TradingSymbol.analysis_requested_at <= datetime.now(UTC), TradingSymbol.symbol.not_in(busy))
.order_by(TradingSymbol.analysis_requested_at).limit(limit)
)).all()
claimed = [(s.symbol, list(s.analysis_reasons)) for s in symbols]
for s in symbols:
s.analysis_requested_at = None
s.analysis_reasons = []
await session.commit()
return claimed
async def analysis_loop() -> None:
await fail_interrupted()
tasks: dict[str, asyncio.Task] = {}
while True:
try:
for symbol, task in list(tasks.items()):
if task.done():
del tasks[symbol]
if not task.cancelled() and task.exception():
logger.error("%s 分析任务异常退出", symbol, exc_info=task.exception())
await routine_requests()
free = MAX_CONCURRENT_ANALYSES - len(tasks)
if free > 0:
for symbol, reasons in await claim_requests(free, set(tasks)):
tasks[symbol] = asyncio.create_task(run_analysis(symbol, reasons))
except Exception:
logger.exception("调度分析出错,稍后重试")
await asyncio.sleep(ANALYSIS_POLL_SECONDS)
async def main() -> None:
async with asyncio.TaskGroup() as tg:
tg.create_task(snapshot_loop())
tg.create_task(executor_loop())
tg.create_task(analysis_loop())
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())