Run demo trading and allocate capital per symbol

Move running, running_changed_at and capital_usdt from the account to
each trading symbol; the migration copies the account capital to every
symbol and leaves them all stopped. Each symbol is sized against its
own capital with a 3x notional cap, and the capitals together must not
exceed account equity. Scheduling, dry-run decisions and the executor
follow the per-symbol flag. The global start/stop button is replaced by
start/stop and capital controls on each symbol's plan card.
This commit is contained in:
eezd committed 2026-10-16 13:44:02 +08:00
1 parent e3242f2b7a
commit 0397e30870
14 files changed
+262 -186

No files matched your search

+14 -13
View File
@@ -64,7 +64,7 @@ INSTRUCTIONS = f"""你是 AlphaPulse 的模拟盘交易分析器:为一个币
- 止损:区间远端外 {rules.STOP_ATR_FACTOR}×ATR14(5m),做多放在 zone_low 下方,做空放在 zone_high 上方;且距挂单价至少 {rules.MIN_STOP_ATR}×ATR14(5m),挂单价贴近远端时按这条算。
- 止盈:按成交均价,做多 {_ladder("long")};做空 {_ladder("short")}。止盈 1 成交后撤销未成交的入场部分,止损移到保本价。
- 首次成交后最长持仓 {rules.MAX_HOLD_HOURS} 小时,到时市价平仓。平仓后程序会请你重新分析。
- 仓位:单笔风险为交易资金的 {_pct(rules.RISK_PCT)},置信度低时减半;名义仓位合计不超过 {rules.MAX_LEVERAGE} 倍交易资金。
- 仓位:单笔风险为该币交易资金的 {_pct(rules.RISK_PCT)},置信度低时减半;名义仓位不超过 {rules.MAX_LEVERAGE} 倍该币交易资金。
- reevaluate:任一条件满足时程序重新请你分析(最多 {rules.MAX_REEVALUATE} 个),用来表达另一侧方向、需要重新划区的位置。
## 程序的硬性校验
@@ -137,15 +137,16 @@ async def prepare(symbol: str, reasons: list[str], analysis_id: int) -> Prepared
target = await session.get(TradingSymbol, symbol)
if target is None:
raise AnalysisError(f"{symbol} 已不在交易列表中")
if account is None or account.capital_usdt is None:
raise AnalysisError("还没有设置交易资金")
if account is None:
raise AnalysisError("还没有填写模拟盘 key")
if target.capital_usdt is None:
raise AnalysisError(f"{symbol} 还没有设置交易资金")
provider = await session.get(AiProvider, account.provider_id) if account.provider_id else await session.scalar(
select(AiProvider).where(AiProvider.is_default)
)
if provider is None:
raise AnalysisError("没有可用的模型配置,请先到「环境变量」页面添加")
session.expunge(provider)
symbols = list((await session.scalars(select(TradingSymbol.symbol))).all())
recent = (await session.scalars(
select(TradingAnalysis)
.where(TradingAnalysis.symbol == symbol, TradingAnalysis.status == "done", TradingAnalysis.id != analysis_id)
@@ -163,20 +164,20 @@ async def prepare(symbol: str, reasons: list[str], analysis_id: int) -> Prepared
)).all()
] if current else []
system = "\n\n".join([INSTRUCTIONS, "## 数据概况", await data_overview(session), await skills_section(session)])
dry_run = not account.running
dry_run = not target.running
capital_usdt = target.capital_usdt
effort = account.reasoning_effort or provider.reasoning_effort
snapshot = await read_snapshot()
if not snapshot or not snapshot["account"]:
raise AnalysisError("取不到模拟盘账户快照(引擎未同步到币安),无法计算仓位")
equity = Decimal(snapshot["account"]["margin_balance"])
capital = min(account.capital_usdt, equity)
capital = min(capital_usdt, equity)
position = next((p for p in snapshot["positions"] if p["symbol"] == symbol), None)
other_notional = sum((Decimal(p["notional"]) for p in snapshot["positions"] if p["symbol"] != symbol and p["symbol"] in symbols), Decimal(0))
symbol_rules_ = await symbol_rules(symbol)
mark = await mark_price(symbol)
context = PlanContext(
mark_price=mark, capital=capital, other_notional=other_notional, rules=symbol_rules_, atr=await recent_atr(symbol),
mark_price=mark, capital=capital, rules=symbol_rules_, atr=await recent_atr(symbol),
has_plan=current is not None, position_side=position_side(snapshot, symbol),
)
@@ -186,12 +187,12 @@ async def prepare(symbol: str, reasons: list[str], analysis_id: int) -> Prepared
"## 任务",
f"- 币种:{symbol}(币安 U 本位永续)",
f"- 触发原因:{';'.join(reasons)}",
f"- 运行模式:{'试运行(引擎已停止,计划只记录、不下单)' if dry_run else '交易中(计划会在模拟盘执行)'}",
f"- 运行模式:{'试运行(该币未在交易,计划只记录、不下单)' if dry_run else '交易中(计划会在模拟盘执行)'}",
"",
"## 账户(程序据此计算数量)",
f"- 交易资金:{capital:.2f} USDT(设定 {account.capital_usdt} USDT,账户权益 {equity:.2f} USDT,取较小者)",
f"- 该币交易资金:{capital:.2f} USDT(设定 {capital_usdt} USDT,账户权益 {equity:.2f} USDT,取较小者)",
f"- 本次单笔风险金额:{risk:.2f} USDT({_pct(rules.RISK_PCT)};置信度低时 {risk * rules.LOW_CONFIDENCE_FACTOR:.2f})",
f"- 可用名义仓位:{capital * rules.MAX_LEVERAGE - other_notional:.2f} USDT({rules.MAX_LEVERAGE} 倍交易资金减去交易列表其他币的持仓 {other_notional:.2f})",
f"- 名义仓位上限:{capital * rules.MAX_LEVERAGE:.2f} USDT({rules.MAX_LEVERAGE} 倍该币交易资金)",
f"- 模拟盘交易规则:价格精度 {symbol_rules_.tick_size},数量精度 {symbol_rules_.step_size},最小下单金额 {symbol_rules_.min_notional} USDT",
f"- 模拟盘标记价:{mark}(校验以此为准;行情数据来自实盘,两者通常相差不到 0.1%)",
f"- 最近已收盘 5m K 线的 ATR14:{context.atr:.{max(0, -symbol_rules_.tick_size.as_tuple().exponent)}f}(程序按它计算止损)",
@@ -266,8 +267,8 @@ async def submit(prepared: Prepared, symbol: str, analysis_id: int, arguments: d
async def run_analysis(symbol: str, reasons: list[str]) -> None:
"""一次完整的分析;结果和失败原因都写进 trading_analyses 和时间线,不向外抛出"""
async with SessionLocal() as session:
account = await session.get(TradingAccount, 1)
analysis = TradingAnalysis(symbol=symbol, reasons=reasons, dry_run=not (account and account.running), status="running")
target = await session.get(TradingSymbol, symbol)
analysis = TradingAnalysis(symbol=symbol, reasons=reasons, dry_run=not (target and target.running), status="running")
session.add(analysis)
await session.commit()
analysis_id, dry_run = analysis.id, analysis.dry_run