Replace cookie authentication with revocable Bearer sessions, optional one-use PNG challenges, bounded login limits, and short-lived market WebSocket tickets. Keep captcha disabled by default. Standardize ordinary JSON responses while preserving HTTP statuses, raw OpenAPI, redirects, SSE, and WebSocket semantics. Revoke long connections with their owning session. Expose official calendar data and collector status to the frontend and research tools, support bilingual news topics, and keep summaries and forward-only labels explicit. Verify the staged snapshot with all 305 backend tests.
65 lines
3.6 KiB
Python
65 lines
3.6 KiB
Python
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())).data
|
|
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())
|