Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
djkazic committed Nov 8, 2023
0 parents commit 0f266ff
Show file tree
Hide file tree
Showing 4 changed files with 801 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
387 changes: 387 additions & 0 deletions main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
// Dependencies
import chalk from 'chalk';
import dotenv from 'dotenv';
import Taapi from 'taapi';
import { exec } from 'child_process';

dotenv.config();

function logger(level, message) {
const timestamp = new Date().toISOString();
let color = chalk.hex('#15B5B0');
if (level == 'error') {
color = chalk.red;
} else if (level == 'warn') {
color = chalk.hex('#FFA500');
} else if (level == 'finance') {
color = chalk.hex('#3EAB76');
} else if (level == 'finance-profit') {
color = chalk.hex('#FF00DD');
}
console.log(color(`${timestamp} - ${message}`));
}

function sendTelegramMessage(message) {
exec(`telegram-send "${message}"`, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
return;
}
if (stderr) {
console.error(`Stderr: ${stderr}`);
return;
}
// console.log(`Stdout: ${stdout}`);
});
}

// Dynamically import the ES Module '@ln-markets/api'
async function loadModules() {
const { createRestClient, createWebsocketClient } = await import('@ln-markets/api');
// Configuration
const apiConfig = {
key: process.env.LNM_API_KEY,
secret: process.env.LNM_API_SECRET,
passphrase: process.env.LNM_PASSPHRASE
};
const taapiClient = new Taapi.default(process.env.TAAPI_API_KEY);

// RestClient Initialization
const restClient = createRestClient(apiConfig);

// Retry
let retryCount = 0;
const maxRetries = 5;

// Trading State
let lastTradeTime = 0;
let lastPrice = null;
let indexPrice = null;
const fetchInterval = 60000;
let lastCalledTime = Date.now() - fetchInterval;
let lastTradeLogicCall = 0;
let rsiData = [];
const period = 14;
const tradeLogicCooldown = 60 * 1000;

const canMakeTrade = () => Date.now() - lastTradeTime > 1000;

function calculateMovingAverage(data, period) {
let movingAverages = [];
for (let i = period - 1; i < data.length; i++) {
let sum = 0;
for (let j = 0; j < period; j++) {
if (isNaN(data[i - j])) {
logger('error', `Non-numeric data encountered: ${data[i - j]}`);
return NaN; // Or handle this case as appropriate
}
sum += data[i - j];
}
let average = sum / period;
movingAverages.push(average);
}
// logger('debug', `Calculated moving averages: ${movingAverages}`);
return movingAverages.length > 0 ? movingAverages[movingAverages.length - 1] : NaN;
}

function addRsiSample(sample) {
if (rsiData.length > 14) {
// Remove the oldest sample to make room for the new one
rsiData.shift();
}
// Add the new RSI sample
rsiData.push(sample);
}

async function fetchRSI(timeframe) {
const currentTime = Date.now();
if (currentTime - lastCalledTime < 15000) {
return Promise.reject('RSI call is on cooldown');
}
try {
// Call the RSI function and update the last called time
const rsiResponse = await taapiClient.getIndicator('rsi', 'BTC/USDT', timeframe);
lastCalledTime = currentTime;
return rsiResponse;
} catch (error) {
console.error(`Failed to fetch RSI: ${error.message}`);
return Promise.reject(error);
}
}

async function fetchBbands(timeframe) {
const currentTime = Date.now();
if (currentTime - lastCalledTime < 15000) {
return Promise.reject('RSI call is on cooldown');
}
try {
// Call the RSI function and update the last called time
const bbandsResponse = await taapiClient.getIndicator('bbands', 'BTC/USDT', timeframe);
lastCalledTime = currentTime;
return bbandsResponse;
} catch (error) {
console.error(`Failed to fetch Bbands: ${error.message}`);
return Promise.reject(error);
}
}

async function fetchUltosc(timeframe) {
const currentTime = Date.now();
if (currentTime - lastCalledTime < 15000) {
return Promise.reject('Ultosc call is on cooldown');
}
try {
const ultoscResponse = await taapiClient.getIndicator('ultosc', 'BTC/USDT', timeframe);
lastCalledTime = currentTime;
return ultoscResponse;
} catch (error) {
console.error(`Failed to fetch ultosc: ${error.message}`);
return Promise.reject(error);
}
}

async function fetchStddev(timeframe) {
const currentTime = Date.now();
if (currentTime - lastCalledTime < 15000) {
return Promise.reject('Stddev call is on cooldown');
}
try {
const stddevResponse = await taapiClient.getIndicator('stddev', 'BTC/USDT', timeframe);
lastCalledTime = currentTime;
return stddevResponse;
} catch (error) {
console.error(`Failed to fetch stddev: ${error.message}`);
return Promise.reject(error);
}
}

async function fetchMacd(timeframe) {
const currentTime = Date.now();
if (currentTime - lastCalledTime < 15000) {
return Promise.reject('Macd call is on cooldown');
}
try {
const macdResponse = await taapiClient.getIndicator('macd', 'BTC/USDT', timeframe);
lastCalledTime = currentTime;
return macdResponse;
} catch (error) {
console.error(`Failed to fetch macd: ${error.message}`);
return Promise.reject(error);
}
}

function shouldCallTradeLogic() {
const now = Date.now();
return (now - lastTradeLogicCall) >= tradeLogicCooldown;
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Trading logic
async function tradeLogic() {
if (!canMakeTrade()) {
logger('error', 'Trade not possible: Cooling down period has not elapsed.');
return;
}
if (lastPrice === null || indexPrice === null) {
return;
}
// Fetch positions
let totalSellExposure = 0;
let totalBuyExposure = 0;
try {
let runningPositions = await restClient.futuresGetTrades({
"type": "running",
});
// Calculate total exposure for both sides
let profitableSells = 0;
let profitableBuys = 0;
let sellPl = 0;
let buyPl = 0;
let changedPos = false;
runningPositions.forEach(position => {
if (position.side === 's') {
totalSellExposure += position.quantity;
if (position.pl > 10) {
logger('finance-profit', `CLOSING SHORT POSITION ${JSON.stringify(position)}`);
restClient.futuresCloseTrade(position.id);
profitableSells += 2;
sendTelegramMessage(`Closed profitable short on LNM: ${JSON.stringify(position)}`);
changedPos = true;
} else if (position.pl < -20) {
logger('error', `CLOSING SHORT POSITION AT LOSS ${JSON.stringify(position)}`);
restClient.futuresCloseTrade(position.id);
sendTelegramMessage(`Closed short at a loss on LNM: ${JSON.stringify(position)}`);
changedPos = true;
}
} else if (position.side === 'b') {
totalBuyExposure += position.quantity;
if (position.pl > 10) {
logger('finance-profit', `CLOSING LONG POSITION ${JSON.stringify(position)}`);
restClient.futuresCloseTrade(position.id);
profitableBuys += 2;
sendTelegramMessage(`Closed profitable long on LNM: ${JSON.stringify(position)}`);
changedPos = true;
} else if (position.pl < -20) {
logger('error', `CLOSING LONG POSITION AT LOSS ${JSON.stringify(position)}`);
restClient.futuresCloseTrade(position.id);
sendTelegramMessage(`Closed long at a loss on LNM: ${JSON.stringify(position)}`);
changedPos = true;
}
}
});
// Reread positions
if (changedPos) {
totalSellExposure = 0;
totalBuyExposure = 0;
await sleep(1000);
runningPositions = await restClient.futuresGetTrades({
"type": "running",
});
runningPositions.forEach(position => {
if (position.side === 's') {
totalSellExposure += position.quantity;
sellPl += position.pl;
} else if (position.side === 'b') {
totalBuyExposure += position.quantity;
buyPl += position.pl;
}
});
}
logger('info', `Profitable sells: $${profitableSells} (exp $${totalSellExposure} pl ${sellPl} sats), profitable buys: $${profitableBuys} (exp $${totalBuyExposure} pl ${buyPl} sats)`);
// logger('info', `Sell exposure: $${totalSellExposure}, Buy exposure: $${totalBuyExposure}, Position: $${totalBuyExposure-totalSellExposure}`);
} catch (error) {
logger('error', `Fetching positions failed: ${JSON.stringify(error)}`);
logger(error.stack);
}
try {
let action = 'none';
let rsi = await fetchRSI('15m');
await sleep(15000);
let bbands = await fetchBbands('15m');
await sleep(15000);
// let ultosc = await fetchUltosc('15m');
// await sleep(15000);
let adjustedSellRsiThreshold = 75;
let adjustedBuyRsiThreshold = 45;
addRsiSample(rsi.value);
// Keep consuming RSI samples until we have `period` number of samples
// logger('finance', rsiData);
// logger('finance', `Ultosc: ${ultosc.value}`);
if (rsiData.length >= period) {
// Get moving average and print calculated thresholds
const movingAverageRSI = Number(calculateMovingAverage(rsiData, period));
logger('finance', `RSI_movAvg: ${movingAverageRSI}`);
logger('finance', `RSI_mBuy: ${movingAverageRSI-1}, RSI_mSell: ${movingAverageRSI+12}`);
adjustedSellRsiThreshold = movingAverageRSI+12;
adjustedBuyRsiThreshold = movingAverageRSI-1;
}
logger('finance', `RSI: ${rsi.value}, Bands: ${bbands.valueLowerBand} / ${bbands.valueUpperBand}`);
logger('finance', `Price ${lastPrice}, Index price ${indexPrice}`);
if (totalSellExposure > 19 || totalBuyExposure > 19) {
// logger('info', 'Exposure on one side is greater than $9, returning early.');
return;
}
let sellPriceThreshold = bbands.valueLowerBand * 1.03;
let buyPriceThreshold = bbands.valueUpperBand * 0.9961;
logger('info', `Sell thresh $${sellPriceThreshold}, Buy thresh $${buyPriceThreshold}`);
// Check for sell conditions
if (rsi.value >= adjustedSellRsiThreshold && lastPrice > sellPriceThreshold) {
action = 'sell';
logger('warn', `Condition met for selling: RSI is above ${adjustedSellRsiThreshold} and price is 3% higher than Bollinger Lower Band. Attempting to sell at ${lastPrice}.`);
await restClient.futuresNewTrade({
"side": "s",
"type": "m",
"leverage": 2,
"quantity": 2,
});
sendTelegramMessage(`Shorted on LNM at ${lastPrice}`);
} else if (rsi.value <= adjustedBuyRsiThreshold && lastPrice < buyPriceThreshold) {
action = 'buy';
logger('warn', `Condition met for buying: RSI is below ${adjustedBuyRsiThreshold} and price is 0.39% lower than Bollinger Higher Band. Attempting to buy at ${lastPrice}.`);
await restClient.futuresNewTrade({
"side": "b",
"type": "m",
"leverage": 2,
"quantity": 2,
});
sendTelegramMessage(`Longed on LNM at ${lastPrice}`);
}
// Update the timestamp of the last trade
lastTradeTime = Date.now();
if (action !== "none") {
logger('finance', `Trade action executed: ${action}`);
}
} catch (error) {
const errorLog = {
message: error.message, // The error message
stack: error.stack, // The stack trace
name: error.name, // The name of the error
// Include other properties as needed
};

// Log the error details
logger('error', `Trade execution failed: ${errorLog.message}`);
logger('error', errorLog.stack);

// If you need to log the whole error object, you can use a custom replacer function with JSON.stringify
const errorDetails = JSON.stringify(error, Object.getOwnPropertyNames(error));
logger('error', `Error details: ${errorDetails}`);
}
}

// WebSocket client for live data
async function setupWebSocket() {
try {
const wsClient = await createWebsocketClient(apiConfig);
await wsClient.publicSubscribe(['futures:btc_usd:last-price', 'futures:btc_usd:index']);
wsClient.ws.on('futures:btc_usd:last-price', (data) => {
// console.log(`Received data: ${JSON.stringify(data, null, 2)}`); // Log the entire data object
if (data?.lastPrice !== undefined) {
lastPrice = data.lastPrice;
// console.log(`Updated last price: ${lastPrice}`);
if (shouldCallTradeLogic()) {
tradeLogic();
lastTradeLogicCall = Date.now();
}
} else {
logger('error', 'Last price data is undefined in the received data');
}
});
wsClient.ws.on('futures:btc_usd:index', (data) => {
// console.log(`Received index data: ${JSON.stringify(data, null, 2)}`); // Log the entire data object
if (data?.index !== undefined) {
indexPrice = data.index;
if (shouldCallTradeLogic()) {
tradeLogic();
lastTradeLogicCall = Date.now();
}
} else {
logger('error', 'Index price data is undefined in the received data');
}
});
wsClient.ws.on('open', () => retryCount = 0);
wsClient.ws.on('close', () => {
logger('info', 'WebSocket closed');
if (retryCount < maxRetries) {
let delay = Math.min(1000 * (2 ** retryCount), 30000);
logger('info', 'Reconnecting websocket');
setTimeout(setupWebSocket, delay);
retryCount++;
} else {
logger('error', 'Max retries reached for reconnect');
}
});
wsClient.ws.on('error', (error) => console.error(`WebSocket error: ${error.message}`));
} catch (error) {
logger('error', `WebSocket setup failed: ${error.message}`);
}
}
// Initialize WebSocket connection
setupWebSocket();
// Implement additional features like signal handling for graceful shutdowns, logging, monitoring, etc.
}

loadModules().catch(console.error);
Loading

0 comments on commit 0f266ff

Please sign in to comment.