feat: update environment configuration and improve repository handling
CI / lint-test-build (push) Failing after 11s

- Added PG_PASSWORD to .env.example for database connection.
- Removed unnecessary imports and streamlined code in various modules.
- Enhanced error handling in ConfigSettingRepository and ConfigPairingRepository.
- Updated test files to remove unused imports and improve clarity.
This commit is contained in:
2026-06-07 14:50:55 +02:00
parent 529ff967cc
commit ef22e217c7
11 changed files with 69 additions and 37 deletions
+5
View File
@@ -1,6 +1,11 @@
APP_ENV=dev APP_ENV=dev
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
APP_PORT=8000 APP_PORT=8000
PG_HOST=192.168.88.35
PG_PORT=5432
PG_DATABASE=arbitrade
PG_USER=arbitrade
PG_PASSWORD=arbitrade
LOG_LEVEL=INFO LOG_LEVEL=INFO
LOG_JSON=true LOG_JSON=true
ALERTS_ENABLED=true ALERTS_ENABLED=true
-1
View File
@@ -10,7 +10,6 @@ Current stack:
- Native Kraken WebSocket planned for market-data hot path - Native Kraken WebSocket planned for market-data hot path
- Gitea Actions + Gitea container registry - Gitea Actions + Gitea container registry
Project plan lives in [PLAN.md](PLAN.md).
Task checklist lives in [.github/instructions/TODO.md](.github/instructions/TODO.md). Task checklist lives in [.github/instructions/TODO.md](.github/instructions/TODO.md).
Coolify deployment runbooks live in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md). Coolify deployment runbooks live in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
+7 -2
View File
@@ -25,10 +25,15 @@ from arbitrade.market_data.feed_builder import (
) )
from arbitrade.metrics import MetricsCalculator from arbitrade.metrics import MetricsCalculator
from arbitrade.runtime.lifecycle import graceful_shutdown, restore_runtime_state from arbitrade.runtime.lifecycle import graceful_shutdown, restore_runtime_state
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.market_snapshots import AsyncMarketSnapshotWriter from arbitrade.storage.market_snapshots import AsyncMarketSnapshotWriter
from arbitrade.storage.opportunities import AsyncOpportunityWriter from arbitrade.storage.opportunities import AsyncOpportunityWriter
from arbitrade.storage.repositories import AuditRepository, RuntimeStateRepository, MarketSnapshotRepository, OpportunityRepository from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import (
AuditRepository,
MarketSnapshotRepository,
OpportunityRepository,
RuntimeStateRepository,
)
_LOG = structlog.get_logger(__name__) _LOG = structlog.get_logger(__name__)
+16 -9
View File
@@ -47,13 +47,15 @@ class TriangularExecutionSequencer:
rest_client: SupportsOrderPlacement, rest_client: SupportsOrderPlacement,
*, *,
available_pairs: Sequence[str], available_pairs: Sequence[str],
volume_for_leg: Callable[[OpportunityEvent, ExecutionLeg, int], float] | None = None, volume_for_leg: Callable[[OpportunityEvent,
ExecutionLeg, int], float] | None = None,
execution_writer: AsyncExecutionWriter | None = None, execution_writer: AsyncExecutionWriter | None = None,
alert_notifier: SupportsAlerts | None = None, alert_notifier: SupportsAlerts | None = None,
audit_repository: AuditRepository | None = None, audit_repository: AuditRepository | None = None,
) -> None: ) -> None:
self._rest_client = rest_client self._rest_client = rest_client
self._available_pairs = {self._normalize_pair(pair) for pair in available_pairs} self._available_pairs = {self._normalize_pair(
pair) for pair in available_pairs}
self._volume_for_leg = volume_for_leg or self._default_volume_for_leg self._volume_for_leg = volume_for_leg or self._default_volume_for_leg
self._execution_writer = execution_writer self._execution_writer = execution_writer
self._alert_notifier = alert_notifier self._alert_notifier = alert_notifier
@@ -100,12 +102,15 @@ class TriangularExecutionSequencer:
raise ValueError(f"No tradable pair for leg {from_cur}->{to_cur}") raise ValueError(f"No tradable pair for leg {from_cur}->{to_cur}")
def _build_legs(self, event: OpportunityEvent) -> tuple[ExecutionLeg, ...]: def _build_legs(self, event: OpportunityEvent) -> tuple[ExecutionLeg, ...]:
currencies = [part.strip().upper() for part in event.cycle.split("->") if part.strip()] currencies = [part.strip().upper()
for part in event.cycle.split("->") if part.strip()]
if len(currencies) < 4 or currencies[0] != currencies[-1]: if len(currencies) < 4 or currencies[0] != currencies[-1]:
raise ValueError("cycle must be a closed triangular path like A->B->C->A") raise ValueError(
"cycle must be a closed triangular path like A->B->C->A")
if len(currencies) != 4: if len(currencies) != 4:
raise ValueError("cycle must contain exactly three unique currencies") raise ValueError(
"cycle must contain exactly three unique currencies")
legs: list[ExecutionLeg] = [] legs: list[ExecutionLeg] = []
for idx in range(3): for idx in range(3):
@@ -120,7 +125,8 @@ class TriangularExecutionSequencer:
) )
volume = self._volume_for_leg(event, placeholder_leg, idx) volume = self._volume_for_leg(event, placeholder_leg, idx)
if volume <= 0.0: if volume <= 0.0:
raise ValueError("volume_for_leg must return a positive volume") raise ValueError(
"volume_for_leg must return a positive volume")
legs.append(self._resolve_leg(from_currency, to_currency, volume)) legs.append(self._resolve_leg(from_currency, to_currency, volume))
return tuple(legs) return tuple(legs)
@@ -158,7 +164,7 @@ class TriangularExecutionSequencer:
) )
except Exception as exc: except Exception as exc:
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
@@ -209,7 +215,8 @@ class TriangularExecutionSequencer:
responses.append(response) responses.append(response)
if self._execution_writer is not None: if self._execution_writer is not None:
order_ref = self._order_ref_from_response(response, f"leg-{idx}") order_ref = self._order_ref_from_response(
response, f"leg-{idx}")
await self._execution_writer.enqueue( await self._execution_writer.enqueue(
OrderRecord( OrderRecord(
trade_ref=trade_ref, trade_ref=trade_ref,
@@ -265,7 +272,7 @@ class TriangularExecutionSequencer:
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
+21 -16
View File
@@ -38,7 +38,8 @@ class MarketDataFeed:
opportunity_writer: AsyncOpportunityWriter | None = None, opportunity_writer: AsyncOpportunityWriter | None = None,
paper_trading_mode: bool = True, paper_trading_mode: bool = True,
opportunity_executor: ( opportunity_executor: (
Callable[[OpportunityEvent], Awaitable[ExecutionOutcome | float | None]] | None Callable[[OpportunityEvent],
Awaitable[ExecutionOutcome | float | None]] | None
) = None, ) = None,
trade_capital: float = 1.0, trade_capital: float = 1.0,
max_trade_capital: float | None = None, max_trade_capital: float | None = None,
@@ -92,7 +93,8 @@ class MarketDataFeed:
return {} return {}
start = currencies[0] start = currencies[0]
exposure_assets = {currency for currency in currencies[1:] if currency != start} exposure_assets = {
currency for currency in currencies[1:] if currency != start}
return {asset: event.allocated_capital for asset in exposure_assets} return {asset: event.allocated_capital for asset in exposure_assets}
async def run(self) -> None: async def run(self) -> None:
@@ -144,7 +146,7 @@ class MarketDataFeed:
symbol=delta.symbol, symbol=delta.symbol,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -172,7 +174,7 @@ class MarketDataFeed:
for event in opportunities: for event in opportunities:
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="detector", actor="detector",
@@ -207,7 +209,7 @@ class MarketDataFeed:
net_pct=event.net_pct, net_pct=event.net_pct,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
@@ -228,7 +230,7 @@ class MarketDataFeed:
updated_pair=event.updated_pair, updated_pair=event.updated_pair,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
@@ -250,7 +252,7 @@ class MarketDataFeed:
reason=self._kill_switch.reason, reason=self._kill_switch.reason,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -275,7 +277,7 @@ class MarketDataFeed:
reason=self._stop_conditions_guard.halted_reason, reason=self._stop_conditions_guard.halted_reason,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -298,7 +300,7 @@ class MarketDataFeed:
reason=self._loss_limit_guard.halted_reason, reason=self._loss_limit_guard.halted_reason,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -313,7 +315,8 @@ class MarketDataFeed:
continue continue
if self._pre_trade_validator is not None and self._balance_provider is not None: if self._pre_trade_validator is not None and self._balance_provider is not None:
required_balances = {self._quote_balance_asset: event.allocated_capital} required_balances = {
self._quote_balance_asset: event.allocated_capital}
balances = { balances = {
asset.upper(): amount asset.upper(): amount
for asset, amount in self._balance_provider().items() for asset, amount in self._balance_provider().items()
@@ -329,7 +332,7 @@ class MarketDataFeed:
required_by_asset=required_balances, required_by_asset=required_balances,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -358,7 +361,7 @@ class MarketDataFeed:
exposure_by_asset=exposure_by_asset, exposure_by_asset=exposure_by_asset,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="risk_manager", actor="risk_manager",
@@ -381,7 +384,8 @@ class MarketDataFeed:
outcome = await self._opportunity_executor(event) outcome = await self._opportunity_executor(event)
except Exception as exc: except Exception as exc:
if self._trade_limits_guard is not None: if self._trade_limits_guard is not None:
self._trade_limits_guard.close_trade(exposure_by_asset) self._trade_limits_guard.close_trade(
exposure_by_asset)
dispatch_alert_nowait( dispatch_alert_nowait(
self._alert_notifier, self._alert_notifier,
@@ -420,7 +424,7 @@ class MarketDataFeed:
updated_pair=event.updated_pair, updated_pair=event.updated_pair,
) )
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
@@ -447,7 +451,8 @@ class MarketDataFeed:
realized_pnl = outcome realized_pnl = outcome
if realized_pnl is not None and self._loss_limit_guard is not None: if realized_pnl is not None and self._loss_limit_guard is not None:
self._loss_limit_guard.register_realized_pnl(realized_pnl) self._loss_limit_guard.register_realized_pnl(
realized_pnl)
if self._loss_limit_guard.is_halted: if self._loss_limit_guard.is_halted:
_LOG.warning( _LOG.warning(
"loss_limit_halt_triggered", "loss_limit_halt_triggered",
@@ -459,7 +464,7 @@ class MarketDataFeed:
self._trade_limits_guard.close_trade(exposure_by_asset) self._trade_limits_guard.close_trade(exposure_by_asset)
if self._audit_repository is not None: if self._audit_repository is not None:
self._audit_repository.insert( await self._audit_repository.insert(
AuditRecord( AuditRecord(
occurred_at=datetime.now(UTC), occurred_at=datetime.now(UTC),
actor="execution_engine", actor="execution_engine",
+16 -4
View File
@@ -521,7 +521,11 @@ class ConfigSettingRepository:
""", """,
key, key,
) )
return result != "DELETE 0" if result is None:
return False
elif isinstance(result, str):
return result != "DELETE 0"
return False
async def list_settings(self, section: str | None = None) -> list[ConfigSetting]: async def list_settings(self, section: str | None = None) -> list[ConfigSetting]:
"""List all configuration settings, optionally filtered by section.""" """List all configuration settings, optionally filtered by section."""
@@ -567,7 +571,7 @@ class ConfigSettingRepository:
ts = row["latest_updated_at"] ts = row["latest_updated_at"]
if isinstance(ts, str): if isinstance(ts, str):
return datetime.fromisoformat(ts.replace("Z", "+00:00")) return datetime.fromisoformat(ts.replace("Z", "+00:00"))
return ts return ts # type: ignore[no-any-return]
return None return None
@@ -663,7 +667,11 @@ class ConfigPairingRepository:
""", """,
base_asset, quote_asset, base_asset, quote_asset,
) )
return result != "DELETE 0" if result is None:
return False
elif isinstance(result, str):
return result != "DELETE 0"
return False
async def upsert_pairing(self, pairing: ConfigPairing) -> ConfigPairing: async def upsert_pairing(self, pairing: ConfigPairing) -> ConfigPairing:
"""Insert or update a currency pairing (upsert on base_asset, quote_asset).""" """Insert or update a currency pairing (upsert on base_asset, quote_asset)."""
@@ -996,4 +1004,8 @@ class BacktestJobRepository:
"DELETE FROM backtest_jobs WHERE id = $1", "DELETE FROM backtest_jobs WHERE id = $1",
job_id, job_id,
) )
return result != "DELETE 0" if result is None:
return False
elif isinstance(result, str):
return result != "DELETE 0"
return False
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
from datetime import UTC, datetime from datetime import UTC, datetime
import pytest import pytest
import pytest_asyncio
from arbitrade.config.settings import get_settings from arbitrade.config.settings import get_settings
from arbitrade.storage.pg_store import PgStore from arbitrade.storage.pg_store import PgStore
@@ -10,8 +10,8 @@ import pytest
from arbitrade.config.settings import get_settings from arbitrade.config.settings import get_settings
from arbitrade.detection.engine import OpportunityEvent from arbitrade.detection.engine import OpportunityEvent
from arbitrade.execution.sequencer import TriangularExecutionSequencer from arbitrade.execution.sequencer import TriangularExecutionSequencer
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.executions import AsyncExecutionWriter from arbitrade.storage.executions import AsyncExecutionWriter
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import OrderRepository, PnLRepository, TradeRepository from arbitrade.storage.repositories import OrderRepository, PnLRepository, TradeRepository
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
-1
View File
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import pytest import pytest
import pytest_asyncio
from arbitrade.config.settings import get_settings from arbitrade.config.settings import get_settings
from arbitrade.metrics import MetricsCalculator from arbitrade.metrics import MetricsCalculator
+1 -1
View File
@@ -8,8 +8,8 @@ import pytest
from arbitrade.config.settings import get_settings from arbitrade.config.settings import get_settings
from arbitrade.detection.engine import OpportunityEvent from arbitrade.detection.engine import OpportunityEvent
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.opportunities import AsyncOpportunityWriter from arbitrade.storage.opportunities import AsyncOpportunityWriter
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import OpportunityRepository from arbitrade.storage.repositories import OpportunityRepository
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
+2 -1
View File
@@ -13,7 +13,7 @@ from contextlib import asynccontextmanager
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from arbitrade.config.settings import Settings, get_settings from arbitrade.config.settings import get_settings
from arbitrade.storage.pg_store import PgStore from arbitrade.storage.pg_store import PgStore
pytestmark = pytest.mark.integration pytestmark = pytest.mark.integration
@@ -332,6 +332,7 @@ async def test_audit_list_recent(pg: PgStore) -> None:
"""AuditRepository.list_recent returns records in desc order.""" """AuditRepository.list_recent returns records in desc order."""
await pg.migrate() await pg.migrate()
from datetime import UTC, datetime from datetime import UTC, datetime
from arbitrade.storage.repositories import AuditRecord, AuditRepository from arbitrade.storage.repositories import AuditRecord, AuditRepository
repo = AuditRepository(pg) repo = AuditRepository(pg)