UNPKG

skayn-trading-sdk

Version:

Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture

455 lines (397 loc) 12.7 kB
const IExchangeProvider = require('../../interfaces/IExchangeProvider'); /** * LN Markets Exchange Provider - SDK Compliant * Implements IExchangeProvider interface for Lightning Network futures trading */ class LNMarketsProvider extends IExchangeProvider { constructor(config = {}) { super({ ...config, exchangeType: 'lightning', custodyModel: 'custodial' }); this.apiClient = null; this.positions = new Map(); this.balance = 0; this.network = config.network || 'testnet'; } async connect() { try { // Dynamically import LN Markets API const api = await import('@ln-markets/api'); this.apiClient = api.createRestClient({ key: this.config.key, secret: this.config.secret, passphrase: this.config.passphrase, network: this.network }); // Test connection const userInfo = await this.apiClient.userGet(); this.balance = userInfo.balance || 1000; // Mock balance for testnet this.isConnected = true; return true; } catch (error) { throw this.mapError(error); } } async disconnect() { this.isConnected = false; this.apiClient = null; } async executeSignal(signal, executionParams = {}) { if (!this.isConnected) { throw new Error('Not connected to LN Markets'); } const { quantity = executionParams.quantity || 0.001, leverage = executionParams.leverage || 1, symbol = 'BTCUSD' } = executionParams; try { let result; if (signal.action === 'BUY') { result = await this.placeMarketOrder('buy', quantity, symbol, { leverage }); } else if (signal.action === 'SELL') { result = await this.placeMarketOrder('sell', quantity, symbol, { leverage }); } else { return { success: false, reason: 'Invalid signal action' }; } return { success: true, signal, execution: result, timestamp: new Date().toISOString() }; } catch (error) { return { success: false, error: this.mapError(error), signal, timestamp: new Date().toISOString() }; } } async placeMarketOrder(side, amount, symbol = 'BTCUSD', options = {}) { const { leverage = 1 } = options; const currentPrice = await this.getCurrentPrice(symbol); // Calculate margin in sats const positionValueUSD = amount * currentPrice; const marginUSD = positionValueUSD / leverage; const marginSats = Math.floor((marginUSD / currentPrice) * 100000000); const params = { side: side === 'buy' ? 'b' : 's', type: 'm', // market order margin: Math.max(marginSats, 1000), // Minimum 1000 sats leverage }; try { const position = await this.apiClient.futuresNewTrade(params); this.positions.set(position.id, position); return { success: true, orderId: position.id, executedPrice: position.price, executedQuantity: position.quantity, side, symbol, leverage, margin: position.margin, fees: this.calculateFees(position, currentPrice) }; } catch (error) { throw this.mapError(error); } } async placeLimitOrder(side, amount, price, symbol = 'BTCUSD', options = {}) { // LN Markets primarily uses market orders, but we can simulate limit behavior const currentPrice = await this.getCurrentPrice(symbol); // For now, treat as market order if price is within reasonable range if (Math.abs(price - currentPrice) / currentPrice < 0.001) { return this.placeMarketOrder(side, amount, symbol, options); } throw new Error('LN Markets does not support limit orders in current API version'); } async cancelOrder(orderId, symbol) { try { await this.apiClient.futuresCloseTrade(orderId); this.positions.delete(orderId); return { success: true, orderId }; } catch (error) { throw this.mapError(error); } } async getPositions(symbol = null) { try { const positions = await this.apiClient.futuresGetTrades({ type: 'running' }); // Update local position tracking this.positions.clear(); positions.forEach(pos => this.positions.set(pos.id, pos)); return positions.map(pos => ({ id: pos.id, symbol: 'BTCUSD', side: pos.side === 'b' ? 'long' : 'short', quantity: pos.quantity, entryPrice: pos.price, currentPrice: pos.current_price || pos.price, leverage: pos.leverage, margin: pos.margin, unrealizedPnL: pos.pl || 0, timestamp: pos.creation_ts, exchange: 'LNMarkets' })); } catch (error) { throw this.mapError(error); } } async getBalance(asset = null) { try { const userInfo = await this.apiClient.userGet(); this.balance = userInfo.balance || this.balance; return { totalBalance: this.balance, availableBalance: this.balance, currency: 'sats', usdValue: (this.balance / 100000000) * (await this.getCurrentPrice('BTCUSD')) }; } catch (error) { return { totalBalance: this.balance, availableBalance: this.balance, currency: 'sats', usdValue: (this.balance / 100000000) * 45000 // Fallback price }; } } async getOrderHistory(symbol, limit = 100) { try { const trades = await this.apiClient.futuresGetTrades({ type: 'closed', limit }); return trades.map(trade => ({ orderId: trade.id, symbol: 'BTCUSD', side: trade.side === 'b' ? 'buy' : 'sell', quantity: trade.quantity, price: trade.price, status: 'filled', timestamp: trade.creation_ts, fees: this.calculateFees(trade, trade.exit_price || trade.price) })); } catch (error) { throw this.mapError(error); } } async getCurrentPrice(symbol = 'BTCUSD') { try { const ticker = await this.apiClient.futuresGetTicker(); return ticker.index || 45000; // Fallback price } catch (error) { // Fallback to external price service if available return 45000; } } supportsFeature(feature) { const supportedFeatures = { 'leverage': true, 'futures': true, 'lightning': true, 'stopLoss': true, 'takeProfit': false, 'marketOrders': true, 'limitOrders': false, 'websocket': true }; return supportedFeatures[feature] || false; } getSupportedFeatures() { return [ 'leverage', 'futures', 'lightning', 'stopLoss', 'marketOrders', 'websocket' ]; } getConfigRequirements() { return { required: ['key', 'secret', 'passphrase'], optional: ['network'], schema: { key: { type: 'string', description: 'LN Markets API key' }, secret: { type: 'string', description: 'LN Markets API secret' }, passphrase: { type: 'string', description: 'LN Markets API passphrase' }, network: { type: 'string', enum: ['testnet', 'mainnet'], default: 'testnet', description: 'Network to connect to' } } }; } validateConfig(config) { const issues = []; const requirements = this.getConfigRequirements(); requirements.required.forEach(field => { if (!config[field]) { issues.push(`Missing required field: ${field}`); } }); if (config.network && !['testnet', 'mainnet'].includes(config.network)) { issues.push('Network must be either "testnet" or "mainnet"'); } return { valid: issues.length === 0, issues }; } calculatePositionSize(signal, portfolioValue, currentPrice) { const maxRiskPercent = 0.02; // 2% max risk per trade const riskAmount = portfolioValue * maxRiskPercent; const quantity = riskAmount / currentPrice; return { quantity, riskAmount, positionValue: quantity * currentPrice, leverage: signal.leverage || 1, margin: (quantity * currentPrice) / (signal.leverage || 1) }; } async setupRiskManagement(riskParams) { // Store risk parameters for position sizing this.riskParams = { maxPositionSize: riskParams.maxPositionSize || 0.01, maxLeverage: riskParams.maxLeverage || 10, stopLossPercent: riskParams.stopLossPercent || 5, ...riskParams }; } async emergencyStop() { try { const positions = await this.getPositions(); const closeResults = []; for (const position of positions) { try { await this.cancelOrder(position.id); closeResults.push({ positionId: position.id, success: true }); } catch (error) { closeResults.push({ positionId: position.id, success: false, error: error.message }); } } return { success: true, closedPositions: closeResults.length, results: closeResults, timestamp: new Date().toISOString() }; } catch (error) { throw this.mapError(error); } } calculateFees(position, currentPrice) { const positionValue = position.quantity * position.price; const exitValue = position.quantity * currentPrice; const feeRate = 0.001; // 0.1% LN Markets fee return { opening: positionValue * feeRate, closing: exitValue * feeRate, funding: positionValue * 0.0001, // Conservative funding estimate total: (positionValue + exitValue) * feeRate + positionValue * 0.0001, details: { feeRate, exchange: 'LN Markets', currency: 'USD' } }; } calculateEnhancedPnL(position, currentPrice) { if (!position || !currentPrice) return null; const entryPrice = position.price; const quantity = position.quantity; const leverage = position.leverage || 1; const margin = position.margin; // Position values const positionValue = quantity * entryPrice; const currentValue = quantity * currentPrice; const priceChange = currentValue - positionValue; // Apply leverage to P&L const leveragedPnL = priceChange * leverage; // Calculate fees const fees = this.calculateFees(position, currentPrice); // Net P&L after fees const netPnLUSD = leveragedPnL - fees.total; const netPnLSats = Math.round((netPnLUSD / currentPrice) * 100000000); // Percentage calculations const priceChangePercent = ((currentPrice / entryPrice) - 1) * 100; const marginUSD = (margin / 100000000) * currentPrice; const returnPercent = (netPnLUSD / marginUSD) * 100; return { entryPrice, currentPrice, priceChange: priceChangePercent, grossPnL: priceChange, leveragedPnL, netPnLUSD, netPnLSats, fees, totalFees: fees.total, returnPercent, marginUSD, apiPnLSats: position.pl || 0, exchange: 'LN Markets' }; } getFeeStructure() { return { exchange: 'LN Markets', feeModel: 'tier-based', tiers: [ { volume: '0-100 BTC', rate: '0.1%' }, { volume: '100-500 BTC', rate: '0.08%' }, { volume: '500+ BTC', rate: '0.06%' } ], fundingRate: { frequency: 'Every 8 hours', times: ['00:00 UTC', '08:00 UTC', '16:00 UTC'], calculation: 'Aggregated methodology across exchanges', typical: '~0.01% per 8h period' }, notes: [ 'Opening and closing fees deducted from maintenance margin', 'Funding rates can be positive or negative', 'Long positions pay when funding is positive', 'Short positions receive when funding is positive' ] }; } mapError(error) { let errorType = 'exchange_error'; let retryable = false; if (error.status === 500) { errorType = 'server_error'; retryable = true; } else if (error.status === 429) { errorType = 'rate_limit'; retryable = true; } else if (error.status === 401) { errorType = 'authentication_error'; } else if (error.status === 400) { errorType = 'invalid_request'; } return { type: errorType, exchange: 'LNMarkets', originalError: error.message, code: error.status || error.code || 'unknown', timestamp: new Date().toISOString(), retryable }; } } module.exports = LNMarketsProvider;