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_HOST=0.0.0.0
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_JSON=true
ALERTS_ENABLED=true
-1
View File
@@ -10,7 +10,6 @@ Current stack:
- Native Kraken WebSocket planned for market-data hot path
- 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).
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.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.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__)
+16 -9
View File
@@ -47,13 +47,15 @@ class TriangularExecutionSequencer:
rest_client: SupportsOrderPlacement,
*,
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,
alert_notifier: SupportsAlerts | None = None,
audit_repository: AuditRepository | None = None,
) -> None:
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._execution_writer = execution_writer
self._alert_notifier = alert_notifier
@@ -100,12 +102,15 @@ class TriangularExecutionSequencer:
raise ValueError(f"No tradable pair for leg {from_cur}->{to_cur}")
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]:
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:
raise ValueError("cycle must contain exactly three unique currencies")
raise ValueError(
"cycle must contain exactly three unique currencies")
legs: list[ExecutionLeg] = []
for idx in range(3):
@@ -120,7 +125,8 @@ class TriangularExecutionSequencer:
)
volume = self._volume_for_leg(event, placeholder_leg, idx)
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))
return tuple(legs)
@@ -158,7 +164,7 @@ class TriangularExecutionSequencer:
)
except Exception as exc:
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
@@ -209,7 +215,8 @@ class TriangularExecutionSequencer:
responses.append(response)
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(
OrderRecord(
trade_ref=trade_ref,
@@ -265,7 +272,7 @@ class TriangularExecutionSequencer:
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
+21 -16
View File
@@ -38,7 +38,8 @@ class MarketDataFeed:
opportunity_writer: AsyncOpportunityWriter | None = None,
paper_trading_mode: bool = True,
opportunity_executor: (
Callable[[OpportunityEvent], Awaitable[ExecutionOutcome | float | None]] | None
Callable[[OpportunityEvent],
Awaitable[ExecutionOutcome | float | None]] | None
) = None,
trade_capital: float = 1.0,
max_trade_capital: float | None = None,
@@ -92,7 +93,8 @@ class MarketDataFeed:
return {}
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}
async def run(self) -> None:
@@ -144,7 +146,7 @@ class MarketDataFeed:
symbol=delta.symbol,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -172,7 +174,7 @@ class MarketDataFeed:
for event in opportunities:
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="detector",
@@ -207,7 +209,7 @@ class MarketDataFeed:
net_pct=event.net_pct,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
@@ -228,7 +230,7 @@ class MarketDataFeed:
updated_pair=event.updated_pair,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
@@ -250,7 +252,7 @@ class MarketDataFeed:
reason=self._kill_switch.reason,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -275,7 +277,7 @@ class MarketDataFeed:
reason=self._stop_conditions_guard.halted_reason,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -298,7 +300,7 @@ class MarketDataFeed:
reason=self._loss_limit_guard.halted_reason,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -313,7 +315,8 @@ class MarketDataFeed:
continue
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 = {
asset.upper(): amount
for asset, amount in self._balance_provider().items()
@@ -329,7 +332,7 @@ class MarketDataFeed:
required_by_asset=required_balances,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -358,7 +361,7 @@ class MarketDataFeed:
exposure_by_asset=exposure_by_asset,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="risk_manager",
@@ -381,7 +384,8 @@ class MarketDataFeed:
outcome = await self._opportunity_executor(event)
except Exception as exc:
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(
self._alert_notifier,
@@ -420,7 +424,7 @@ class MarketDataFeed:
updated_pair=event.updated_pair,
)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
@@ -447,7 +451,8 @@ class MarketDataFeed:
realized_pnl = outcome
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:
_LOG.warning(
"loss_limit_halt_triggered",
@@ -459,7 +464,7 @@ class MarketDataFeed:
self._trade_limits_guard.close_trade(exposure_by_asset)
if self._audit_repository is not None:
self._audit_repository.insert(
await self._audit_repository.insert(
AuditRecord(
occurred_at=datetime.now(UTC),
actor="execution_engine",
+16 -4
View File
@@ -521,7 +521,11 @@ class ConfigSettingRepository:
""",
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]:
"""List all configuration settings, optionally filtered by section."""
@@ -567,7 +571,7 @@ class ConfigSettingRepository:
ts = row["latest_updated_at"]
if isinstance(ts, str):
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
return ts
return ts # type: ignore[no-any-return]
return None
@@ -663,7 +667,11 @@ class ConfigPairingRepository:
""",
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:
"""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",
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
import pytest
import pytest_asyncio
from arbitrade.config.settings import get_settings
from arbitrade.storage.pg_store import PgStore
@@ -10,8 +10,8 @@ import pytest
from arbitrade.config.settings import get_settings
from arbitrade.detection.engine import OpportunityEvent
from arbitrade.execution.sequencer import TriangularExecutionSequencer
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.executions import AsyncExecutionWriter
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import OrderRepository, PnLRepository, TradeRepository
pytestmark = pytest.mark.integration
-1
View File
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
from datetime import UTC, datetime, timedelta
import pytest
import pytest_asyncio
from arbitrade.config.settings import get_settings
from arbitrade.metrics import MetricsCalculator
+1 -1
View File
@@ -8,8 +8,8 @@ import pytest
from arbitrade.config.settings import get_settings
from arbitrade.detection.engine import OpportunityEvent
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.opportunities import AsyncOpportunityWriter
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import OpportunityRepository
pytestmark = pytest.mark.integration
+2 -1
View File
@@ -13,7 +13,7 @@ from contextlib import asynccontextmanager
import pytest
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
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."""
await pg.migrate()
from datetime import UTC, datetime
from arbitrade.storage.repositories import AuditRecord, AuditRepository
repo = AuditRepository(pg)