Support selectable demo exchanges and configurable analysis limits

Persist manual requests until exchange confirmation, preserve stop protection, and reconcile fills before settling plans. Tighten model stream, crawler, skill, research accounting, and frontend race handling, with regression coverage. Keep queued analyses pending until a fresh account snapshot is available.
This commit is contained in:
eezd committed 2026-10-20 18:45:00 +08:00
1 parent fcaedfb358
commit 285b3952d7
33 files changed
+1890 -325

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
import asyncio
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from types import SimpleNamespace
import pytest
def test_forward_endpoint_excludes_labels_at_or_after_entry(trading):
from alphapulse.api.trading import list_news_signals
from alphapulse.trading.news_signals import is_forward_sample
published = datetime(2026, 10, 20, 12, 0, 59, tzinfo=UTC)
entry = published.replace(second=0) + timedelta(minutes=5)
assert is_forward_sample(published, entry - timedelta(microseconds=1))
assert not is_forward_sample(published, entry)
prices = {symbol: {"entry": "100", "1h": "101", "4h": "102"} for symbol in ("BTCUSDT", "ETHUSDT")}
rows = [(SimpleNamespace(news_id=i, labeled_at=labeled, category="event", direction="bullish", prices=prices), published, "test", "新闻")
for i, labeled in enumerate((entry - timedelta(seconds=1), entry, entry + timedelta(hours=5)), 1)]
class Session:
async def execute(self, statement):
return SimpleNamespace(all=lambda: rows)
result = asyncio.run(list_news_signals(Session()))
assert result.labeled == result.filled == 1
assert [item.news_id for item in result.recent] == [1]
assert all(s.n == 1 for s in result.stats)
@pytest.mark.parametrize("scenario", ["complete", "missing_close", "partial_entry"])
def test_plan_pnl_includes_early_entry_fee_and_manual_close_only(trading, monkeypatch, scenario):
from alphapulse.models import TradingFill
start = datetime(2026, 10, 20, 10, tzinfo=UTC)
fills = [TradingFill(symbol="BTCUSDT", order_id=order_id, side=side, qty=Decimal(1), price=Decimal(price),
realized_pnl=Decimal(pnl), commission=Decimal("0.1"), commission_asset="USDT", time=start + timedelta(seconds=seconds))
for seconds, order_id, side, price, pnl in [(1, "entry", "BUY", "100", "0"), (120, "manual", "SELL", "102", "2"),
(130, "other", "BUY", "100", "0"), (140, "other-close", "SELL", "200", "100")]
if not (scenario == "missing_close" and order_id == "manual")]
if scenario == "partial_entry":
# 同一张限价单在前一笔仓位归零的同一毫秒继续成交,后一笔平仓记录尚未同步。
fills.insert(2, TradingFill(symbol="BTCUSDT", order_id="entry", side="BUY", qty=Decimal(1), price=Decimal(100),
realized_pnl=Decimal(0), commission=Decimal("0.1"), commission_asset="USDT", time=start + timedelta(seconds=120)))
class Session:
async def scalars(self, query):
return fills
class Client:
exchange = "bybit"
async def trades(self, symbol, since_ms):
assert since_ms <= int(start.timestamp() * 1000)
return []
async def save(*args):
pass
monkeypatch.setattr(trading, "save_fills", save)
async def check():
engine = trading.Executor()
p = SimpleNamespace(symbol="BTCUSDT", first_fill_at=start + timedelta(seconds=90))
entry = SimpleNamespace(purpose="entry", filled_qty=Decimal(2 if scenario == "partial_entry" else 1), exchange_id="entry", created_at=start)
try:
if scenario != "complete":
with pytest.raises(trading.ExchangeError, match="完整的开平仓成交"):
await engine.realized(Session(), Client(), p, [entry])
else:
assert await engine.realized(Session(), Client(), p, [entry]) == {"realized_pnl": "2", "commission": "0.2", "net": "1.8"}
finally:
await engine.market.http.aclose()
asyncio.run(check())