SimpleChatbot / hermes_overlay /tests /test_phase4_validation_order.py
Amin
Deploy: HermesFace finalized project to HF Space
2e658e7
Raw
History Blame Contribute Delete
10 kB
from __future__ import annotations
import asyncio
import json
import pytest
import trading.domain.execution_service as execution_module
from trading.adapters.paper_adapter import PaperExchangeAdapter
from trading.domain.approvals import create_approval
from trading.domain.execution_contracts import ExecuteApprovedPlanCommand, ExecutionErrorCode
from trading.domain.execution_service import ExecutionService, ExecutionServiceError
from trading.domain.identity_repo import ensure_paper_exchange_account
from trading.domain.plan_revalidation import PlanRevalidationResult
from trading.domain.plans_repo import create_plan
from trading.domain.risk_limits import MarketRiskContext, RiskRejected
from trading.domain.schema import apply_migrations, connect
from trading.futures_execution import TradingError
class SpyPaperAdapter(PaperExchangeAdapter):
def __init__(self, *, default_price: float = 100.0) -> None:
super().__init__(default_price=default_price)
self.load_market_calls = 0
self.configure_calls = 0
self.create_order_calls = 0
async def load_markets(self):
self.load_market_calls += 1
return await super().load_markets()
async def configure_account(self, symbol, leverage, margin_mode, position_mode):
self.configure_calls += 1
return await super().configure_account(symbol, leverage, margin_mode, position_mode)
async def create_order(self, req):
self.create_order_calls += 1
return await super().create_order(req)
@pytest.fixture(autouse=True)
def _db(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("TRADING_MODE", "paper")
monkeypatch.setenv("HERMES_EXECUTION_ENABLED", "true")
ok, messages = apply_migrations()
assert ok, messages
def _plan(**overrides):
plan = {
"symbol": "BTCUSDT",
"decision": "LONG",
"entry": 100.0,
"stop_loss": 95.0,
"take_profit": 110.0,
"leverage": 3,
"quantity": 1.0,
"risk_profile": "moderate",
"risk_percent": 0.01,
"futuresVerified": True,
"noTradeGuard": False,
"trading_readiness": "ready",
"executable": True,
"expires_at": "2099-01-01T00:00:00Z",
}
plan.update(overrides)
return plan
def _accepted(plan_id: str) -> PlanRevalidationResult:
return PlanRevalidationResult(
revalidation_id=f"rv-{plan_id}",
accepted=True,
reasons=(),
context_hash="context-hash",
observed_price=100.0,
observed_spread_bps=1.0,
observed_slippage_percent=0.0,
market_risk=MarketRiskContext(
provider_fresh=True,
reconciliation_clear=True,
spread_bps=1.0,
funding_rate=0.0,
liquidity_ok=True,
),
orderbook={"bids": [[99.9, 100]], "asks": [[100.0, 100]]},
)
def _setup(owner_id: str = "owner-validation"):
account = ensure_paper_exchange_account(owner_id)
plan = create_plan(_plan(), owner_id=owner_id, account_id=account.account_id)
approval_id = create_approval(
plan["plan_id"], plan["plan_hash"], owner_id, account_id=account.account_id,
)
return owner_id, account, plan, approval_id
def _command(base_owner_id, base_account, plan, approval_id, **overrides):
values = {
"plan_id": plan["plan_id"],
"approval_id": approval_id,
"owner_id": base_owner_id,
"account_id": base_account.account_id,
"session_id": None,
"idempotency_key": f"validation:{plan['plan_id']}:{approval_id}",
}
values.update(overrides)
return ExecuteApprovedPlanCommand(**values)
def test_binding_mode_integrity_and_revalidation_fail_before_adapter_calls(monkeypatch):
owner_id, account, plan, approval_id = _setup()
adapter = SpyPaperAdapter()
service = ExecutionService(adapter)
async def rejected(plan_id, **_kwargs):
result = _accepted(plan_id)
return PlanRevalidationResult(
**{**result.__dict__, "accepted": False, "reasons": ("plan expired",)}
)
async def run():
with pytest.raises(ExecutionServiceError) as owner_error:
await service.execute_approved_plan(
_command(owner_id, account, plan, approval_id, owner_id="other-owner")
)
assert owner_error.value.code is ExecutionErrorCode.BINDING_MISMATCH
with pytest.raises(ExecutionServiceError) as account_error:
await service.execute_approved_plan(
_command(owner_id, account, plan, approval_id, account_id="other-account")
)
assert account_error.value.code is ExecutionErrorCode.BINDING_MISMATCH
conn = connect()
try:
conn.execute("UPDATE plans SET mode='testnet' WHERE plan_id=?", (plan["plan_id"],))
conn.commit()
finally:
conn.close()
with pytest.raises(ExecutionServiceError) as mode_error:
await service.execute_approved_plan(_command(owner_id, account, plan, approval_id))
assert mode_error.value.code is ExecutionErrorCode.MODE_NOT_ALLOWED
conn = connect()
try:
conn.execute("UPDATE plans SET mode='paper' WHERE plan_id=?", (plan["plan_id"],))
row = conn.execute("SELECT payload FROM plans WHERE plan_id=?", (plan["plan_id"],)).fetchone()
payload = json.loads(row[0])
payload["entry"] = 101.0
conn.execute("UPDATE plans SET payload=? WHERE plan_id=?", (json.dumps(payload), plan["plan_id"]))
conn.commit()
finally:
conn.close()
with pytest.raises(ExecutionServiceError) as hash_error:
await service.execute_approved_plan(_command(owner_id, account, plan, approval_id))
assert hash_error.value.code is ExecutionErrorCode.PLAN_INTEGRITY_FAILED
# Restore immutable payload and then prove an expiry/freshness rejection also
# occurs before any adapter account mutation or submission.
conn = connect()
try:
conn.execute("UPDATE plans SET payload=? WHERE plan_id=?", (json.dumps(plan), plan["plan_id"]))
conn.commit()
finally:
conn.close()
monkeypatch.setattr(execution_module, "revalidate_plan", rejected)
with pytest.raises(ExecutionServiceError) as stale_error:
await service.execute_approved_plan(_command(owner_id, account, plan, approval_id))
assert stale_error.value.code is ExecutionErrorCode.REVALIDATION_FAILED
asyncio.run(run())
assert adapter.load_market_calls == 0
assert adapter.configure_calls == 0
assert adapter.create_order_calls == 0
def test_nonfinite_geometry_and_final_risk_fail_before_configuration_or_submission(monkeypatch):
adapter = SpyPaperAdapter()
service = ExecutionService(adapter)
async def run():
with pytest.raises(TradingError, match="finite and positive"):
await service._submit_entry(
symbol="BTCUSDT", side="long", size=float("nan"), price=100.0,
stop_loss=95.0, take_profit=110.0,
)
assert adapter.load_market_calls == 0
def reject_risk(**_kwargs):
raise RiskRejected("final risk rejected")
monkeypatch.setattr(execution_module, "assert_entry_allowed", reject_risk)
with pytest.raises(RiskRejected, match="final risk rejected"):
await service._submit_entry(
symbol="BTCUSDT", side="long", size=1.0, price=100.0,
stop_loss=95.0, take_profit=110.0,
)
asyncio.run(run())
assert adapter.configure_calls == 0
assert adapter.create_order_calls == 0
def test_unverified_market_fails_before_account_configuration_or_submission():
class MissingMarketAdapter(SpyPaperAdapter):
async def load_markets(self):
self.load_market_calls += 1
return {}
adapter = MissingMarketAdapter()
service = ExecutionService(adapter)
async def run():
with pytest.raises(TradingError, match="market metadata unavailable"):
await service._submit_entry(
symbol="BTCUSDT", side="long", size=1.0, price=100.0,
stop_loss=95.0, take_profit=110.0,
)
asyncio.run(run())
assert adapter.load_market_calls == 1
assert adapter.configure_calls == 0
assert adapter.create_order_calls == 0
def test_existing_exposure_is_reconciled_before_second_configuration_or_submission(monkeypatch):
owner_id, account, first_plan, first_approval = _setup("owner-exposure")
adapter = SpyPaperAdapter()
service = ExecutionService(adapter)
async def accepted(plan_id, **_kwargs):
return _accepted(plan_id)
monkeypatch.setattr(execution_module, "revalidate_plan", accepted)
async def run():
first = await service.execute_approved_plan(
_command(owner_id, account, first_plan, first_approval)
)
assert first["status"] == "protected"
baseline = (adapter.load_market_calls, adapter.configure_calls, adapter.create_order_calls)
second_plan = create_plan(
_plan(entry=101.0, stop_loss=96.0, take_profit=111.0),
owner_id=owner_id,
account_id=account.account_id,
)
second_approval = create_approval(
second_plan["plan_id"], second_plan["plan_hash"], owner_id,
account_id=account.account_id,
)
with pytest.raises(ExecutionServiceError, match="existing exposure") as caught:
await service.execute_approved_plan(
_command(owner_id, account, second_plan, second_approval)
)
assert caught.value.code is ExecutionErrorCode.EXECUTION_FAILED
assert (adapter.load_market_calls, adapter.configure_calls, adapter.create_order_calls) == baseline
asyncio.run(run())