- Added FastAPI backend with core API endpoints for strategies, backtests, and data management. - Introduced Vue 3 frontend with a dark theme, enabling users to run backtests, adjust parameters, and compare results. - Implemented Pydantic schemas for request/response validation and SQLAlchemy models for database interactions. - Enhanced project structure with dedicated modules for services, routers, and components. - Updated dependencies in `pyproject.toml` and `frontend/package.json` to include FastAPI, SQLAlchemy, and Vue-related packages. - Improved `.gitignore` to exclude unnecessary files and directories.
339 lines
10 KiB
Python
339 lines
10 KiB
Python
"""
|
|
OKX Exchange Client for Live Trading.
|
|
|
|
Handles connection to OKX API, order execution, and account management.
|
|
Supports demo/sandbox mode for paper trading.
|
|
"""
|
|
import logging
|
|
from typing import Optional
|
|
from datetime import datetime, timezone
|
|
|
|
import ccxt
|
|
|
|
from .config import OKXConfig, TradingConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OKXClient:
|
|
"""
|
|
OKX Exchange client wrapper using CCXT.
|
|
|
|
Supports both live and demo (sandbox) trading modes.
|
|
Demo mode uses OKX's official sandbox environment.
|
|
"""
|
|
|
|
def __init__(self, okx_config: OKXConfig, trading_config: TradingConfig):
|
|
self.okx_config = okx_config
|
|
self.trading_config = trading_config
|
|
self.exchange: Optional[ccxt.okx] = None
|
|
self._setup_exchange()
|
|
|
|
def _setup_exchange(self) -> None:
|
|
"""Initialize CCXT OKX exchange instance."""
|
|
self.okx_config.validate()
|
|
|
|
config = {
|
|
'apiKey': self.okx_config.api_key,
|
|
'secret': self.okx_config.secret,
|
|
'password': self.okx_config.password,
|
|
'sandbox': self.okx_config.demo_mode,
|
|
'options': {
|
|
'defaultType': 'swap', # Perpetual futures
|
|
},
|
|
'timeout': 30000,
|
|
'enableRateLimit': True,
|
|
}
|
|
|
|
self.exchange = ccxt.okx(config)
|
|
|
|
mode_str = "DEMO/SANDBOX" if self.okx_config.demo_mode else "LIVE"
|
|
logger.info(f"OKX Exchange initialized in {mode_str} mode")
|
|
|
|
# Configure trading settings
|
|
self._configure_trading_settings()
|
|
|
|
def _configure_trading_settings(self) -> None:
|
|
"""Configure leverage and margin mode."""
|
|
symbol = self.trading_config.eth_symbol
|
|
leverage = self.trading_config.leverage
|
|
margin_mode = self.trading_config.margin_mode
|
|
|
|
try:
|
|
# Set position mode to one-way (net) first
|
|
self.exchange.set_position_mode(False) # False = one-way mode
|
|
logger.info("Position mode set to One-Way (Net)")
|
|
except Exception as e:
|
|
# Position mode might already be set
|
|
logger.debug(f"Position mode setting: {e}")
|
|
|
|
try:
|
|
# Set margin mode with leverage parameter (required by OKX)
|
|
self.exchange.set_margin_mode(
|
|
margin_mode,
|
|
symbol,
|
|
params={'lever': leverage}
|
|
)
|
|
logger.info(
|
|
f"Margin mode set to {margin_mode} with {leverage}x leverage "
|
|
f"for {symbol}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Could not set margin mode: {e}")
|
|
# Try setting leverage separately
|
|
try:
|
|
self.exchange.set_leverage(leverage, symbol)
|
|
logger.info(f"Leverage set to {leverage}x for {symbol}")
|
|
except Exception as e2:
|
|
logger.warning(f"Could not set leverage: {e2}")
|
|
|
|
def fetch_ohlcv(
|
|
self,
|
|
symbol: str,
|
|
timeframe: str = "1h",
|
|
limit: int = 500
|
|
) -> list:
|
|
"""
|
|
Fetch OHLCV candle data.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol (e.g., "ETH/USDT:USDT")
|
|
timeframe: Candle timeframe (e.g., "1h")
|
|
limit: Number of candles to fetch
|
|
|
|
Returns:
|
|
List of OHLCV data
|
|
"""
|
|
return self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
|
|
|
def get_balance(self) -> dict:
|
|
"""
|
|
Get account balance.
|
|
|
|
Returns:
|
|
Balance dictionary with 'total' and 'free' USDT amounts
|
|
"""
|
|
balance = self.exchange.fetch_balance()
|
|
return {
|
|
'total': balance.get('USDT', {}).get('total', 0),
|
|
'free': balance.get('USDT', {}).get('free', 0),
|
|
}
|
|
|
|
def get_positions(self) -> list:
|
|
"""
|
|
Get open positions.
|
|
|
|
Returns:
|
|
List of open position dictionaries
|
|
"""
|
|
positions = self.exchange.fetch_positions()
|
|
return [p for p in positions if float(p.get('contracts', 0)) != 0]
|
|
|
|
def get_position(self, symbol: str) -> Optional[dict]:
|
|
"""
|
|
Get position for a specific symbol.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
|
|
Returns:
|
|
Position dictionary or None if no position
|
|
"""
|
|
positions = self.get_positions()
|
|
for pos in positions:
|
|
if pos.get('symbol') == symbol:
|
|
return pos
|
|
return None
|
|
|
|
def place_market_order(
|
|
self,
|
|
symbol: str,
|
|
side: str,
|
|
amount: float,
|
|
reduce_only: bool = False
|
|
) -> dict:
|
|
"""
|
|
Place a market order.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
side: "buy" or "sell"
|
|
amount: Order amount in base currency
|
|
reduce_only: If True, only reduce existing position
|
|
|
|
Returns:
|
|
Order result dictionary
|
|
"""
|
|
params = {
|
|
'tdMode': self.trading_config.margin_mode,
|
|
}
|
|
if reduce_only:
|
|
params['reduceOnly'] = True
|
|
|
|
order = self.exchange.create_market_order(
|
|
symbol, side, amount, params=params
|
|
)
|
|
logger.info(
|
|
f"Market {side.upper()} order placed: {amount} {symbol} "
|
|
f"@ market price, order_id={order['id']}"
|
|
)
|
|
return order
|
|
|
|
def place_limit_order(
|
|
self,
|
|
symbol: str,
|
|
side: str,
|
|
amount: float,
|
|
price: float,
|
|
reduce_only: bool = False
|
|
) -> dict:
|
|
"""
|
|
Place a limit order.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
side: "buy" or "sell"
|
|
amount: Order amount in base currency
|
|
price: Limit price
|
|
reduce_only: If True, only reduce existing position
|
|
|
|
Returns:
|
|
Order result dictionary
|
|
"""
|
|
params = {
|
|
'tdMode': self.trading_config.margin_mode,
|
|
}
|
|
if reduce_only:
|
|
params['reduceOnly'] = True
|
|
|
|
order = self.exchange.create_limit_order(
|
|
symbol, side, amount, price, params=params
|
|
)
|
|
logger.info(
|
|
f"Limit {side.upper()} order placed: {amount} {symbol} "
|
|
f"@ {price}, order_id={order['id']}"
|
|
)
|
|
return order
|
|
|
|
def set_stop_loss_take_profit(
|
|
self,
|
|
symbol: str,
|
|
side: str,
|
|
amount: float,
|
|
stop_loss_price: float,
|
|
take_profit_price: float
|
|
) -> tuple:
|
|
"""
|
|
Set stop-loss and take-profit orders.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
side: Position side ("long" or "short")
|
|
amount: Position size
|
|
stop_loss_price: Stop-loss trigger price
|
|
take_profit_price: Take-profit trigger price
|
|
|
|
Returns:
|
|
Tuple of (sl_order, tp_order)
|
|
"""
|
|
# For long position: SL sells, TP sells
|
|
# For short position: SL buys, TP buys
|
|
close_side = "sell" if side == "long" else "buy"
|
|
|
|
# Stop-loss order
|
|
sl_params = {
|
|
'tdMode': self.trading_config.margin_mode,
|
|
'reduceOnly': True,
|
|
'stopLossPrice': stop_loss_price,
|
|
}
|
|
|
|
try:
|
|
sl_order = self.exchange.create_order(
|
|
symbol, 'market', close_side, amount,
|
|
params={
|
|
'tdMode': self.trading_config.margin_mode,
|
|
'reduceOnly': True,
|
|
'slTriggerPx': str(stop_loss_price),
|
|
'slOrdPx': '-1', # Market price
|
|
}
|
|
)
|
|
logger.info(f"Stop-loss set at {stop_loss_price}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not set stop-loss: {e}")
|
|
sl_order = None
|
|
|
|
# Take-profit order
|
|
try:
|
|
tp_order = self.exchange.create_order(
|
|
symbol, 'market', close_side, amount,
|
|
params={
|
|
'tdMode': self.trading_config.margin_mode,
|
|
'reduceOnly': True,
|
|
'tpTriggerPx': str(take_profit_price),
|
|
'tpOrdPx': '-1', # Market price
|
|
}
|
|
)
|
|
logger.info(f"Take-profit set at {take_profit_price}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not set take-profit: {e}")
|
|
tp_order = None
|
|
|
|
return sl_order, tp_order
|
|
|
|
def close_position(self, symbol: str) -> Optional[dict]:
|
|
"""
|
|
Close an open position.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
|
|
Returns:
|
|
Order result or None if no position
|
|
"""
|
|
position = self.get_position(symbol)
|
|
if not position:
|
|
logger.info(f"No open position for {symbol}")
|
|
return None
|
|
|
|
contracts = abs(float(position.get('contracts', 0)))
|
|
if contracts == 0:
|
|
return None
|
|
|
|
side = position.get('side', 'long')
|
|
close_side = "sell" if side == "long" else "buy"
|
|
|
|
order = self.place_market_order(
|
|
symbol, close_side, contracts, reduce_only=True
|
|
)
|
|
logger.info(f"Position closed for {symbol}")
|
|
return order
|
|
|
|
def get_ticker(self, symbol: str) -> dict:
|
|
"""
|
|
Get current ticker/price for a symbol.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
|
|
Returns:
|
|
Ticker dictionary with 'last', 'bid', 'ask' prices
|
|
"""
|
|
return self.exchange.fetch_ticker(symbol)
|
|
|
|
def get_funding_rate(self, symbol: str) -> float:
|
|
"""
|
|
Get current funding rate for a perpetual symbol.
|
|
|
|
Args:
|
|
symbol: Trading pair symbol
|
|
|
|
Returns:
|
|
Current funding rate as decimal
|
|
"""
|
|
try:
|
|
funding = self.exchange.fetch_funding_rate(symbol)
|
|
return float(funding.get('fundingRate', 0))
|
|
except Exception as e:
|
|
logger.warning(f"Could not fetch funding rate: {e}")
|
|
return 0.0
|