import os import unittest from unittest.mock import Mock, patch os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test") from src.services.execution import ( AccountRiskState, RiskContext, build_execution_observation_from_backtest_result, build_execution_ticket_snapshot, ExecutionService, classify_execution_lifecycle, compute_entry_price, evaluate_account_risk_limits, evaluate_execution_decision, trade_execution_matches_decision, ) class ExecutionDecisionTests(unittest.TestCase): def test_allows_paper_execution_for_valid_signal(self) -> None: decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "medium", "meta": {"execution_permission": True}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, ) self.assertTrue(decision.allowed) self.assertEqual(decision.status, "open") self.assertEqual(decision.execution_mode, "paper") self.assertGreater(decision.size, 0.0) self.assertEqual(decision.reasons, []) def test_semi_auto_uses_pending_status_when_allowed(self) -> None: decision = evaluate_execution_decision( signal={ "side": "sell", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 102.0, "tp1": 99.0, "tp2": 97.0, "session_code": "NY_AM", "execution_intensity": "high", "meta": {"execution_permission": True}, }, execution_mode="semi_auto", account_equity=12000.0, risk_pct=0.01, ) self.assertTrue(decision.allowed) self.assertEqual(decision.status, "pending") self.assertEqual(decision.execution_mode, "semi_auto") self.assertTrue(decision.requires_confirmation) def test_rejects_signal_without_stop_or_allowed_session(self) -> None: decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_context", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": None, "tp1": 102.0, "tp2": 104.0, "session_code": "ASIA", "execution_intensity": "low", "meta": {"execution_permission": False}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.03, ) self.assertFalse(decision.allowed) self.assertEqual(decision.status, "rejected") self.assertIn("missing_stop_loss", decision.reasons) self.assertIn("session_not_allowed", decision.reasons) self.assertIn("signal_not_executable", decision.reasons) self.assertIn("signal_execution_permission_denied", decision.reasons) self.assertIn("bias_execution_intensity_low", decision.reasons) self.assertIn("risk_pct_out_of_bounds", decision.reasons) def test_account_risk_limits_reject_execution(self) -> None: account_state = AccountRiskState( daily_pnl_pct=-0.02, consecutive_losses=3, open_positions=1, same_side_open_positions=1, ) decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "medium", "meta": {"execution_permission": True}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, account_state=account_state, ) self.assertFalse(decision.allowed) self.assertEqual(decision.status, "rejected") self.assertEqual( decision.reasons, [ "daily_loss_limit_reached", "consecutive_loss_limit_reached", "max_open_positions_reached", "same_side_exposure_limit_reached", ], ) self.assertEqual(decision.meta["account_risk"]["consecutive_losses"], 3) def test_account_risk_limits_allow_clean_state(self) -> None: self.assertEqual( evaluate_account_risk_limits( AccountRiskState( daily_pnl_pct=-0.005, consecutive_losses=1, open_positions=0, same_side_open_positions=0, ) ), [], ) def test_account_risk_kill_switch_rejects_execution(self) -> None: decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "medium", "meta": {"execution_permission": True}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, account_state=AccountRiskState(kill_switch=True), ) self.assertFalse(decision.allowed) self.assertEqual(decision.status, "rejected") self.assertIn("execution_kill_switch_enabled", decision.reasons) self.assertTrue(decision.meta["account_risk"]["kill_switch"]) def test_rejects_modeled_signal_without_structural_targets(self) -> None: decision = evaluate_execution_decision( signal={ "model_code": "SWEEP_MSS_FVG", "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "medium", "meta": {"execution_permission": True, "target_plan": {}, "management_rules": []}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, ) self.assertFalse(decision.allowed) self.assertIn("missing_structural_targets", decision.reasons) self.assertIn("missing_management_rules", decision.reasons) class ExecutionServiceTests(unittest.TestCase): def test_semi_auto_creation_requires_explicit_confirmation(self) -> None: repository = Mock() repository.fetch_signal_for_execution.return_value = { "id": 1, "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "high", "meta": {"execution_permission": True}, } repository.fetch_latest_execution.return_value = None repository.insert_trade_execution.return_value = 88 service = ExecutionService(repository=repository) execution_id = service.create_execution(signal_id=1, execution_mode="semi_auto") self.assertIsNone(execution_id) repository.insert_trade_execution.assert_not_called() def test_semi_auto_creation_allows_confirmed_execution(self) -> None: repository = Mock() repository.fetch_signal_for_execution.return_value = { "id": 1, "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "high", "meta": {"execution_permission": True}, } repository.fetch_latest_execution.return_value = None repository.insert_trade_execution.return_value = 89 service = ExecutionService(repository=repository) execution_id = service.create_execution(signal_id=1, execution_mode="semi_auto", confirmed=True) self.assertIsNone(execution_id) repository.insert_trade_execution.assert_not_called() def test_semi_auto_ticket_must_be_confirmed_before_pending_execution(self) -> None: repository = Mock() ticket_row = { "id": 51, "signal_id": 1, "execution_mode": "semi_auto", "status": "awaiting_confirmation", "created_ts": "created", "expires_ts": "later", "confirmed_by": None, "confirmed_at": None, "execution_id": None, "ticket_snapshot": {}, } repository.fetch_signal_for_execution.return_value = _valid_signal() repository.fetch_latest_execution_ticket.return_value = None repository.insert_execution_ticket.return_value = 51 repository.fetch_execution_ticket_by_id.side_effect = lambda ticket_id: dict(ticket_row) repository.fetch_latest_execution.return_value = None repository.insert_trade_execution.return_value = 89 service = ExecutionService(repository=repository) service.build_risk_context_for_signal = Mock(return_value=RiskContext()) repository.update_execution_ticket.side_effect = lambda **kwargs: ticket_row.update( { "status": kwargs["status"], "ticket_snapshot": kwargs["ticket_snapshot"], "confirmed_by": kwargs.get("confirmed_by"), "confirmed_at": kwargs.get("confirmed_at"), "execution_id": kwargs.get("execution_id"), } ) ticket = service.create_execution_ticket(signal_id=1, execution_mode="semi_auto") self.assertIsNotNone(ticket) repository.insert_trade_execution.assert_not_called() confirmed = service.confirm_execution_ticket(51, confirmed_by="alice") self.assertIsNotNone(confirmed) assert confirmed is not None self.assertEqual(confirmed.status, "confirmed") repository.insert_trade_execution.assert_called_once() kwargs = repository.insert_trade_execution.call_args.kwargs self.assertEqual(kwargs["status"], "pending") self.assertEqual(kwargs["meta"]["execution_ticket_id"], 51) self.assertEqual(kwargs["meta"]["confirmed_by"], "alice") def test_ticket_rejects_when_risk_check_fails_before_creation(self) -> None: repository = Mock() repository.fetch_signal_for_execution.return_value = _valid_signal() repository.fetch_latest_execution_ticket.return_value = None repository.insert_execution_ticket.return_value = 61 service = ExecutionService(repository=repository) service.build_risk_context_for_signal = Mock( return_value=RiskContext.from_account_state(AccountRiskState(kill_switch=True)) ) ticket = service.create_execution_ticket( signal_id=1, execution_mode="semi_auto", risk_context=None, account_state=AccountRiskState(kill_switch=True), ) self.assertIsNotNone(ticket) assert ticket is not None self.assertEqual(ticket.status, "rejected") self.assertIn("execution_kill_switch_enabled", ticket.reasons) repository.insert_trade_execution.assert_not_called() def test_confirm_ticket_rejects_when_recheck_fails(self) -> None: repository = Mock() ticket_row = { "id": 71, "signal_id": 1, "execution_mode": "semi_auto", "status": "awaiting_confirmation", "created_ts": "now", "expires_ts": None, "confirmed_by": None, "confirmed_at": None, "execution_id": None, "ticket_snapshot": {}, } repository.fetch_signal_for_execution.return_value = _valid_signal() repository.fetch_execution_ticket_by_id.side_effect = lambda ticket_id: dict(ticket_row) repository.fetch_latest_execution.return_value = None service = ExecutionService(repository=repository) service.build_risk_context_for_signal = Mock( return_value=RiskContext.from_account_state(AccountRiskState(kill_switch=True)) ) repository.update_execution_ticket.side_effect = lambda **kwargs: ticket_row.update( { "status": kwargs["status"], "ticket_snapshot": kwargs["ticket_snapshot"], "confirmed_by": kwargs.get("confirmed_by"), "confirmed_at": kwargs.get("confirmed_at"), "execution_id": kwargs.get("execution_id"), } ) confirmed = service.confirm_execution_ticket( 71, confirmed_by="bob", account_state=AccountRiskState(kill_switch=True), ) self.assertIsNotNone(confirmed) assert confirmed is not None self.assertEqual(confirmed.status, "rejected") repository.insert_trade_execution.assert_not_called() repository.update_execution_ticket.assert_called_once() def test_ticket_snapshot_is_complete_for_replay(self) -> None: repository = Mock() repository.fetch_signal_for_execution.return_value = _valid_signal() risk_context = RiskContext() decision = evaluate_execution_decision( signal=_valid_signal(), execution_mode="paper", account_equity=10000.0, risk_pct=0.01, ) snapshot = build_execution_ticket_snapshot( signal=_valid_signal(), decision=decision, risk_context=risk_context, execution_mode="semi_auto", created_ts="created", status="awaiting_confirmation", account_equity=10000.0, risk_pct=0.01, ) self.assertEqual(snapshot["signal"]["signal_id"], 1) self.assertEqual(snapshot["decision"]["status"], "open") self.assertIn("risk_context", snapshot) self.assertEqual(snapshot["execution_mode"], "semi_auto") self.assertEqual(snapshot["signal"]["model_code"], "SWEEP_MSS_FVG") def test_paper_ticket_confirmation_creates_open_execution(self) -> None: repository = Mock() ticket_row = { "id": 81, "signal_id": 1, "execution_mode": "paper", "status": "awaiting_confirmation", "created_ts": "created", "expires_ts": "later", "confirmed_by": None, "confirmed_at": None, "execution_id": None, "ticket_snapshot": {}, } repository.fetch_signal_for_execution.return_value = _valid_signal() repository.fetch_latest_execution_ticket.return_value = None repository.insert_execution_ticket.return_value = 81 repository.fetch_execution_ticket_by_id.side_effect = lambda ticket_id: dict(ticket_row) repository.fetch_latest_execution.return_value = None repository.insert_trade_execution.return_value = 91 repository.update_execution_ticket.side_effect = lambda **kwargs: ticket_row.update( { "status": kwargs["status"], "ticket_snapshot": kwargs["ticket_snapshot"], "confirmed_by": kwargs.get("confirmed_by"), "confirmed_at": kwargs.get("confirmed_at"), "execution_id": kwargs.get("execution_id"), } ) service = ExecutionService(repository=repository) service.build_risk_context_for_signal = Mock(return_value=RiskContext()) ticket = service.create_execution_ticket(signal_id=1, execution_mode="paper") self.assertIsNotNone(ticket) confirmed = service.confirm_execution_ticket(81, confirmed_by="paper_user") self.assertIsNotNone(confirmed) assert confirmed is not None self.assertEqual(confirmed.status, "confirmed") kwargs = repository.insert_trade_execution.call_args.kwargs self.assertEqual(kwargs["status"], "open") self.assertEqual(kwargs["meta"]["execution_ticket_id"], 81) self.assertEqual(kwargs["meta"]["confirmed_by"], "paper_user") def test_submit_confirmed_ticket_creates_execution_and_updates_ticket(self) -> None: repository = Mock() ticket_row = { "id": 91, "signal_id": 1, "execution_mode": "semi_auto", "status": "confirmed", "created_ts": "created", "expires_ts": "later", "confirmed_by": "alice", "confirmed_at": "2026-05-06T00:00:00+00:00", "execution_id": None, "ticket_snapshot": {}, } repository.fetch_execution_ticket_by_id.side_effect = lambda ticket_id: dict(ticket_row) repository.fetch_signal_for_execution.return_value = _valid_signal() repository.fetch_latest_execution.return_value = None repository.insert_trade_execution.return_value = 101 repository.update_execution_ticket.side_effect = lambda **kwargs: ticket_row.update( { "status": kwargs["status"], "ticket_snapshot": kwargs["ticket_snapshot"], "execution_id": kwargs.get("execution_id"), } ) service = ExecutionService(repository=repository) service.build_risk_context_for_signal = Mock(return_value=RiskContext()) result = service.submit_execution_ticket(91) self.assertIsNotNone(result) assert result is not None self.assertEqual(result.status, "submitted") self.assertEqual(result.execution_id, 101) repository.insert_trade_execution.assert_called_once() repository.update_execution_ticket.assert_called_once() def test_build_risk_context_for_signal_embeds_broker_state(self) -> None: repository = Mock() repository.fetch_latest_backtest_quality_gate.return_value = {"passed": True} market_state_service = Mock() market_state_service.build_market_state.return_value = {"freshness": {"code": "market_data_fresh"}} market_state_service.repository = repository service = ExecutionService(repository=Mock(), market_state_service=market_state_service) from src.services.broker.broker_read_model_service import BrokerReadModelSnapshot with patch("src.services.execution.execution_service.BrokerReadModelService.fetch_snapshot") as fetch_snapshot, patch( "src.services.execution.execution_service.BrokerReadModelService.build_account_risk_state" ) as build_account_state: fetch_snapshot.return_value = BrokerReadModelSnapshot( status="broker_state_ok", provider="okx_read_only", fetched_ts=None, account={"equity": 1000.0, "daily_pnl": -10.0}, positions=[{"instrument_id": "BTC-USDT-SWAP", "side": "buy"}], orders=[], fills=[], alerts=[], error=None, ) build_account_state.return_value = AccountRiskState(open_positions=1, same_side_open_positions=1) context = service.build_risk_context_for_signal(_valid_signal()) self.assertEqual(context.open_positions, 1) self.assertEqual(context.same_side_open_positions, 1) self.assertEqual(context.broker_state["status"], "broker_state_ok") fetch_snapshot.assert_called_once() build_account_state.assert_called_once() if __name__ == "__main__": unittest.main() def test_close_execution_by_id_sets_closed_state(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = {"id": 13, "status": "open", "meta": {}, "closed_ts": None} service = ExecutionService(repository=repository) settled = service.close_execution_by_id(13) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["close_reason"], "manual_close") class ExecutionAuditTests(unittest.TestCase): def test_classifies_lifecycle_states(self) -> None: self.assertEqual(classify_execution_lifecycle("pending", None), "awaiting_confirmation") self.assertEqual(classify_execution_lifecycle("open", {"tp1_hit": True, "tp2_hit": False, "outcome": "breakeven", "invalidated_before_entry": False}), "tp1_reached") self.assertEqual(classify_execution_lifecycle("open", {"tp1_hit": True, "tp2_hit": True, "outcome": "win", "invalidated_before_entry": False}), "tp2_closed") self.assertEqual(classify_execution_lifecycle("open", {"tp1_hit": False, "tp2_hit": False, "outcome": "loss", "invalidated_before_entry": False}), "stopped_out") self.assertEqual(classify_execution_lifecycle("open", {"tp1_hit": False, "tp2_hit": False, "outcome": "missed", "invalidated_before_entry": False}), "missed") self.assertEqual(classify_execution_lifecycle("open", {"tp1_hit": False, "tp2_hit": False, "outcome": "missed", "invalidated_before_entry": True}), "invalidated_before_entry") self.assertEqual(classify_execution_lifecycle("cancelled", None), "cancelled") self.assertEqual(classify_execution_lifecycle("partially_closed", None), "tp1_reached") def test_audit_execution_reports_review_state(self) -> None: audit = ExecutionService().audit_execution( trade_execution={"status": "rejected", "meta": {"risk_checks": ["session_not_allowed"]}}, backtest_result={"outcome": "loss", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": False}, ) self.assertEqual(audit["audit_status"], "review") self.assertFalse(audit["is_actionable"]) self.assertIn("execution_rejected", audit["error_tags"]) self.assertIn("session_not_allowed", audit["error_tags"]) self.assertEqual(audit["lifecycle_state"], "rejected") self.assertEqual(audit["backtest_lifecycle_state"], "stopped_out") self.assertTrue(audit["stop_loss_hit"]) def test_apply_execution_observation_supports_manual_cancellation(self) -> None: settled = ExecutionService().apply_execution_observation( trade_execution={"status": "pending", "meta": {}, "closed_ts": None}, observation={"source": "manual", "status": "cancelled", "close_reason": "cancelled"}, ) self.assertEqual(settled["status"], "cancelled") self.assertEqual(settled["meta"]["lifecycle_state"], "cancelled") self.assertEqual(settled["meta"]["settlement_source"], "manual") self.assertEqual(settled["meta"]["position_state"], "cancelled") self.assertIsNotNone(settled["closed_ts"]) def test_apply_execution_observation_marks_tp1_progress_as_partially_closed(self) -> None: settled = ExecutionService().apply_execution_observation( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, observation={"source": "manual", "entry_triggered": True, "tp1_hit": True}, ) self.assertEqual(settled["status"], "partially_closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp1_reached") self.assertEqual(settled["meta"]["position_state"], "partially_closed") self.assertEqual(settled["meta"]["fill_state"], "filled") def test_settle_execution_from_backtest_closes_tp2_hit(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "win", "tp1_hit": True, "tp2_hit": True, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp2_closed") self.assertTrue(settled["meta"]["tp1_hit"]) self.assertTrue(settled["meta"]["tp2_hit"]) self.assertEqual(settled["meta"]["settlement_source"], "backtest_projection") self.assertIsNotNone(settled["closed_ts"]) def test_settle_execution_from_backtest_marks_stopped_out(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "loss", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "stopped_out") self.assertTrue(settled["meta"]["stop_loss_hit"]) def test_settle_execution_from_backtest_keeps_tp1_progress_open(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "breakeven", "tp1_hit": True, "tp2_hit": False, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "partially_closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp1_reached") self.assertTrue(settled["meta"]["tp1_hit"]) self.assertFalse(settled["meta"]["tp2_hit"]) self.assertFalse(settled["meta"]["stop_loss_hit"]) self.assertEqual(settled["meta"]["position_state"], "partially_closed") def test_settle_execution_from_backtest_closes_when_management_finishes_remaining_position(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={ "outcome": "breakeven", "tp1_hit": True, "tp2_hit": False, "invalidated_before_entry": False, "meta": {"management_applied": {"tp1_partial": True, "remaining_position_closed": True}}, }, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp1_partial_managed_to_close") self.assertEqual(settled["meta"]["position_state"], "closed") def test_settle_execution_from_backtest_closes_invalidated_before_entry(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "missed", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": True}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "invalidated_before_entry") self.assertEqual(settled["meta"]["backtest_lifecycle_state"], "invalidated_before_entry") self.assertTrue(settled["meta"]["invalidated_before_entry"]) def test_settle_execution_from_backtest_closes_missed_without_entry_invalidation(self) -> None: settled = ExecutionService().settle_execution_from_backtest( trade_execution={"status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "missed", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "missed") self.assertEqual(settled["meta"]["backtest_lifecycle_state"], "missed") self.assertFalse(settled["meta"]["invalidated_before_entry"]) def test_build_execution_observation_from_backtest_result_marks_projection_source(self) -> None: observation = build_execution_observation_from_backtest_result( {"outcome": "win", "tp1_hit": True, "tp2_hit": True, "invalidated_before_entry": False} ) self.assertEqual(observation["source"], "backtest_projection") self.assertEqual(observation["close_reason"], "tp2_closed") self.assertTrue(observation["entry_triggered"]) def test_build_execution_observation_closes_partial_trade_when_management_finishes_remaining_position(self) -> None: observation = build_execution_observation_from_backtest_result( { "outcome": "breakeven", "tp1_hit": True, "tp2_hit": False, "invalidated_before_entry": False, "meta": {"management_applied": {"tp1_partial": True, "remaining_position_closed": True}}, } ) self.assertEqual(observation["status"], "closed") self.assertEqual(observation["lifecycle_state"], "tp1_partial_managed_to_close") self.assertEqual(observation["position_state"], "closed") def test_persist_execution_settlement_updates_repository_when_execution_has_id(self) -> None: repository = Mock() service = ExecutionService(repository=repository) settled = service.persist_execution_settlement( trade_execution={"id": 7, "status": "open", "meta": {}, "closed_ts": None}, backtest_result={"outcome": "win", "tp1_hit": True, "tp2_hit": True, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") repository.update_trade_execution_lifecycle.assert_called_once() kwargs = repository.update_trade_execution_lifecycle.call_args.kwargs self.assertEqual(kwargs["execution_id"], 7) self.assertEqual(kwargs["status"], "closed") self.assertEqual(kwargs["meta"]["lifecycle_state"], "tp2_closed") def test_settle_execution_by_id_fetches_and_persists(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = {"id": 8, "status": "open", "meta": {}, "closed_ts": None} service = ExecutionService(repository=repository) settled = service.settle_execution_by_id( execution_id=8, backtest_result={"outcome": "loss", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "stopped_out") repository.fetch_execution_by_id.assert_called_once_with(8) repository.update_trade_execution_lifecycle.assert_called_once() def test_settle_execution_by_id_returns_none_when_missing(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = None service = ExecutionService(repository=repository) settled = service.settle_execution_by_id(execution_id=404, backtest_result={"outcome": "missed", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": True}) self.assertIsNone(settled) repository.update_trade_execution_lifecycle.assert_not_called() def test_settle_execution_by_id_returns_closed_tp2_sample(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = {"id": 9, "status": "open", "meta": {}, "closed_ts": None} service = ExecutionService(repository=repository) settled = service.settle_execution_by_id( execution_id=9, backtest_result={"outcome": "win", "tp1_hit": True, "tp2_hit": True, "invalidated_before_entry": False}, ) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp2_closed") def test_settle_execution_by_id_from_latest_backtest_fetches_result(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = {"id": 10, "signal_id": 3, "status": "open", "meta": {}, "closed_ts": None} repository.fetch_latest_backtest_result_for_signal.return_value = {"outcome": "loss", "tp1_hit": False, "tp2_hit": False, "invalidated_before_entry": False} service = ExecutionService(repository=repository) settled = service.settle_execution_by_id_from_latest_backtest(execution_id=10) self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "stopped_out") repository.fetch_execution_by_id.assert_called_once_with(10) repository.fetch_latest_backtest_result_for_signal.assert_called_once_with(3) repository.update_trade_execution_lifecycle.assert_called_once() def test_settle_execution_by_id_from_latest_backtest_returns_none_without_result(self) -> None: repository = Mock() repository.fetch_execution_by_id.return_value = {"id": 11, "signal_id": 4, "status": "open", "meta": {}, "closed_ts": None} repository.fetch_latest_backtest_result_for_signal.return_value = None service = ExecutionService(repository=repository) settled = service.settle_execution_by_id_from_latest_backtest(execution_id=11) self.assertIsNone(settled) repository.update_trade_execution_lifecycle.assert_not_called() def test_settle_latest_execution_for_signal_uses_latest_execution(self) -> None: repository = Mock() repository.fetch_latest_execution.return_value = {"id": 12, "signal_id": 5, "status": "open", "meta": {}, "closed_ts": None} repository.fetch_latest_backtest_result_for_signal.return_value = {"outcome": "win", "tp1_hit": True, "tp2_hit": True, "invalidated_before_entry": False} service = ExecutionService(repository=repository) settled = service.settle_latest_execution_for_signal(signal_id=5, execution_mode="paper") self.assertEqual(settled["status"], "closed") self.assertEqual(settled["meta"]["lifecycle_state"], "tp2_closed") repository.fetch_latest_execution.assert_called_once_with(signal_id=5, execution_mode="paper") repository.fetch_latest_backtest_result_for_signal.assert_called_once_with(5) repository.update_trade_execution_lifecycle.assert_called_once() def test_settle_latest_execution_for_signal_can_use_manual_observation(self) -> None: repository = Mock() repository.fetch_latest_execution.return_value = {"id": 12, "signal_id": 5, "status": "pending", "meta": {}, "closed_ts": None} service = ExecutionService(repository=repository) settled = service.settle_latest_execution_for_signal( signal_id=5, execution_mode="semi_auto", observation={"source": "manual", "status": "cancelled", "close_reason": "cancelled"}, ) self.assertEqual(settled["status"], "cancelled") self.assertEqual(settled["meta"]["settlement_source"], "manual") repository.fetch_latest_backtest_result_for_signal.assert_not_called() repository.update_trade_execution_lifecycle.assert_called_once() def test_settle_latest_execution_for_signal_returns_none_without_execution(self) -> None: repository = Mock() repository.fetch_latest_execution.return_value = None service = ExecutionService(repository=repository) settled = service.settle_latest_execution_for_signal(signal_id=6) self.assertIsNone(settled) repository.fetch_latest_backtest_result_for_signal.assert_not_called() repository.update_trade_execution_lifecycle.assert_not_called() class ExecutionPricingTests(unittest.TestCase): def test_entry_price_uses_midpoint_when_range_exists(self) -> None: self.assertEqual( compute_entry_price({"entry_low": 100.0, "entry_high": 102.0}), 101.0, ) def test_entry_price_falls_back_to_single_bound(self) -> None: self.assertEqual(compute_entry_price({"entry_low": 99.5, "entry_high": None}), 99.5) class ExecutionIdempotencyTests(unittest.TestCase): def test_detects_equivalent_open_execution(self) -> None: decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "medium", "daily_bias": "bullish", "draw_side": "buy_side", "meta": {"execution_permission": True}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, ) existing = { "execution_mode": "paper", "status": "open", "side": "buy", "entry_price": decision.entry_price, "stop_loss": decision.meta["stop_loss"], "take_profit_1": decision.meta["tp1"], "take_profit_2": decision.meta["tp2"], "size": decision.size, "risk_pct": decision.risk_pct, "meta": decision.meta, } self.assertTrue(trade_execution_matches_decision(existing, decision)) def test_detects_equivalent_open_execution_with_rounding_differences(self) -> None: decision = evaluate_execution_decision( signal={ "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 77200.0, "entry_high": 77223.3, "stop_loss": 77122.8, "tp1": 77300.5233, "tp2": 77454.9699, "session_code": "LONDON", "execution_intensity": "medium", "daily_bias": "bullish", "draw_side": "buy_side", "meta": {"execution_permission": True}, }, execution_mode="paper", account_equity=10000.0, risk_pct=0.01, ) existing = { "execution_mode": "paper", "status": "open", "side": "buy", "entry_price": "77211.6500000000", "stop_loss": "77122.8000000000", "take_profit_1": "77300.5233000000", "take_profit_2": "77454.9699000000", "size": "1.1254924029", "risk_pct": "0.0100", "meta": decision.meta, } self.assertTrue(trade_execution_matches_decision(existing, decision)) def _valid_signal() -> dict: target_plan = { "status": "ready", "tp1": {"price": 102.0, "objective_layer": "IRL", "source": {"type": "session_pivot", "table": "market_sessions", "field": "high"}}, "tp2": {"price": 104.0, "objective_layer": "ERL", "source": {"type": "dealing_range_boundary", "table": "dealing_ranges", "field": "high"}}, "management_rules": [{"code": "tp1_partial", "fraction": 0.5}], } return { "id": 1, "instrument_id": 1, "model_code": "SWEEP_MSS_FVG", "side": "buy", "status": "awaiting_confirmation", "entry_type": "limit", "entry_low": 100.0, "entry_high": 101.0, "stop_loss": 99.0, "tp1": 102.0, "tp2": 104.0, "session_code": "LONDON", "execution_intensity": "high", "daily_bias": "bullish", "draw_side": "buy_side", "created_ts": "2026-05-06T00:00:00+00:00", "expires_ts": "2026-05-06T04:00:00+00:00", "invalidations": [], "evidence": {"mss_id": 1, "fvg_id": 2}, "target_plan": target_plan, "meta": { "execution_permission": True, "management_rules": target_plan["management_rules"], }, } if __name__ == "__main__": unittest.main()