Files
AlphaPulse/backend/tests/test_research_accounting.py
T
eezd f67a8ca402 Align backend authentication and API response contracts
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.
2026-10-26 12:04:21 +08:00

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())