Add integration tests for execution persistence, metrics, and opportunity writing
CI / lint-test-build (push) Failing after 1m23s
CI / lint-test-build (push) Failing after 1m23s
- Implemented integration tests for the execution writer to ensure trade orders and PnL are persisted correctly. - Created integration tests for the metrics calculator to summarize execution data accurately. - Added integration tests for the opportunity writer to verify event persistence. - Established PostgreSQL schema validation tests to ensure all expected tables, columns, and constraints exist. - Removed outdated unit tests that relied on DuckDB and replaced them with tests using PgStore.
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from arbitrade.config.settings import Settings
|
||||
from arbitrade.storage.db import DuckDBStore
|
||||
from arbitrade.storage.repositories import AuditRecord, AuditRepository
|
||||
|
||||
|
||||
def test_audit_repository_inserts_and_lists_recent(tmp_path) -> None:
|
||||
settings = Settings(_env_file=None, DUCKDB_PATH=tmp_path / "audit.duckdb")
|
||||
store = DuckDBStore(settings)
|
||||
store.migrate()
|
||||
repository = AuditRepository(store)
|
||||
|
||||
repository.insert(
|
||||
AuditRecord(
|
||||
occurred_at=datetime.now(UTC),
|
||||
actor="dashboard_user",
|
||||
event_type="dashboard.control.start",
|
||||
decision="approved",
|
||||
payload={"execution_status": "running"},
|
||||
correlation_id="req-1",
|
||||
)
|
||||
)
|
||||
|
||||
recent = repository.list_recent(limit=5)
|
||||
|
||||
assert len(recent) == 1
|
||||
assert recent[0].actor == "dashboard_user"
|
||||
assert recent[0].event_type == "dashboard.control.start"
|
||||
assert recent[0].decision == "approved"
|
||||
assert recent[0].payload == {"execution_status": "running"}
|
||||
assert recent[0].correlation_id == "req-1"
|
||||
@@ -8,7 +8,7 @@ from arbitrade.config.service import (
|
||||
ConfigPairing,
|
||||
ConfigSetting,
|
||||
)
|
||||
from arbitrade.storage.db import DuckDBStore
|
||||
from arbitrade.storage.pg_store import PgStore
|
||||
from arbitrade.storage.repositories import (
|
||||
ConfigBacktestingDefaultsRepository,
|
||||
ConfigPairingRepository,
|
||||
@@ -19,7 +19,7 @@ from arbitrade.storage.repositories import (
|
||||
@pytest.fixture
|
||||
def mock_store():
|
||||
"""Create a mock database store."""
|
||||
store = Mock(spec=DuckDBStore)
|
||||
store = Mock(spec=PgStore)
|
||||
return store
|
||||
|
||||
|
||||
@@ -244,7 +244,8 @@ def test_config_pairing_repository_create_pairing(mock_store):
|
||||
]
|
||||
|
||||
# Create pairing
|
||||
pairing = ConfigPairing(base_asset="BTC", quote_asset="USD", enabled=True, source="Kraken")
|
||||
pairing = ConfigPairing(
|
||||
base_asset="BTC", quote_asset="USD", enabled=True, source="Kraken")
|
||||
|
||||
result = repo.create_pairing(pairing)
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from arbitrade.config.settings import Settings
|
||||
from arbitrade.detection.engine import OpportunityEvent
|
||||
from arbitrade.execution.sequencer import TriangularExecutionSequencer
|
||||
from arbitrade.storage.db import DuckDBStore
|
||||
from arbitrade.storage.executions import AsyncExecutionWriter
|
||||
from arbitrade.storage.repositories import OrderRepository, PnLRepository, TradeRepository
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _FakeRestClient:
|
||||
calls: int = 0
|
||||
|
||||
async def place_market_order(self, *, pair: str, side: str, volume: float) -> dict[str, object]:
|
||||
self.calls += 1
|
||||
return {"txid": [f"tx-{self.calls}"], "status": "submitted"}
|
||||
|
||||
|
||||
def _sample_event() -> OpportunityEvent:
|
||||
return OpportunityEvent(
|
||||
detected_at=datetime.now(UTC),
|
||||
cycle="USD->BTC->ETH->USD",
|
||||
updated_pair="BTC/USD",
|
||||
gross_rate=1.04,
|
||||
net_rate=1.03,
|
||||
gross_pct=4.0,
|
||||
net_pct=3.0,
|
||||
est_profit=0.03,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execution_writer_persists_trade_order_and_pnl(tmp_path) -> None:
|
||||
settings = Settings(_env_file=None, DUCKDB_PATH=tmp_path / "exec.duckdb")
|
||||
store = DuckDBStore(settings)
|
||||
store.migrate()
|
||||
writer = AsyncExecutionWriter(
|
||||
TradeRepository(store),
|
||||
OrderRepository(store),
|
||||
PnLRepository(store),
|
||||
max_queue_size=10,
|
||||
)
|
||||
await writer.start()
|
||||
|
||||
client = _FakeRestClient()
|
||||
sequencer = TriangularExecutionSequencer(
|
||||
client,
|
||||
available_pairs=["BTC/USD", "ETH/BTC", "ETH/USD"],
|
||||
execution_writer=writer,
|
||||
)
|
||||
|
||||
result = await sequencer.execute(_sample_event())
|
||||
await writer.stop()
|
||||
|
||||
assert result.success
|
||||
assert client.calls == 3
|
||||
|
||||
with store.connect() as conn:
|
||||
trades = conn.execute(
|
||||
"SELECT trade_ref, status, estimated_pnl, capital_used, cycle, leg_count FROM trades"
|
||||
).fetchall()
|
||||
orders = conn.execute(
|
||||
"SELECT trade_ref, order_ref, leg_index, pair, side, volume, status "
|
||||
"FROM orders ORDER BY leg_index"
|
||||
).fetchall()
|
||||
pnls = conn.execute("SELECT trade_ref, kind, pnl_usd, source FROM pnl_events").fetchall()
|
||||
|
||||
assert len(trades) == 1
|
||||
assert trades[0][1] == "filled"
|
||||
assert trades[0][2] == 0.03
|
||||
assert trades[0][3] == 1.0
|
||||
assert trades[0][4] == "USD->BTC->ETH->USD"
|
||||
assert trades[0][5] == 3
|
||||
|
||||
assert len(orders) == 3
|
||||
assert orders[0][2] == 0
|
||||
assert orders[1][2] == 1
|
||||
assert orders[2][2] == 2
|
||||
assert orders[0][6] == "submitted"
|
||||
|
||||
assert len(pnls) == 1
|
||||
assert pnls[0][1] == "estimated"
|
||||
assert pnls[0][2] == 0.03
|
||||
@@ -1,144 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from arbitrade.config.settings import Settings
|
||||
from arbitrade.metrics import MetricsCalculator
|
||||
from arbitrade.storage.db import DuckDBStore
|
||||
|
||||
|
||||
def test_metrics_calculator_summarizes_execution_data(tmp_path) -> None:
|
||||
settings = Settings(_env_file=None, DUCKDB_PATH=tmp_path / "metrics.duckdb")
|
||||
store = DuckDBStore(settings)
|
||||
store.migrate()
|
||||
|
||||
started = datetime.now(UTC)
|
||||
finished = started + timedelta(seconds=30)
|
||||
started_two = started + timedelta(minutes=1)
|
||||
finished_two = started_two + timedelta(seconds=90)
|
||||
|
||||
with store.connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO trades (
|
||||
trade_ref,
|
||||
started_at,
|
||||
finished_at,
|
||||
status,
|
||||
realized_pnl,
|
||||
estimated_pnl,
|
||||
capital_used,
|
||||
cycle,
|
||||
leg_count
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
"trade-1",
|
||||
started,
|
||||
finished,
|
||||
"filled",
|
||||
12.5,
|
||||
10.0,
|
||||
100.0,
|
||||
"USD->BTC->ETH->USD",
|
||||
3,
|
||||
"trade-2",
|
||||
started_two,
|
||||
finished_two,
|
||||
"filled",
|
||||
-4.5,
|
||||
-2.0,
|
||||
200.0,
|
||||
"USD->ETH->BTC->USD",
|
||||
3,
|
||||
],
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO opportunities (
|
||||
detected_at,
|
||||
cycle,
|
||||
gross_pct,
|
||||
net_pct,
|
||||
est_profit,
|
||||
executed
|
||||
) VALUES (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
started,
|
||||
"USD->BTC->ETH->USD",
|
||||
4.0,
|
||||
3.0,
|
||||
0.03,
|
||||
True,
|
||||
started_two,
|
||||
"USD->ETH->BTC->USD",
|
||||
2.0,
|
||||
1.0,
|
||||
0.01,
|
||||
False,
|
||||
started_two + timedelta(seconds=30),
|
||||
"USD->BTC->ETH->USD",
|
||||
5.0,
|
||||
4.0,
|
||||
0.04,
|
||||
True,
|
||||
],
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO orders (
|
||||
trade_ref,
|
||||
order_ref,
|
||||
leg_index,
|
||||
pair,
|
||||
side,
|
||||
volume,
|
||||
user_ref,
|
||||
status,
|
||||
filled_volume,
|
||||
avg_price,
|
||||
raw_response,
|
||||
recorded_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
"trade-1",
|
||||
"order-1",
|
||||
0,
|
||||
"BTC/USD",
|
||||
"buy",
|
||||
2.0,
|
||||
101,
|
||||
"closed",
|
||||
2.0,
|
||||
100.0,
|
||||
"{}",
|
||||
started,
|
||||
"trade-2",
|
||||
"order-2",
|
||||
0,
|
||||
"ETH/USD",
|
||||
"sell",
|
||||
4.0,
|
||||
202,
|
||||
"closed",
|
||||
3.0,
|
||||
200.0,
|
||||
"{}",
|
||||
started_two,
|
||||
],
|
||||
)
|
||||
|
||||
metrics = MetricsCalculator(store).compute()
|
||||
|
||||
assert metrics.realized_pnl_usd == 8.0
|
||||
assert metrics.win_rate == 0.5
|
||||
assert metrics.avg_trade_duration_seconds == 60.0
|
||||
assert metrics.opportunities_per_minute == 2.0
|
||||
assert metrics.fill_rate == 0.875
|
||||
assert metrics.latency_p50_seconds == 60.0
|
||||
assert metrics.latency_p95_seconds == 87.0
|
||||
assert metrics.latency_p99_seconds == pytest.approx(89.4)
|
||||
@@ -1,48 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from arbitrade.config.settings import Settings
|
||||
from arbitrade.detection.engine import OpportunityEvent
|
||||
from arbitrade.storage.db import DuckDBStore
|
||||
from arbitrade.storage.opportunities import AsyncOpportunityWriter
|
||||
from arbitrade.storage.repositories import OpportunityRepository
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_opportunity_writer_persists_events(tmp_path) -> None:
|
||||
settings = Settings(_env_file=None, DUCKDB_PATH=tmp_path / "test.duckdb")
|
||||
store = DuckDBStore(settings)
|
||||
store.migrate()
|
||||
|
||||
repository = OpportunityRepository(store)
|
||||
writer = AsyncOpportunityWriter(repository, max_queue_size=10)
|
||||
await writer.start()
|
||||
|
||||
event = OpportunityEvent(
|
||||
detected_at=datetime.now(UTC),
|
||||
cycle="USD->BTC->ETH->USD",
|
||||
updated_pair="BTC/USD",
|
||||
gross_rate=1.04,
|
||||
net_rate=1.03,
|
||||
gross_pct=4.0,
|
||||
net_pct=3.0,
|
||||
est_profit=0.03,
|
||||
)
|
||||
|
||||
await writer.enqueue(event)
|
||||
await writer.stop()
|
||||
|
||||
with store.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT cycle, gross_pct, net_pct, est_profit, executed FROM opportunities"
|
||||
).fetchall()
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0][0] == "USD->BTC->ETH->USD"
|
||||
assert rows[0][1] == 4.0
|
||||
assert rows[0][2] == 3.0
|
||||
assert rows[0][3] == 0.03
|
||||
assert rows[0][4] is False
|
||||
Reference in New Issue
Block a user