Files
ai-exchange/tests/unit/test_execution_service.py
T
2026-05-08 08:09:00 +08:00

916 lines
40 KiB
Python

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