""" Tests for PaperExecutor.close_position() settlement payout. Regression: the old code computed cash += position_cost * resolution, which ignores direction — a winning BUY_NO (resolution = 0.0) paid out $0. Correct settlement: BUY_YES: payout = shares * resolution BUY_NO: payout = shares * (1 - resolution) pnl = payout - net_cost """ import asyncio import pytest from bot.executor import paper from bot.executor.paper import PaperExecutor class FakeDB: """Minimal Database stub for close_position().""" def __init__(self, trades_by_market: dict[str, list[dict]]): self._trades = trades_by_market self.closed: list[tuple] = [] async def get_open_trades_for_market(self, market_id: str) -> list[dict]: return self._trades.get(market_id, []) async def close_paper_position(self, market_id, reason="", resolution=None): self.closed.append((market_id, reason, resolution)) def _close(direction: str, resolution: float): """Open one paper trade (size $100 @ 0.5 → 200 shares, net_cost $102) and settle it at `resolution`. Returns (pnl, executor, notifications).""" notifications: list[tuple] = [] async def fake_trade_closed(question, pnl): notifications.append((question, pnl)) async def run(): db = FakeDB({ "mkt1": [{"direction": direction, "shares": 200.0, "net_cost": 102.0}], }) ex = PaperExecutor(db=db, bankroll=1000.0) ex._portfolio.cash = 898.0 # 1000 - net_cost spent at entry ex._portfolio.positions["mkt1"] = 100.0 # size_usdc, as execute() stores it original = paper.telegram.trade_closed paper.telegram.trade_closed = fake_trade_closed try: pnl = await ex.close_position("mkt1", resolution, question="Test market?") await asyncio.sleep(0) # let the notification task run finally: paper.telegram.trade_closed = original return pnl, ex, db pnl, ex, db = asyncio.run(run()) return pnl, ex, db, notifications def test_buy_yes_wins(): pnl, ex, db, notif = _close("BUY_YES", resolution=1.0) assert pnl == pytest.approx(200.0 - 102.0) # payout = 200 * 1.0 assert pnl > 0 assert ex._portfolio.cash == pytest.approx(898.0 + 200.0) assert notif[0][1] > 0 # Telegram reports a win def test_buy_yes_loses(): pnl, ex, db, notif = _close("BUY_YES", resolution=0.0) assert pnl == pytest.approx(-102.0) # payout = 0 assert pnl < 0 assert ex._portfolio.cash == pytest.approx(898.0) assert notif[0][1] < 0 # Telegram reports a loss def test_buy_no_wins(): pnl, ex, db, notif = _close("BUY_NO", resolution=0.0) assert pnl == pytest.approx(200.0 - 102.0) # payout = 200 * (1 - 0.0) assert pnl > 0 assert ex._portfolio.cash == pytest.approx(898.0 + 200.0) assert notif[0][1] > 0 # win despite resolution = 0.0 def test_buy_no_loses(): pnl, ex, db, notif = _close("BUY_NO", resolution=1.0) assert pnl == pytest.approx(-102.0) # payout = 200 * (1 - 1.0) = 0 assert pnl < 0 assert ex._portfolio.cash == pytest.approx(898.0) assert notif[0][1] < 0 # loss despite resolution = 1.0 def test_position_is_removed_and_persisted(): pnl, ex, db, notif = _close("BUY_YES", resolution=1.0) assert "mkt1" not in ex._portfolio.positions assert db.closed == [("mkt1", "market_resolved resolution=1.0", 1.0)] def test_unknown_market_returns_none(): async def run(): ex = PaperExecutor(db=FakeDB({}), bankroll=1000.0) return await ex.close_position("nope", 1.0) assert asyncio.run(run()) is None