530 lines
No EOL
19 KiB
JavaScript
530 lines
No EOL
19 KiB
JavaScript
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
|
|
} |