Spaces:
Sleeping
Sleeping
| """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 | |
| 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 | |