import tempfile import unittest from datetime import datetime from unittest import mock import pandas as pd import overnight_paper as paper class PaperOrders: def __init__(self, responses, held=None): self.responses = iter(responses) self.held = held or [] self.pending = [] self.sent = [] self.existing = None def get(self, path, **params): if path == "/account": return {"equity": "10000"} if path == "/positions": return self.held if path == "/orders": return self.pending if path == "/orders:by_client_order_id" and self.existing: return self.existing raise RuntimeError("order not found") def order(self, payload): self.sent.append(payload.copy()) response = next(self.responses) if isinstance(response, int): response = {"http": response, "message": "insufficient buying power"} else: response = {"http": 201, "id": str(len(self.sent)), "status": "accepted", **payload, **response} if response["http"] == 201 and response.get("status") == "accepted": self.pending.append(response) return response class OvernightOrderTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) day = datetime(2026, 10, 12, 15, 40, tzinfo=paper.NY) self.day = day calendar = pd.DataFrame([{"date": day.date(), "open": day.replace(hour=9, minute=30), "close": day.replace(hour=16, minute=0)}]).set_index("date") class Clock(datetime): @classmethod def now(cls, tz=None): return day.astimezone(tz) patches = [mock.patch.object(paper, "OUT", self.tmp.name), mock.patch.object(paper, "datetime", Clock), mock.patch.object(paper, "now_utc", return_value=day), mock.patch.object(paper, "sessions", return_value=calendar), mock.patch.object(paper, "compute_signal", return_value={ "top": ["AAA", "BBB"], "prev_close": {"AAA": 100, "BBB": 100}, "bars_through": "2026-10-09"}), mock.patch.object(paper, "latest_prices", return_value={"AAA": 100, "BBB": 100})] for patch in patches: patch.start() self.addCleanup(patch.stop) def test_partial_buy_failure_retries_only_unaccepted_orders(self): client = PaperOrders([{}, 403, {}]) with self.assertRaisesRegex(RuntimeError, "HTTP 403"): paper.buy(client) self.assertNotIn("bought", paper._state()) paper.buy(client) self.assertEqual([o["symbol"] for o in client.sent], ["AAA", "BBB", "BBB"]) self.assertEqual(paper._state()["bought"], "2026-10-12") paper.buy(client) self.assertEqual(len(client.sent), 3) def test_partial_sell_failure_does_not_mark_the_session_complete(self): client = PaperOrders([{}, 403, {}], [{"symbol": "AAA", "qty": "10"}, {"symbol": "BBB", "qty": "20"}]) with mock.patch.object(paper, "now_utc", return_value=self.day.replace(hour=9, minute=10)): with self.assertRaisesRegex(RuntimeError, "HTTP 403"): paper.sell(client) self.assertNotIn("sold", paper._state()) paper.sell(client) self.assertEqual([o["symbol"] for o in client.sent], ["AAA", "BBB", "BBB"]) self.assertEqual(paper._state()["sold"], "2026-10-12") def test_duplicate_is_success_only_for_the_same_accepted_order(self): payload = {"symbol": "AAA", "qty": "10", "side": "buy", "type": "market", "time_in_force": "cls", "client_order_id": "onb-2026-10-12-AAA"} for status, quantity, succeeds in [("accepted", "10", True), ("rejected", "10", False), ("accepted", "11", False)]: with self.subTest(status=status, quantity=quantity): client = PaperOrders([{"http": 422, "message": "client_order_id must be unique"}]) client.existing = {**payload, "id": "existing", "status": status, "qty": quantity} if succeeds: paper._submit(client, "moc", payload, False) else: with self.assertRaises(RuntimeError): paper._submit(client, "moc", payload, False) def test_http_success_with_rejected_status_is_not_completion(self): client = PaperOrders([{"status": "rejected"}]) with self.assertRaisesRegex(RuntimeError, "rejected"): paper.buy(client) self.assertNotIn("bought", paper._state()) if __name__ == "__main__": unittest.main()