multi currency support better logging
This commit is contained in:
commit
bf08fd7cb4
12 changed files with 5938 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.env
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
268
index.js
Normal file
268
index.js
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
|
||||
const { ToadScheduler, SimpleIntervalJob, AsyncTask } = require('toad-scheduler');
|
||||
|
||||
const {
|
||||
settings,
|
||||
transferProfit,
|
||||
saveProfit,
|
||||
getAllClosedOrders,
|
||||
getAllOpenOrders,
|
||||
getOpenOrder,
|
||||
monitorOrderComplete,
|
||||
calculateAdaptiveTargets,
|
||||
sleep,
|
||||
placeSellOrder,
|
||||
placeBuyOrder,
|
||||
evaluateMarketConditions,
|
||||
getAvailableUSDBalance,
|
||||
getHistoricalData,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
settingsFile,
|
||||
getCurrencyInfo,
|
||||
openTradesFile,
|
||||
closedTradesFile,
|
||||
setTradingCurrency,
|
||||
setCurrencyInfo,
|
||||
manageStaleOrders,
|
||||
shuffle,
|
||||
syncSettings
|
||||
} = require("./utils")
|
||||
|
||||
async function monitorAndPlace(currencyData, usdAllocatedBalance, currency) {
|
||||
let CURRENCY_INFO = currencyData;
|
||||
// const settings = readJsonFile(settingsFile);
|
||||
// console.log(tradingCurrency + 'USD', CURRENCY_INFO)
|
||||
let openTrades = readJsonFile(openTradesFile);
|
||||
if (!settings) return {openTrades, removedIds: []};
|
||||
|
||||
let closedTrades = await getAllClosedOrders();
|
||||
// console.log("closedTrades", JSON.stringify(closedTrades, null, 4))
|
||||
// return
|
||||
const pair = currency.tradingCurrency + 'USD';
|
||||
let candleQuant = 16;
|
||||
const marketCondition = await evaluateMarketConditions(pair);
|
||||
const averageLow = (await getHistoricalData(pair, 15)).slice(-candleQuant).reduce((acc, candle) => acc + parseFloat(candle[3]), 0) / candleQuant;
|
||||
const availableUSD = usdAllocatedBalance;
|
||||
let removedIds = [];
|
||||
if(availableUSD <= 0){
|
||||
console.log("Insufficient / error reaching funds")
|
||||
return {openTrades, removedIds: []}
|
||||
}
|
||||
const balance = availableUSD * settings.maximumOrderSize;
|
||||
let oTrades = await getAllOpenOrders(false);
|
||||
for(let x = 0; x < Object.keys(oTrades).length; x++){
|
||||
let id = Object.keys(oTrades)[x];
|
||||
let sellid = null;
|
||||
let trade = oTrades[id];
|
||||
//make sure we are only working with orders for this pair
|
||||
if(!(trade.descr.pair === pair || trade.descr.pair === currency?.compPair)){
|
||||
continue;
|
||||
}
|
||||
if(trade.descr.type === 'sell'){
|
||||
//find original buying order
|
||||
sellid = id;
|
||||
console.log("id ", id);
|
||||
let buyingOrder = Object.entries(openTrades).find(itemArr => itemArr[1].sellid === id)
|
||||
if(!buyingOrder){
|
||||
console.log("Unable to find local trade")
|
||||
continue;
|
||||
}
|
||||
id = buyingOrder[1].id;
|
||||
console.log(buyingOrder)
|
||||
openTrades[id].sellid = sellid;
|
||||
}
|
||||
let localTrade = openTrades[id];
|
||||
if(!localTrade){
|
||||
openTrades[id] = {
|
||||
...openTrades[id],
|
||||
id: id,
|
||||
userref: trade.userref,
|
||||
pair: pair,
|
||||
starttimestamp: new Date().toISOString(),
|
||||
type: trade.descr.type,
|
||||
status: trade.status,
|
||||
ordertype: trade.descr.ordertype,
|
||||
volume: trade.vol,
|
||||
}
|
||||
console.log(typeof trade.descr.price)
|
||||
if(trade.descr.type === 'sell'){
|
||||
openTrades[id].sellprice = Number(trade.descr.price).toFixed(CURRENCY_INFO.pair_decimals);
|
||||
}else {
|
||||
openTrades[id].price = Number(trade.descr.price).toFixed(CURRENCY_INFO.pair_decimals);
|
||||
}
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
}
|
||||
}
|
||||
let allIds = Object.keys(openTrades);
|
||||
for(let x = 0; x < allIds.length; x++){
|
||||
let id = allIds[x];
|
||||
console.log(id);
|
||||
|
||||
let trade = oTrades[id];
|
||||
let localTrade = openTrades[id];
|
||||
let closedTrade = closedTrades[id];
|
||||
// console.log("trade", localTrade)
|
||||
if(!(localTrade.pair === pair || localTrade.pair === currency?.compPair)){
|
||||
continue;
|
||||
}
|
||||
|
||||
await calculateAdaptiveTargets(pair, settings.profitTargetPercent, settings.stopLossPercentage, riskRewardRatio = 10)
|
||||
if(localTrade.type === 'buy'){
|
||||
if(['pending', 'open', 'closed'].includes(closedTrade?.status) || ['pending', 'open', 'closed'].includes(localTrade.status)){
|
||||
if(closedTrade?.status === 'closed' || localTrade.status === 'closed'){
|
||||
console.log('Buy order completed:', localTrade);
|
||||
const sellPrice = parseFloat(localTrade.price) * (1 + settings.profitTargetPercent);
|
||||
const stopLoss = parseFloat(localTrade.price) * (1 - settings.stopLossPercentage);
|
||||
const sellOrder = await placeSellOrder(pair, localTrade.volume, sellPrice, stopLoss, localTrade.userref, localTrade.id, localTrade);
|
||||
if(sellOrder){
|
||||
openTrades[localTrade.id] = sellOrder;
|
||||
console.log('Sell order placed:', sellOrder);
|
||||
}
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
}
|
||||
}else {
|
||||
console.log("Buy order expired or cancelled, clearing from local list")
|
||||
//order expired or cancelled, whipe from list
|
||||
delete openTrades[id];
|
||||
removedIds.push(id)
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
}
|
||||
}else{
|
||||
localTrade = openTrades[id]; //originalBuyTrade
|
||||
trade = oTrades[localTrade.sellid]; //openselltrade
|
||||
closedTrade = closedTrades[localTrade.sellid]; //potentially closed sell trade
|
||||
if(['pending', 'open', 'closed'].includes(closedTrade?.status) || ['pending', 'open', 'closed'].includes(trade.status)){
|
||||
if(closedTrade?.status === 'closed' || trade.status === 'closed'){
|
||||
let sellOrder = {...openTrades[localTrade.id]};
|
||||
console.log('Sell order completed:', sellOrder);
|
||||
delete openTrades[sellOrder.id];
|
||||
removedIds.push(sellOrder.id)
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
const sellPrice = parseFloat(localTrade.sellprice)
|
||||
|
||||
const closedTrades = readJsonFile(closedTradesFile);
|
||||
const buyTotal = parseFloat(localTrade.price) * localTrade.volume;
|
||||
const sellTotal = sellPrice * localTrade.volume;
|
||||
const transactionFees = (settings.transactionFeePercent) * (buyTotal + sellTotal);
|
||||
const profit = sellTotal - buyTotal - transactionFees;
|
||||
console.log('Sell order Profit:', profit);
|
||||
|
||||
if (settings.profitWallet && settings.profitSavePercent) {
|
||||
const profitSaveAmount = profit * (settings.profitSavePercent);
|
||||
console.log('Saving Profit:', profitSaveAmount);
|
||||
await saveProfit(profitSaveAmount, profit);
|
||||
}
|
||||
|
||||
closedTrades[sellOrder.id] = {
|
||||
...sellOrder,
|
||||
sellPrice,
|
||||
volume: localTrade.volume,
|
||||
profit,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
writeJsonFile(closedTradesFile, closedTrades);
|
||||
//transfer profits and write to closedTrades.json
|
||||
|
||||
}
|
||||
}else {
|
||||
//order expired or cancelled, whipe from list
|
||||
console.log("Sell order expired or cancelled, clearing from local list")
|
||||
console.log("trade", trade)
|
||||
console.log("localTrade", localTrade)
|
||||
console.log("closedTrade", closedTrade)
|
||||
console.log("closedTrades", JSON.stringify(closedTrades, null, 4))
|
||||
|
||||
delete openTrades[id];
|
||||
removedIds.push(id)
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oTrades = await getAllOpenOrders(false);
|
||||
let openTradeIds = Object.keys(oTrades);
|
||||
let priceRangeExclusion = settings.priceRangeExclusion;
|
||||
let excludeBasedOnPrevOrder = [];
|
||||
for(let x = 0; x < openTradeIds.length; x++){
|
||||
|
||||
let id = openTradeIds[x];
|
||||
let trade = oTrades[id];
|
||||
if(!(trade.descr.pair === pair || trade.descr.pair === currency?.compPair)){
|
||||
continue;
|
||||
}
|
||||
let price = parseFloat(trade.descr.price);
|
||||
let diffPer = Math.abs(price - averageLow) / averageLow;
|
||||
// console.log(`DIff Percentage ${diffPer} ${price} ${averageLow}`)
|
||||
if(diffPer < priceRangeExclusion){
|
||||
excludeBasedOnPrevOrder.push(id);
|
||||
}
|
||||
if(excludeBasedOnPrevOrder.length >= settings.priceRangeExclusionMaxOpen){
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(excludeBasedOnPrevOrder.length >= settings.priceRangeExclusionMaxOpen){
|
||||
console.log('Excluding based on open orders: ', excludeBasedOnPrevOrder.join(", "))
|
||||
|
||||
return {openTrades, removedIds}
|
||||
}
|
||||
console.log(` Attempting to place buy order for ${balance} USD worth of ${currency.tradingCurrency} at ${averageLow} USD = ${balance / averageLow} ${currency.tradingCurrency}`);
|
||||
|
||||
|
||||
if (marketCondition) {
|
||||
const buyOrder = await placeBuyOrder(pair, balance / averageLow, averageLow);
|
||||
if (buyOrder) {
|
||||
console.log('Buy order placed:', buyOrder);
|
||||
openTrades[buyOrder.id] = {...buyOrder};
|
||||
// writeJsonFile(openTradesFile, openTrades);
|
||||
}
|
||||
}
|
||||
|
||||
return {openTrades, removedIds}
|
||||
}
|
||||
|
||||
async function cycleCurrencies(){
|
||||
let builtCurriencies = [];
|
||||
let settings = syncSettings();
|
||||
for(let x = 0; x < settings.tradingCurrencies.length; x++){
|
||||
let currency = settings.tradingCurrencies[x];
|
||||
let CURRENCY_INFO = await getCurrencyInfo(currency.tradingCurrency);
|
||||
const availableUSD = await getAvailableUSDBalance(currency.allocation)
|
||||
builtCurriencies.push({CURRENCY_INFO, availableUSD, currency});
|
||||
}
|
||||
shuffle(builtCurriencies);
|
||||
let trades = readJsonFile(openTradesFile);
|
||||
for(let x = 0; x < builtCurriencies.length; x++){
|
||||
console.log("*********************************************")
|
||||
console.log("*********************************************")
|
||||
console.log("***************TRADING***********************")
|
||||
console.log("*********************************************")
|
||||
console.log(`*****************${builtCurriencies[x].currency.tradingCurrency}*************************`)
|
||||
console.log("*********************************************")
|
||||
console.log("*********************************************")
|
||||
console.log("*********************************************")
|
||||
setTradingCurrency(builtCurriencies[x].currency.tradingCurrency);
|
||||
setCurrencyInfo(builtCurriencies[x].CURRENCY_INFO);
|
||||
|
||||
let {openTrades, removedIds} = await monitorAndPlace(builtCurriencies[x].CURRENCY_INFO, builtCurriencies[x].availableUSD, builtCurriencies[x].currency);
|
||||
trades = {...trades, ...openTrades};
|
||||
for(let i = 0; i < removedIds.length; i++){
|
||||
delete trades[removedIds[i]]
|
||||
}
|
||||
writeJsonFile(openTradesFile, trades);
|
||||
}
|
||||
await manageStaleOrders();
|
||||
}
|
||||
const scheduler = new ToadScheduler();
|
||||
|
||||
const task = new AsyncTask('monitor and place orders', cycleCurrencies, (err) => {
|
||||
console.error('Error in scheduled task:', err);
|
||||
});
|
||||
|
||||
cycleCurrencies();
|
||||
|
||||
const job = new SimpleIntervalJob({ [settings.tradingIntervalType]: settings.tradingInterval }, task);
|
||||
scheduler.addSimpleIntervalJob(job);
|
||||
|
||||
console.log('Trading bot is running.');
|
||||
|
||||
0
logs/log.txt
Normal file
0
logs/log.txt
Normal file
0
logs/output.txt
Normal file
0
logs/output.txt
Normal file
0
logs/output2.txt
Normal file
0
logs/output2.txt
Normal file
1264
package-lock.json
generated
Normal file
1264
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "simpletrader",
|
||||
"version": "1.0.0",
|
||||
"description": "A trading bot for Nano cryptocurrency on Kraken Pro.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"ffish-es6": "^0.7.7",
|
||||
"node-kraken-api": "^2.2.2",
|
||||
"toad-scheduler": "^1.0.0",
|
||||
"ts-kraken": "^4.0.4",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^8.0.0"
|
||||
},
|
||||
"author": "Me",
|
||||
"license": "ISC"
|
||||
}
|
||||
2255
src/LocalStore/ClosedTrades.json
Normal file
2255
src/LocalStore/ClosedTrades.json
Normal file
File diff suppressed because it is too large
Load diff
105
src/LocalStore/OpenTrades.json
Normal file
105
src/LocalStore/OpenTrades.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"OUD5CV-CD7M5-PC42DT": {
|
||||
"id": "OUD5CV-CD7M5-PC42DT",
|
||||
"userref": 1746939220,
|
||||
"pair": "NANOUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "1.045188",
|
||||
"starttimestamp": "2025-05-11T04:53:41.774Z",
|
||||
"volume": 4.78382722,
|
||||
"sellid": "OJFWEU-72CFF-5KPE4D",
|
||||
"sellprice": "1.060866",
|
||||
"stopprice": 0.8884098,
|
||||
"startselltimestamp": "2025-05-11T14:13:52.788Z"
|
||||
},
|
||||
"ORP4HY-R5NKT-2F66F5": {
|
||||
"id": "ORP4HY-R5NKT-2F66F5",
|
||||
"userref": 1746939223,
|
||||
"pair": "GIGAUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "0.028026",
|
||||
"starttimestamp": "2025-05-11T04:53:44.554Z",
|
||||
"volume": 178.40895,
|
||||
"sellid": "OFMP67-Z236I-DZOGIP",
|
||||
"sellprice": "0.028446",
|
||||
"stopprice": 0.0238221,
|
||||
"startselltimestamp": "2025-05-11T05:24:25.528Z"
|
||||
},
|
||||
"OUA6VO-RB6IM-QMMXMW": {
|
||||
"id": "OUA6VO-RB6IM-QMMXMW",
|
||||
"userref": 1746939226,
|
||||
"status": "open",
|
||||
"pair": "XDGUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "0.2401872",
|
||||
"starttimestamp": "2025-05-11T04:53:47.473Z",
|
||||
"volume": 20.81709816,
|
||||
"sellid": "OVSIP6-4Z53V-RVMM6G",
|
||||
"sellprice": "0.2437900",
|
||||
"stopprice": 0.20415912,
|
||||
"startselltimestamp": "2025-05-11T14:35:21.046Z"
|
||||
},
|
||||
"ORXKJ7-CJEAR-RF7KF5": {
|
||||
"id": "ORXKJ7-CJEAR-RF7KF5",
|
||||
"userref": 1746939230,
|
||||
"status": "open",
|
||||
"pair": "ATOMUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "5.2943",
|
||||
"starttimestamp": "2025-05-11T04:53:51.632Z",
|
||||
"volume": 0.94441749,
|
||||
"sellid": "O24ZUM-MUVP6-BIYWNN",
|
||||
"sellprice": "5.3737",
|
||||
"stopprice": 4.5001549999999995,
|
||||
"startselltimestamp": "2025-05-11T05:44:25.854Z"
|
||||
},
|
||||
"OEJIUM-RU467-6QFNIV": {
|
||||
"id": "OEJIUM-RU467-6QFNIV",
|
||||
"userref": 1746939520,
|
||||
"status": "open",
|
||||
"pair": "ATOMUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "5.2943",
|
||||
"starttimestamp": "2025-05-11T04:58:42.133Z",
|
||||
"volume": 0.76822504,
|
||||
"sellid": "O3ZB54-272G7-J6EFN5",
|
||||
"sellprice": "5.3737",
|
||||
"stopprice": 4.5001549999999995,
|
||||
"startselltimestamp": "2025-05-11T05:44:26.651Z"
|
||||
},
|
||||
"O4KLLD-RSN7V-UYM763": {
|
||||
"id": "O4KLLD-RSN7V-UYM763",
|
||||
"userref": 1746939525,
|
||||
"status": "open",
|
||||
"pair": "NANOUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "1.045188",
|
||||
"starttimestamp": "2025-05-11T04:58:46.149Z",
|
||||
"volume": 3.89134667,
|
||||
"sellid": "OUYZEZ-VCY47-FAVQEM",
|
||||
"sellprice": "1.060866",
|
||||
"stopprice": 0.8884098,
|
||||
"startselltimestamp": "2025-05-11T14:13:53.615Z"
|
||||
},
|
||||
"OCBGVS-V4FWI-ZH7SP5": {
|
||||
"id": "OCBGVS-V4FWI-ZH7SP5",
|
||||
"userref": 1746939527,
|
||||
"pair": "GIGAUSD",
|
||||
"type": "sell",
|
||||
"ordertype": "limit",
|
||||
"price": "0.028026",
|
||||
"starttimestamp": "2025-05-11T04:58:48.858Z",
|
||||
"volume": 145.12461,
|
||||
"sellid": "O3GAWS-OITFS-PMSZZR",
|
||||
"status": "open",
|
||||
"sellprice": "0.028446",
|
||||
"stopprice": 0.0238221,
|
||||
"startselltimestamp": "2025-05-11T05:24:26.284Z"
|
||||
}
|
||||
}
|
||||
38
src/LocalStore/Settings.json
Normal file
38
src/LocalStore/Settings.json
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"stopLossPercentage": 0.15,
|
||||
"profitSavePercent": 0.075,
|
||||
"maximumOrderSize": 0.75,
|
||||
"tradingInterval": 5,
|
||||
"tradingIntervalType": "minutes",
|
||||
"maximumOrderValUSD": 5,
|
||||
"priceRangeExclusion": 0.015,
|
||||
"priceRangeExclusionMaxOpen": 3,
|
||||
"profitTargetPercent": 0.015,
|
||||
"profitAggUSD": 0.4985724690877609,
|
||||
"totalProfit": 6.647632921170934,
|
||||
"transactionFeePercent": 0.004,
|
||||
"tradingCurrencies": [
|
||||
{
|
||||
"tradingCurrency": "GIGA",
|
||||
"allocation": 0.2
|
||||
},
|
||||
{
|
||||
"tradingCurrency": "ATOM",
|
||||
"allocation": 0.2
|
||||
},
|
||||
{
|
||||
"tradingCurrency": "XXMRZ",
|
||||
"allocation": 0.2,
|
||||
"compPair": "XMRUSD"
|
||||
},
|
||||
{
|
||||
"tradingCurrency": "XDG",
|
||||
"allocation": 0.2
|
||||
},
|
||||
{
|
||||
"tradingCurrency": "NANO",
|
||||
"allocation": 0.2
|
||||
}
|
||||
],
|
||||
"profitWallet": "nano_1cjnndcda5w9c3sx6jo8kgdkn86i7dsi1nd3tr1x3scdmxutu8i7eodk5b83"
|
||||
}
|
||||
1452
testdata.json
Normal file
1452
testdata.json
Normal file
File diff suppressed because it is too large
Load diff
530
utils.js
Normal file
530
utils.js
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue