skayn-trading-sdk
Version:
Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture
455 lines (397 loc) • 12.7 kB
JavaScript
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;