Implement FastAPI backend and Vue 3 frontend for Lowkey Backtest UI
- 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.
This commit is contained in:
338
live_trading/okx_client.py
Normal file
338
live_trading/okx_client.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user