summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 17:24:57 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 17:24:57 +0900
commit21c6b777530b4a027aec9c12bf63092e5a7c006d (patch)
tree0c4302b258a3318d473e31bfba391ba05e247d92
parent4e6ae373b6abc7ef0d5fb810385d14250757f3f1 (diff)
feat: add multi-exchange support via ccxt factory
-rw-r--r--.env.example2
-rw-r--r--services/data-collector/src/data_collector/binance_ws.py6
-rw-r--r--services/order-executor/src/order_executor/main.py13
-rw-r--r--shared/src/shared/config.py2
-rw-r--r--shared/src/shared/exchange.py51
-rw-r--r--shared/tests/test_exchange.py55
6 files changed, 121 insertions, 8 deletions
diff --git a/.env.example b/.env.example
index 9b3cf9c..d082395 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1,5 @@
+EXCHANGE_ID=binance
+EXCHANGE_SANDBOX=false
BINANCE_API_KEY=
BINANCE_API_SECRET=
REDIS_URL=redis://localhost:6379
diff --git a/services/data-collector/src/data_collector/binance_ws.py b/services/data-collector/src/data_collector/binance_ws.py
index a1c81d6..e25e7a6 100644
--- a/services/data-collector/src/data_collector/binance_ws.py
+++ b/services/data-collector/src/data_collector/binance_ws.py
@@ -1,4 +1,8 @@
-"""Binance WebSocket client for real-time kline/candle data."""
+"""Binance WebSocket client for real-time kline/candle data.
+
+NOTE: This module is Binance-specific (uses Binance WebSocket URL and message format).
+Multi-exchange WebSocket support would require exchange-specific implementations.
+"""
import asyncio
import json
diff --git a/services/order-executor/src/order_executor/main.py b/services/order-executor/src/order_executor/main.py
index 0198f65..24a166e 100644
--- a/services/order-executor/src/order_executor/main.py
+++ b/services/order-executor/src/order_executor/main.py
@@ -3,12 +3,11 @@
import asyncio
from decimal import Decimal
-import ccxt.async_support as ccxt
-
from shared.broker import RedisBroker
from shared.db import Database
from shared.events import Event, EventType
from shared.healthcheck import HealthCheckServer
+from shared.exchange import create_exchange
from shared.logging import setup_logging
from shared.metrics import ServiceMetrics
from shared.notifier import TelegramNotifier
@@ -32,11 +31,11 @@ async def run() -> None:
broker = RedisBroker(config.redis_url)
- exchange = ccxt.binance(
- {
- "apiKey": config.binance_api_key,
- "secret": config.binance_api_secret,
- }
+ exchange = create_exchange(
+ exchange_id=config.exchange_id,
+ api_key=config.binance_api_key,
+ api_secret=config.binance_api_secret,
+ sandbox=config.exchange_sandbox,
)
risk_manager = RiskManager(
diff --git a/shared/src/shared/config.py b/shared/src/shared/config.py
index 7b34d78..867702b 100644
--- a/shared/src/shared/config.py
+++ b/shared/src/shared/config.py
@@ -8,6 +8,8 @@ class Settings(BaseSettings):
binance_api_secret: str
redis_url: str = "redis://localhost:6379"
database_url: str = "postgresql://trading:trading@localhost:5432/trading"
+ exchange_id: str = "binance" # Any ccxt exchange ID
+ exchange_sandbox: bool = False # Use sandbox/testnet mode
log_level: str = "INFO"
risk_max_position_size: float = 0.1
risk_stop_loss_pct: float = 5.0
diff --git a/shared/src/shared/exchange.py b/shared/src/shared/exchange.py
new file mode 100644
index 0000000..482bfda
--- /dev/null
+++ b/shared/src/shared/exchange.py
@@ -0,0 +1,51 @@
+"""Exchange factory using ccxt."""
+
+import ccxt.async_support as ccxt
+
+
+def create_exchange(
+ exchange_id: str,
+ api_key: str,
+ api_secret: str,
+ sandbox: bool = False,
+) -> ccxt.Exchange:
+ """Create a ccxt async exchange instance by ID.
+
+ Args:
+ exchange_id: ccxt exchange ID (e.g. 'binance', 'bybit', 'okx', 'kraken')
+ api_key: API key
+ api_secret: API secret
+ sandbox: Use sandbox/testnet mode
+
+ Returns:
+ Configured ccxt async exchange instance
+
+ Raises:
+ ValueError: If exchange_id is not supported by ccxt
+ """
+ if not hasattr(ccxt, exchange_id):
+ available = [
+ x
+ for x in dir(ccxt)
+ if not x.startswith("_")
+ and isinstance(getattr(ccxt, x, None), type)
+ and issubclass(getattr(ccxt, x), ccxt.Exchange)
+ ]
+ raise ValueError(
+ f"Unknown exchange '{exchange_id}'. "
+ f"Available: {', '.join(sorted(available)[:20])}..."
+ )
+
+ exchange_cls = getattr(ccxt, exchange_id)
+ exchange = exchange_cls(
+ {
+ "apiKey": api_key,
+ "secret": api_secret,
+ "enableRateLimit": True,
+ }
+ )
+
+ if sandbox:
+ exchange.set_sandbox_mode(True)
+
+ return exchange
diff --git a/shared/tests/test_exchange.py b/shared/tests/test_exchange.py
new file mode 100644
index 0000000..95dc7d7
--- /dev/null
+++ b/shared/tests/test_exchange.py
@@ -0,0 +1,55 @@
+"""Tests for the exchange factory."""
+
+from unittest.mock import patch
+
+import ccxt.async_support as ccxt
+import pytest
+
+from shared.exchange import create_exchange
+
+
+def test_create_exchange_binance():
+ """Verify create_exchange returns a ccxt.binance instance."""
+ exchange = create_exchange(
+ exchange_id="binance",
+ api_key="test-key",
+ api_secret="test-secret",
+ )
+ assert isinstance(exchange, ccxt.binance)
+ assert exchange.apiKey == "test-key"
+ assert exchange.secret == "test-secret"
+ assert exchange.enableRateLimit is True
+
+
+def test_create_exchange_unknown():
+ """Verify create_exchange raises ValueError for unknown exchange."""
+ with pytest.raises(ValueError, match="Unknown exchange 'not_a_real_exchange'"):
+ create_exchange(
+ exchange_id="not_a_real_exchange",
+ api_key="key",
+ api_secret="secret",
+ )
+
+
+def test_create_exchange_with_sandbox():
+ """Verify sandbox mode is activated when sandbox=True."""
+ with patch.object(ccxt.binance, "set_sandbox_mode") as mock_sandbox:
+ exchange = create_exchange(
+ exchange_id="binance",
+ api_key="key",
+ api_secret="secret",
+ sandbox=True,
+ )
+ mock_sandbox.assert_called_once_with(True)
+ assert isinstance(exchange, ccxt.binance)
+
+
+def test_create_exchange_no_sandbox_by_default():
+ """Verify sandbox mode is not set when sandbox=False (default)."""
+ with patch.object(ccxt.binance, "set_sandbox_mode") as mock_sandbox:
+ create_exchange(
+ exchange_id="binance",
+ api_key="key",
+ api_secret="secret",
+ )
+ mock_sandbox.assert_not_called()