const fs = require('fs'); const { Kraken } = require('node-kraken-api'); const dotenv = require('dotenv'); dotenv.config(); const settingsFile = './src/LocalStore/Settings.json'; let settings = readJsonFile(settingsFile); const API_KEY = process.env.KRAKEN_API_KEY; const API_SECRET = process.env.KRAKEN_API_SECRET; const kraken = new Kraken({ key: API_KEY, secret: API_SECRET }); let CURRENCY_INFO = null; let tradingCurrency = null; //XXMRZUSD const openTradesFile = './src/LocalStore/OpenTrades.json'; const closedTradesFile = './src/LocalStore/ClosedTrades.json'; function readJsonFile(filePath) { try { const data = fs.readFileSync(filePath); return JSON.parse(data); } catch (error) { console.error(`Error reading file ${filePath}:`, error); return null; } } function writeJsonFile(filePath, data) { try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); } catch (error) { console.error(`Error writing file ${filePath}:`, error); } } async function getHistoricalData(pair, interval = 15) { try { const response = await kraken.ohlc( { pair: pair, interval: interval }); // console.log(response[pair]) return response[pair]; } catch (error) { console.error('Error fetching historical data:', error); return null; } } async function getAvailableUSDBalance(allocation) { try { const settings = readJsonFile(settingsFile); const response = await kraken.balance( ); let openTrades = await getAllOpenOrders(false); let floatingTrades = 0; let tradeIds = Object.keys(openTrades); for(let x = 0; x < tradeIds.length; x++){ let key = tradeIds[x]; let trade = openTrades[key]; if(trade.descr.type !== 'buy') continue let total = parseFloat(trade.vol) * parseFloat(trade.descr.price) floatingTrades += total; } console.log(`USD BALANCE: ${parseFloat(response.ZUSD)}`) console.log(`USD FLOATING: ${floatingTrades}`) console.log(`USD BALANCE AFTER TRADES: ${parseFloat(response.ZUSD) - floatingTrades}`) return Math.max(parseFloat(response.ZUSD) - floatingTrades - settings.profitAggUSD, 0) * allocation; } catch (error) { console.error('Error fetching available USD balance:', error); return 0; } } async function evaluateMarketConditions(pair) { console.log("evaluateMarketConditions") return true const data = await getHistoricalData(pair); if (!data) return false; const lastTwoDays = data.slice(-2); // console.log(lastTwoDays) const priceChange = (lastTwoDays[1][4] - lastTwoDays[0][1]) / lastTwoDays[0][1] * 100; console.log((lastTwoDays[1][4] - lastTwoDays[0][4]), lastTwoDays[0][4], priceChange) if (Math.abs(priceChange) >= 3.5) { console.log('Volatility detected. Trading halted.'); return false; } return true; } function syncSettings(){ settings = readJsonFile(settingsFile); return settings; } async function getCurrencyInfo(cur){ settings = readJsonFile(settingsFile); console.log("Setting Currency", cur) tradingCurrency = cur; const pairData = await kraken.assetPairs( { assets: tradingCurrency + "USD", }); // console.log("pairData", pairData[tradingCurrency + "USD"]); CURRENCY_INFO = pairData[tradingCurrency + "USD"]; CURRENCY_INFO.costmin = parseFloat(CURRENCY_INFO.costmin); CURRENCY_INFO.ordermin = parseFloat( CURRENCY_INFO.ordermin); return CURRENCY_INFO; } async function placeBuyOrder(pair, v, p) { let volume = v; let price = p; if(volume * price >= settings.maximumOrderValUSD){ volume = settings.maximumOrderValUSD / price; } try { let userref = Math.floor(Date.now() / 1000); // console.log("CURRENCY_INFO", CURRENCY_INFO); if(!CURRENCY_INFO){ const pairData = await kraken.assetPairs( { assets: tradingCurrency + "USD", }); // console.log("pairData", pairData[tradingCurrency + "USD"]); CURRENCY_INFO = pairData[tradingCurrency + "USD"]; CURRENCY_INFO.costmin = parseFloat(CURRENCY_INFO.costmin); CURRENCY_INFO.ordermin = parseFloat( CURRENCY_INFO.ordermin); } if(CURRENCY_INFO.ordermin * 1.02 > volume){ console.log("OrderPlacement is below minimum Volume required ", `${CURRENCY_INFO.ordermin * 1.02} > ${volume}`) return null; } let usdBalance = await getAvailableUSDBalance(); console.log("CURRENCY_INFO", CURRENCY_INFO, ) console.log("usdBalance", usdBalance, ) console.log({ pair: pair, type: 'buy', ordertype: 'limit', price: price.toFixed(CURRENCY_INFO.pair_decimals), volume: CURRENCY_INFO.ordermin > volume ? CURRENCY_INFO.ordermin * 1.02 : volume, userref: userref, }) const response = await kraken.addOrder( { pair: pair, type: 'buy', ordertype: 'limit', price: price.toFixed(CURRENCY_INFO.pair_decimals), volume: CURRENCY_INFO.ordermin > volume ? CURRENCY_INFO.ordermin * 1.02 : volume, userref: userref, }); let order = await getOpenOrder(response.txid[0]) console.log("placeBuyOrder", JSON.stringify(response, null ,4)); return { id: response.txid[0], userref: userref, status: order.status, pair: pair, type: 'buy', ordertype: 'limit', price: price.toFixed(CURRENCY_INFO.pair_decimals), starttimestamp: new Date().toISOString(), volume: CURRENCY_INFO.ordermin > volume ? CURRENCY_INFO.ordermin * 1.02 : volume, }; } catch (error) { console.error('Error placing buy order:', error); return null; } } function convertAssetKeys(tradingSymbol){ const assetMap = { 'XXMRZ': 'XXMR', 'XXMR': 'XXMRZ', 'XDG': 'XXDG', 'XXDG': 'XDG', }; return assetMap[tradingSymbol] || tradingSymbol; } async function placeSellOrder(pair, volume, price, stopLoss, userRef, id, buyOrder) { try { if(!CURRENCY_INFO){ const pairData = await kraken.assetPairs({ assets: tradingCurrency + "USD", }); CURRENCY_INFO = pairData[tradingCurrency + "USD"]; CURRENCY_INFO.costmin = parseFloat(CURRENCY_INFO.costmin); CURRENCY_INFO.ordermin = parseFloat(CURRENCY_INFO.ordermin); } // Get the actual available balance const balanceResponse = await kraken.balance(); // const assetKey = tradingCurrency === 'XXMRZ' ? 'XXMR' : tradingCurrency; const assetKey = convertAssetKeys(tradingCurrency); const availableBalance = parseFloat(balanceResponse[assetKey] || 0); // if(availableBalance < 1.0){ // console.log(JSON.stringify(balanceResponse, null, 4)) // console.log(assetKey, balanceResponse[assetKey]) // console.log(tradingCurrency) // } // Use the smaller of requested volume or available balance // console.log("Sell Volume picker", volume, availableBalance) const adjustedVolume = Math.min(volume, availableBalance); // Round to appropriate decimal places based on lot_decimals const roundedVolume = parseFloat(adjustedVolume.toFixed(CURRENCY_INFO.lot_decimals)); if(CURRENCY_INFO.ordermin * 1.02 > roundedVolume){ console.log("SellOrderPlacement is below minimum Volume required ", `${CURRENCY_INFO.ordermin * 1.02} > ${roundedVolume}`) return null; } // console.log(`Available balance: ${availableBalance} ${tradingCurrency}`); // console.log(`Adjusted volume: ${roundedVolume} ${tradingCurrency}`); const response = await kraken.addOrder({ pair: pair, type: 'sell', ordertype: 'limit', price: price.toFixed(CURRENCY_INFO.pair_decimals), volume: roundedVolume, userref: userRef, }); let order = await getOpenOrder(response.txid[0]) console.log("placeSellOrder", response); return { ...buyOrder, id: id, sellid: response.txid[0], status: order.status, pair: pair, type: 'sell', ordertype: 'limit', sellprice: price.toFixed(CURRENCY_INFO.pair_decimals), volume: roundedVolume, userref: userRef, stopprice: stopLoss, startselltimestamp: new Date().toISOString(), }; } catch (error) { console.error('Error placing sell order:', error); return null; } } // need to randomize order of executed currencies to avoid all balances // shifting to first currency in list over time as sells occur // the resulting income is added to the general pool to be split across all currencies // randomizing order should mean all currencies should keep their relative allocation // regardless of profitability of that trading currency function shuffle(array) { let currentIndex = array.length; while (currentIndex != 0) { let randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex]]; } } function sleep(ms) { console.log("Waiting " + ms) return new Promise(resolve => setTimeout(resolve, ms)); } async function monitorOrderComplete(id) { try { let response = await kraken.openOrders( ); console.log("monitorOrderComplete", JSON.stringify(response, null, 4)) while(response.open.hasOwnProperty(id)){ await sleep(60000); response = await kraken.openOrders( ); } return true; } catch (error) { console.error('Error monitoring order:', id, error); return false; } } async function getOpenOrder(id) { try { let response = await kraken.openOrders( ); // console.log("getAllOrders", JSON.stringify(response, null, 4)) return response?.open[id] || {}; } catch (error) { console.error('Error getOpenOrder order:', id, error); return {}; } } async function getAllOpenOrders(log = false) { try { let response = await kraken.openOrders( ); if(log) console.log("getAllOrders", JSON.stringify(response, null, 4)) return response?.open || {}; } catch (error) { console.error('Error getAllOpenOrders order:', error); return {}; } } async function getAllClosedOrders() { try { let response = await kraken.closedOrders( ); // console.log("getAllClosedOrders", JSON.stringify(response, null, 4)) return response?.closed || {}; } catch (error) { console.error('Error getAllClosedOrders order:', error); return {}; } } async function saveProfit(profitSaveAmount, tradeProfit) { // not actually transferring to a different wallet just keeping track of profit which will be removed from usd balance so its not used for orders effectivly saving it try { const settings = readJsonFile(settingsFile); let reduction = settings.profitAggUSD + profitSaveAmount; settings.profitAggUSD = reduction; settings.totalProfit = settings.totalProfit + tradeProfit; writeJsonFile(settingsFile, settings) console.log(`Saved ${profitSaveAmount} USD to profit Segment, current Total Profit: ${reduction}`); return reduction; } catch (error) { console.error('Error transferring profit:', error); } } async function transferProfit(profitAmount) { try { if(tradingCurrency !== "NANO"){ return false; } const settings = readJsonFile(settingsFile); let reduction = settings.profitAggUSD - profitAmount; if(reduction < 0) reduction = 0; settings.profitAggUSD = reduction; const response = await kraken.withdraw({ asset: 'USD', key: settings.profitWallet, amount: profitAmount, }); console.log(`Transferred: ${profitAmount} USD to profit wallet: ${settings.profitWallet}, current local Profit Save: ${settings.profitAggUSD}`); return response; } catch (error) { console.error('Error transferring profit:', error); } } async function calculateAdaptiveTargets(pair, baseTarget = 0.015, baseStopLoss = 0.15, riskRewardRatio = 10) { try { // Get recent price data (1-hour candles for the past 24 hours) const recentCandles = await getHistoricalData(pair, 60); if (!recentCandles || recentCandles.length < 24) { console.log(`Insufficient historical data for ${pair}, using base values`); return { profitTarget: baseTarget, stopLoss: baseStopLoss }; } const last24Hours = recentCandles.slice(-24); // volatility using ATR method let atr = 0; for (let i = 1; i < last24Hours.length; i++) { const high = parseFloat(last24Hours[i][2]); const low = parseFloat(last24Hours[i][3]); const prevClose = parseFloat(last24Hours[i-1][4]); const tr = Math.max( high - low, Math.abs(high - prevClose), Math.abs(low - prevClose) ); atr += tr; } atr /= (last24Hours.length - 1); // market direction (positive or negative) const priceStart = parseFloat(last24Hours[0][1]); // Open of first candle const priceEnd = parseFloat(last24Hours[last24Hours.length - 1][4]); // Close of last candle const priceChange = (priceEnd - priceStart) / priceStart; const isUptrend = priceChange >= 0; // Get current price const currentPrice = priceEnd; const volatilityPercent = atr / currentPrice; // Calculate baseline volatility const baselineVolatility = 0.02; // 2% as baseline const volatilityFactor = volatilityPercent / baselineVolatility; // Asymmetric adjustments based on market direction let profitTargetAdjustment, stopLossAdjustment; if (isUptrend) { // Make sure high volatility increases profit target profitTargetAdjustment = Math.min(2.0, 0.8 + (volatilityFactor * 0.4)); // Keep stop loss tighter in uptrend stopLossAdjustment = Math.min(1.2, Math.max(0.7, volatilityFactor * 0.6)); console.log(`Uptrend detected (${(priceChange * 100).toFixed(2)}%), expanding profit target`); } else { // REDUCE profit target in downtrend, regardless of volatility profitTargetAdjustment = Math.min(0.9, Math.max(0.5, 1.0 - (volatilityFactor * 0.2))); // Wider stop loss in downtrend to avoid whipsaws stopLossAdjustment = Math.min(1.5, Math.max(1.0, volatilityFactor * 0.8)); console.log(`Downtrend detected (${(priceChange * 100).toFixed(2)}%), reducing profit target`); } const adjustedTarget = baseTarget * profitTargetAdjustment; const adjustedStopLoss = baseStopLoss * stopLossAdjustment; const recentCandles4h = last24Hours.slice(-4); const recent4hChange = (parseFloat(recentCandles4h[recentCandles4h.length-1][4]) - parseFloat(recentCandles4h[0][1])) / parseFloat(recentCandles4h[0][1]); // Further adjust based on recent momentum let finalProfitTarget = adjustedTarget; let finalStopLoss = adjustedStopLoss; if (recent4hChange > 0.01) { // Strong recent upward momentum (>1%) finalProfitTarget *= 1.1; // Increase profit target by 10% console.log(`Strong recent upward momentum (${(recent4hChange * 100).toFixed(2)}%), further increasing profit target`); } else if (recent4hChange < -0.01) { // Strong recent downward momentum (>1%) finalStopLoss *= 0.9; // Tighten stop loss by 10% console.log(`Strong recent downward momentum (${(recent4hChange * 100).toFixed(2)}%), tightening stop loss`); } // Calculate actual risk/reward ratio const riskRewardRatio = finalProfitTarget / finalStopLoss; console.log(` Adaptive Analysis for ${pair}: - Overall Direction: ${isUptrend ? 'Uptrend' : 'Downtrend'} (${(priceChange * 100).toFixed(2)}%) - Recent 4h Momentum: ${(recent4hChange * 100).toFixed(2)}% - Volatility: ${(volatilityPercent * 100).toFixed(2)}% - Volatility Factor: ${volatilityFactor.toFixed(2)} - Profit Target: ${(finalProfitTarget * 100).toFixed(2)}% (from base ${(baseTarget * 100).toFixed(2)}%) - Stop Loss: ${(finalStopLoss * 100).toFixed(2)}% (from base ${(baseStopLoss * 100).toFixed(2)}%) - Risk/Reward Ratio: ${riskRewardRatio.toFixed(2)} `); return { profitTarget: finalProfitTarget, stopLoss: finalStopLoss, volatility: volatilityPercent, direction: isUptrend ? 'up' : 'down', riskRewardRatio: riskRewardRatio, recentMomentum: recent4hChange }; } catch (error) { console.error(`Error calculating adaptive targets for ${pair}:`, error); // Fall back to base values if there's an error return { profitTarget: baseTarget, stopLoss: baseStopLoss }; } } async function manageStaleOrders() { const openTrades = readJsonFile(openTradesFile); const currentTime = new Date().getTime(); const maxOrderAge = 12 * 60 * 60 * 1000; // 12 hours in milliseconds for (const [id, trade] of Object.entries(openTrades)) { if (trade.type === 'buy' && new Date(trade.starttimestamp).getTime() + maxOrderAge < currentTime) { // Cancel stale buy orders try { await kraken.cancelOrder({ txid: id }); delete openTrades[id]; console.log(`Canceled stale buy order: ${id}`); } catch (error) { console.error(`Failed to cancel order ${id}:`, error); } } } writeJsonFile(openTradesFile, openTrades); } function setTradingCurrency(cur){ tradingCurrency = cur; } function setCurrencyInfo(cur){ CURRENCY_INFO = cur; } module.exports = { settings, transferProfit, saveProfit, manageStaleOrders, calculateAdaptiveTargets, getAllClosedOrders, getAllOpenOrders, getOpenOrder, monitorOrderComplete, sleep, placeSellOrder, placeBuyOrder, evaluateMarketConditions, getAvailableUSDBalance, getHistoricalData, readJsonFile, writeJsonFile, settingsFile, CURRENCY_INFO, getCurrencyInfo, openTradesFile, closedTradesFile, tradingCurrency, setTradingCurrency, setCurrencyInfo, shuffle, syncSettings }