Files
eezd 010d89e0f5 Rebuild position history from Binance fills
Sync account fills into a new trading_fills table every 60s and
reconstruct closed positions from them, so positions opened outside
the program are included and external closes get an exit price.
Positions link to plans by order id. The history drawer gains symbol,
close-date, outcome and source filters, a summary bar and pagination.
2026-10-16 11:57:45 +08:00

77 lines
2.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from alphapulse.trading.positions import build_positions
T0 = datetime(2026, 10, 15, 12, tzinfo=UTC)
@dataclass
class Fill:
symbol: str
order_id: int
side: str
price: Decimal
qty: Decimal
realized_pnl: Decimal
commission: Decimal
commission_asset: str
time: datetime
def fill(trade_id: int, side: str, qty: str, price: str, pnl: str = "0", fee: str = "0.1", asset: str = "USDT",
symbol: str = "BTCUSDT", order_id: int | None = None) -> Fill:
return Fill(
symbol=symbol, order_id=order_id or trade_id, side=side, qty=Decimal(qty), price=Decimal(price),
realized_pnl=Decimal(pnl), commission=Decimal(fee), commission_asset=asset, time=T0 + timedelta(seconds=trade_id),
)
def test_open_and_close_once():
[p] = build_positions([fill(1, "BUY", "0.01", "80000"), fill(2, "SELL", "0.01", "80500", pnl="5")])
assert (p.direction, p.quantity, p.entry_price, p.exit_price) == ("long", Decimal("0.01"), 80000, 80500)
assert (p.realized_pnl, p.commission, p.net) == (5, Decimal("0.2"), Decimal("4.8"))
assert (p.opened_at, p.closed_at) == (T0 + timedelta(seconds=1), T0 + timedelta(seconds=2))
def test_partial_closes_average_exit_price():
# #40:0.0126 多单分三档止盈
[p] = build_positions([
fill(1, "BUY", "0.0126", "80790"),
fill(2, "SELL", "0.0075", "81193.9", pnl="3.02925"),
fill(3, "SELL", "0.0037", "81395.9", pnl="2.24183"),
fill(4, "SELL", "0.0014", "81597.9", pnl="1.13106"),
])
assert p.quantity == Decimal("0.0126")
assert round(p.exit_price, 2) == Decimal("81298.11")
assert p.realized_pnl == Decimal("6.40214")
def test_flip_through_zero_splits_positions():
# 空 0.01 后一笔买 0.03:先平空 0.01,再开多 0.02;手续费按数量拆分
first, second = build_positions([
fill(1, "SELL", "0.01", "80000"),
fill(2, "BUY", "0.03", "79000", pnl="10", fee="0.3"),
fill(3, "SELL", "0.02", "79500", pnl="10"),
])
assert (first.direction, first.quantity, first.exit_price, first.realized_pnl) == ("short", Decimal("0.01"), 79000, 10)
assert first.commission == Decimal("0.2")
assert (second.direction, second.quantity, second.entry_price) == ("long", Decimal("0.02"), 79000)
assert second.commission == Decimal("0.3")
assert second.opened_at == first.closed_at
def test_open_position_is_not_history_and_symbols_are_independent():
positions = build_positions([
fill(1, "BUY", "1", "2400", symbol="ETHUSDT"),
fill(2, "BUY", "0.01", "80000"),
fill(3, "SELL", "0.01", "80100", pnl="1"),
])
assert [p.symbol for p in positions] == ["BTCUSDT"]
def test_non_usdt_commission_is_ignored():
[p] = build_positions([fill(1, "BUY", "0.01", "80000", asset="BNB"), fill(2, "SELL", "0.01", "80000")])
assert p.commission == Decimal("0.1")