"""Domain layer tests: schema, idempotency, order state, execution service, risk, sizing.""" from __future__ import annotations import asyncio import os from pathlib import Path import pytest from trading.domain.schema import apply_migrations, validate_schema from trading.domain.idempotency import claim_idempotency, acquire_lock, release_lock, LockNotAcquired from trading.domain.order_state import transition, can_transition, InvalidTransition, is_terminal from trading.domain.orders_repo import create_order, advance_order, get_order from trading.domain.risk_ledger import record_realized_pnl, get_daily_pnl, check_daily_loss_limit from trading.domain.risk_limits import assert_entry_allowed, RiskRejected from trading.domain.sizing import validate_geometry, base_to_contracts, SizingError from trading.domain.approvals import create_approval, consume_approval, ApprovalError from trading.domain.plans_repo import create_plan from trading.domain.security_utils import public_error, safe_extract_member_path from trading.adapters.exchange_port import MarketMeta from trading.adapters.paper_adapter import PaperExchangeAdapter from trading.adapters.exchange_port import OrderRequest from trading.domain.execution_service import ExecutionService from trading.plan_integrity import bind_plan_hash from trading.futures_execution import TradingError @pytest.fixture(autouse=True) def _hermes_home(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setenv("TRADING_MODE", "paper") monkeypatch.setenv("HERMES_EXECUTION_ENABLED", "true") ok, msgs = apply_migrations() assert ok, msgs ok, msgs = validate_schema() assert ok, msgs yield def test_migrations_idempotent(): ok, _ = apply_migrations() assert ok def test_idempotency_claim(): is_new, _ = claim_idempotency("k1", "entry") assert is_new is True is_new2, _ = claim_idempotency("k1", "entry") assert is_new2 is False def test_execution_lock(): h = acquire_lock("L1", holder="a") assert h == "a" with pytest.raises(LockNotAcquired): acquire_lock("L1", holder="b", ttl_s=30) release_lock("L1", "a") acquire_lock("L1", holder="b") def test_order_state_machine(): assert can_transition("created", "submitted") assert transition("submitted", "filled") == "filled" with pytest.raises(InvalidTransition): transition("closed", "filled") assert is_terminal("closed") def test_orders_repo_lifecycle(): oid = create_order(client_order_id="c1", symbol="BTC/USDT:USDT", side="long", size=1) advance_order(oid, "submitted") advance_order(oid, "filled") o = get_order(oid) assert o["state"] == "filled" def test_risk_ledger(): record_realized_pnl(-50) pnl, count = get_daily_pnl() assert pnl == -50 assert count >= 1 assert check_daily_loss_limit(1000) is True assert check_daily_loss_limit(10) is False def test_risk_limits(monkeypatch): monkeypatch.setenv("HERMES_EXECUTION_ENABLED", "true") assert_entry_allowed(leverage=5, notional=100, open_positions=0) with pytest.raises(RiskRejected): assert_entry_allowed(leverage=100, notional=100, open_positions=0) def test_sizing(): meta = MarketMeta("BTC/USDT:USDT", 3, 1, 0.001, 1.0, 50) s = validate_geometry(amount=0.01, price=100.0, meta=meta, stop_loss=95, take_profit=110, side="long") assert s.amount > 0 with pytest.raises(SizingError): validate_geometry(amount=0.01, price=100.0, meta=meta, stop_loss=105, take_profit=110, side="long") assert base_to_contracts(2.0, meta) == 2.0 def test_approval_single_use(): plan = create_plan({ "symbol": "BTCUSDT", "decision": "LONG", "entry": 100.0, "stop_loss": 95.0, "take_profit": 110.0, "leverage": 2, "quantity": 1.0, "risk_profile": "moderate", "futuresVerified": True, "noTradeGuard": False, "trading_readiness": "ready", "executable": True, "expires_at": "2099-01-01T00:00:00Z", }, owner_id="owner1") aid = create_approval(plan["plan_id"], plan["plan_hash"], "owner1") consume_approval(aid, plan["plan_id"], plan["plan_hash"], "owner1") with pytest.raises(ApprovalError): consume_approval(aid, plan["plan_id"], plan["plan_hash"], "owner1") def test_paper_adapter(): async def _run(): ad = PaperExchangeAdapter() markets = await ad.load_markets() assert "BTC/USDT:USDT" in markets r = await ad.create_order(OrderRequest("BTC/USDT:USDT", "buy", 0.01, 100.0, "cid1")) assert r.filled == 0.01 await ad.close() asyncio.run(_run()) def test_execution_service_paper_entry(monkeypatch): monkeypatch.setenv("HERMES_EXECUTION_ENABLED", "true") async def _run(): svc = ExecutionService() plan = bind_plan_hash({ "symbol": "BTCUSDT", "decision": "LONG", "entry": 100, "stop_loss": 95, "take_profit": 110, "leverage": 5, "quantity": 1, "risk_profile": "moderate", "futuresVerified": True, "noTradeGuard": False, "trading_readiness": "ready", "executable": True, "expires_at": "2099-01-01T00:00:00Z", }) result = await svc._submit_entry( symbol="BTC/USDT:USDT", side="long", size=0.1, price=100.0, plan_id="plan1", plan=plan, idempotency_key="unique-entry-1", ) assert result["status"] == "protected" assert len(result["protective_order_ids"]) == 2 # exact replay is a duplicate; changed request content must not be accepted. dup = await svc._submit_entry( symbol="BTC/USDT:USDT", side="long", size=0.1, price=100.0, plan_id="plan1", plan=plan, idempotency_key="unique-entry-1", ) assert dup["status"] == "duplicate" asyncio.run(_run()) def test_execution_service_kill_switch(monkeypatch): monkeypatch.delenv("HERMES_EXECUTION_ENABLED", raising=False) async def _run(): svc = ExecutionService() with pytest.raises(TradingError, match="kill switch"): await svc._submit_entry(symbol="BTC/USDT:USDT", side="long", size=0.01) asyncio.run(_run()) def test_security_utils(tmp_path): assert "REDACTED" in public_error("api_key=supersecret123456") with pytest.raises(ValueError): safe_extract_member_path(tmp_path, "../etc/passwd") p = safe_extract_member_path(tmp_path, "ok/file.txt") assert str(p).startswith(str(tmp_path.resolve())) def test_recovery_gate_clear(): from trading.domain.recovery import startup_recovery_check ok, msgs = startup_recovery_check() assert ok is True def test_smoke_backtest(): from trading.strategy_backtest import run_smoke_backtest r = run_smoke_backtest() assert r.trades >= 0 assert "strategyName" in r.strategy