diff --git a/src/js/apis/binance/BinanceApiClient.js b/src/js/apis/binance/BinanceApiClient.js index 8362b95..e624c9b 100644 --- a/src/js/apis/binance/BinanceApiClient.js +++ b/src/js/apis/binance/BinanceApiClient.js @@ -401,7 +401,7 @@ export default class BinanceApiClient extends APIClient { "symbol": symbol, "orderId": orderId, }).then(resp => { - resp.forEach(trade => event.fills.push(new TransactionFill(parseFloat(trade.price), parseFloat(trade.qty)))); + resp.forEach(trade => event.fills.push(new TransactionFill(parseFloat(trade.price), parseFloat(trade.qty), parseFloat(trade.commission), trade.commissionAsset))); this.dispatchAccountEvent(event); }).catch(e => { this.getLogger().error("Unable to get order fills for " + clientId + "(" + orderId + ")", {code: e.code, msg: e.message, url: e.requestUrl}); diff --git a/src/js/apiwrapper/assets/TraidingPairs.js b/src/js/apiwrapper/assets/TraidingPairs.js index e33159c..2ca5a69 100644 --- a/src/js/apiwrapper/assets/TraidingPairs.js +++ b/src/js/apiwrapper/assets/TraidingPairs.js @@ -9,6 +9,9 @@ export default class TradingPairs { static ETHBTC = new TradingPair(Assets.ETH, Assets.BTC); static ALGOUSDT = new TradingPair(Assets.ALGO, Assets.USDT); static TRXUSDT = new TradingPair(Assets.TRX, Assets.USDT); + static ADAUSDT = new TradingPair(Assets.ADA, Assets.USDT); + static XRPUSDT = new TradingPair(Assets.XRP, Assets.USDT); + static DOGEUSDT = new TradingPair(Assets.DOGE, Assets.USDT); static TRADING_PAIRS = [ this.BTCUSDT, @@ -17,6 +20,9 @@ export default class TradingPairs { this.ETHBTC, this.ALGOUSDT, this.TRXUSDT, + this.ADAUSDT, + this.XRPUSDT, + this.DOGEUSDT, ]; /** diff --git a/src/js/apiwrapper/transaction/TransactionFill.js b/src/js/apiwrapper/transaction/TransactionFill.js index 2f5a316..f2cd6ba 100644 --- a/src/js/apiwrapper/transaction/TransactionFill.js +++ b/src/js/apiwrapper/transaction/TransactionFill.js @@ -9,8 +9,20 @@ export default class TransactionFill { */ quantity = 0; - constructor(price, quantity){ + /** + * @type {number} + */ + commission = 0; + + /** + * @type {string} + */ + commissionAsset = null; + + constructor(price, quantity, commission, commissionAsset){ this.price = price; this.quantity = quantity; + this.commission = commission; + this.commissionAsset = commissionAsset; } } \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index c841bf7..babd5e9 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -21,22 +21,30 @@ import PeakDetector from "./tradingbot/data/asset/PeakDetector.js"; import OrderBookEntry from "./apiwrapper/orderbook/OrderBookEntry.js"; import TestStrategy from "./tradingbot/strategy/TestStrategy.js"; import AvgMinMaxStrategy from "./tradingbot/strategy/AvgMinMaxStrategy.js"; +import AvgEmaStrategy from "./tradingbot/strategy/AvgEmaStrategy.js"; +import SingleTransactionTestWorker from "./tradingbot/worker/SingleTransactionTestWorker.js"; try{ - const tradingBot = new TradingBot("C:/Users/Wlad/Projekte/BinanceBot/binance_bot/data/conf", "config-test.sjson"); - const galaWorker = new DefaultWorker("gala_minmax", TradingPairs.GALAUSDT, new AvgMinMaxStrategy()); + const tradingBot = new TradingBot("C:/Users/Wlad/Projekte/BinanceBot/binance_bot/data/conf", "config-main.sjson"); + const galaWorker = new DefaultWorker("gala_avgema", TradingPairs.GALAUSDT, new AvgEmaStrategy()); const statWorker = new StatisticWorker("gala_stats", TradingPairs.GALAUSDT, new TestStrategy()); - const trxWorker = new DefaultWorker("trx_minmax", TradingPairs.TRXUSDT, new AvgMinMaxStrategy()); - const trxStatWorker = new StatisticWorker("trx_stats", TradingPairs.TRXUSDT, new TestStrategy()); - const algoWorker = new DefaultWorker("algo_minmax", TradingPairs.ALGOUSDT, new AvgMinMaxStrategy()); - const algoStatWorker = new StatisticWorker("algo_stats", TradingPairs.ALGOUSDT, new TestStrategy()); + + const algoWorker = new DefaultWorker("algo_avgema", TradingPairs.ALGOUSDT, new AvgEmaStrategy()); + const dogeWorker = new DefaultWorker("doge_avgema", TradingPairs.DOGEUSDT, new AvgEmaStrategy()); + const xrpWorker = new DefaultWorker("xrp_avgema", TradingPairs.XRPUSDT, new AvgEmaStrategy()); + const adaWorker = new DefaultWorker("ada_avgema", TradingPairs.ADAUSDT, new AvgEmaStrategy()); + + //const testWorker = new SingleTransactionTestWorker("gala_test", TradingPairs.GALAUSDT, new AvgEmaStrategy()); + //tradingBot.registerWorker(testWorker); + //tradingBot.registerWorker(galaWorker); //tradingBot.registerWorker(statWorker); - tradingBot.registerWorker(trxWorker); - tradingBot.registerWorker(trxStatWorker); - tradingBot.registerWorker(algoWorker); - tradingBot.registerWorker(algoStatWorker); + //tradingBot.registerWorker(dogeWorker); + //tradingBot.registerWorker(xrpWorker); + //tradingBot.registerWorker(algoWorker); + //tradingBot.registerWorker(adaWorker); + tradingBot.start(); } catch (e){ console.log(e); @@ -123,14 +131,14 @@ setTimeout(() => {api.requestBuy(transaction);}, 5000);*/ // ############################ /*const path = 'C:/Users/Wlad/Projekte/BinanceBot/binance_bot/data/conf'; const conf = new BotConfig(); -conf.setApiConfig(BinanceApiConfig.createTestNetConfig()); +conf.setApiConfig(BinanceApiConfig.createMainNetConfig()); conf.setWorkingDirectory('C:/Users/Wlad/Projekte/BinanceBot/binance_bot/data'); conf.setRelativeDataPath('data'); conf.setRelativeLogPath('logs'); conf.setRelativeTmpPath('tmp'); conf.setUsdTradeLimit(500); -conf.setPerTradeLimit(30); -SerializableHelper.save(path, "config-test.sjson", conf);*/ +conf.setPerTradeLimit(50); +SerializableHelper.save(path, "config-main.sjson", conf);*/ // ############################ // ## SERIALIZER TESTS ## diff --git a/src/js/logging/Logger.js b/src/js/logging/Logger.js index b9c9318..4b5b2db 100644 --- a/src/js/logging/Logger.js +++ b/src/js/logging/Logger.js @@ -1,24 +1,26 @@ import winston from "winston"; import 'winston-daily-rotate-file'; +import SingleLevelRotateTransport from "./SingleLevelRotateTransport.js"; export default class Logger { #internLogger = null; - /* + #levels = { error: 0, warn: 1, info: 2, - http: 3, - verbose: 4, - debug: 5, - silly: 6 - */ + verbose: 3, + debug: 4, + silly: 5, + event: 101 + } /** * @param {string} logDir */ constructor(logDir) { this.#internLogger = winston.createLogger({ + levels: this.#levels, level: 'debug', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), @@ -26,7 +28,8 @@ export default class Logger { ), transports: [ this.#getCombinedLogTransport(logDir), - this.#getErrorLogTransport(logDir) + this.#getErrorLogTransport(logDir), + this.#getEventLogTransport(logDir) ], }); } @@ -48,7 +51,8 @@ export default class Logger { datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '10m', - maxFiles: '7d' + maxFiles: '7d', + level: 'silly' }); } @@ -68,6 +72,22 @@ export default class Logger { }); } + /** + * @param {string} logPath + * @returns {winston.transport} + */ + #getEventLogTransport(logPath){ + return new SingleLevelRotateTransport({ + filename: 'event-%DATE%.log', + dirname: logPath, + datePattern: 'YYYY-MM-DD', + zippedArchive: false, + maxSize: '20m', + maxFiles: '7d', + level: 'event' + }); + } + /** * Logs to file * @param {...any} obj objects to log @@ -112,8 +132,8 @@ export default class Logger { * Logs to separate file * @param {...any} obj objects to log */ - remark(...obj) { - this.#writeLog('verbose', obj); + event(...obj) { + this.#writeLog('event', obj); } #writeLog(level, params) { diff --git a/src/js/logging/SingleLevelRotateTransport.js b/src/js/logging/SingleLevelRotateTransport.js new file mode 100644 index 0000000..28822fb --- /dev/null +++ b/src/js/logging/SingleLevelRotateTransport.js @@ -0,0 +1,20 @@ +import winston from "winston"; +import 'winston-daily-rotate-file'; + +export default class SingleLevelRotateTransport extends winston.transports.DailyRotateFile{ + /** + * + * @param {import("winston-daily-rotate-file").DailyRotateFileTransportOptions} options + */ + constructor(options = null){ + super(options) + } + + log(info, callback){ + if(info.level.toLowerCase() == this.level.toLowerCase()){ + super.log(info, callback); + } else { + callback(null, false); + } + } +} \ No newline at end of file diff --git a/src/js/tradingbot/TradingBot.js b/src/js/tradingbot/TradingBot.js index bb8f266..894ccf7 100644 --- a/src/js/tradingbot/TradingBot.js +++ b/src/js/tradingbot/TradingBot.js @@ -184,6 +184,20 @@ export default class TradingBot { return this.#transactionHistory.filter(t => t.symbol.getKey().toLowerCase() == tradingPair.getKey().toLowerCase()); } + /** + * @returns {Transaction[]} + */ + getTransactions(){ + return this.#transactions; + } + + /** + * @returns {Transaction[]} + */ + getClosedTransactions(){ + return this.#transactionHistory; + } + /** * @returns {AssetDataCollector} */ @@ -268,7 +282,7 @@ export default class TradingBot { this.#worker.forEach(worker => { if (worker.getTradingPair().getKey() === assetUpdateEvent.tradingPair.getKey()) { - worker.onUpdate(this.#dataCollector.getAssetData(worker.getTradingPair()), this.#transactions.filter(t => t.initiator = worker.getId()), this.#config.getMaxTransactionsPerWorker()); + worker.onUpdate(this.#dataCollector.getAssetData(worker.getTradingPair()), this.#transactions.filter(t => t.initiator == worker.getId()), this.#config.getMaxTransactionsPerWorker()); } }); } @@ -301,10 +315,10 @@ export default class TradingBot { const nextTransactionPhase = TransactionPhase.getPhaseForStatus(transaction.phase, event.status, event.fills.length > 0); if (transaction.phase !== nextTransactionPhase) { if (TransactionPhase.BUY_DONE === nextTransactionPhase) { - this.#calculateTradeValues(transaction.buySettings, event.fills); + this.#calculateTradeValues(transaction.symbol, transaction.buySettings, event.fills); } else if (TransactionPhase.isFinalPhase(nextTransactionPhase)) { if(TransactionPhase.CANCELED != nextTransactionPhase) { - this.#calculateTradeValues(transaction.sellSettings, event.fills); + this.#calculateTradeValues(transaction.symbol, transaction.sellSettings, event.fills); transaction.result = (transaction.sellSettings.quantity * transaction.sellSettings.price) - (transaction.buySettings.quantity * transaction.buySettings.price) } this.#transactionHistory.push(transaction); @@ -320,20 +334,29 @@ export default class TradingBot { } /** + * @param {TradingPair} tradingpair * @param {TransactionSettings} taSettings * @param {TransactionFill[]} fills */ - #calculateTradeValues(taSettings, fills) { + #calculateTradeValues(tradingpair, taSettings, fills) { let priceSum = 0; let quantity = 0; + let commissionAsset1 = 0; + let commissionAsset2 = 0; fills.forEach(fill => { priceSum += fill.price * fill.quantity; quantity += fill.quantity; + + if(fill.commissionAsset != null && fill.commissionAsset.toLowerCase() == tradingpair.getFirstCurrency().getSymbol().toLowerCase()){ + commissionAsset1 += fill.commission; + } else if(fill.commissionAsset != null && fill.commissionAsset.toLowerCase() == tradingpair.getSecondCurrency().getSymbol().toLowerCase()){ + commissionAsset2 += fill.commission; + } }); - taSettings.quantity = quantity; - taSettings.price = quantity != 0 ? priceSum / quantity : 0; + taSettings.quantity = quantity - commissionAsset1; + taSettings.price = quantity != 0 ? (priceSum - commissionAsset2) / taSettings.quantity : 0; } /** diff --git a/src/js/tradingbot/TradingBotRestDataProvider.js b/src/js/tradingbot/TradingBotRestDataProvider.js index 7c4f6d6..3df5028 100644 --- a/src/js/tradingbot/TradingBotRestDataProvider.js +++ b/src/js/tradingbot/TradingBotRestDataProvider.js @@ -45,17 +45,25 @@ export default class TradingBotRestDataProvider extends WebViewDataProvider { * @param {string} symbol * @returns {Transaction[]} */ - getActiveTransactions(symbol) { - return this.#tradingBot.getTransactionsByAsset(TradingPairs.fromString(symbol)).sort((t1, t2) => t1.timestamp - t2.timestamp); + getActiveTransactions(symbol = null) { + if(symbol == null){ + return this.#tradingBot.getTransactions().sort((t1, t2) => t1.timestamp - t2.timestamp); + } else { + return this.#tradingBot.getTransactionsByAsset(TradingPairs.fromString(symbol)).sort((t1, t2) => t1.timestamp - t2.timestamp); + } } /** * @param {string} symbol * @returns {Transaction[]} */ - getAllTransactions(symbol) { + getAllTransactions(symbol = null) { const tp = TradingPairs.fromString(symbol); - return this.#tradingBot.getTransactionsByAsset(tp).concat(this.#tradingBot.getClosedTransactionsByAsset(tp)).sort((t1, t2) => t1.timestamp - t2.timestamp); + if(symbol == null){ + return this.#tradingBot.getTransactions().concat(this.#tradingBot.getClosedTransactions()).sort((t1, t2) => t1.timestamp - t2.timestamp); + } else { + return this.#tradingBot.getTransactionsByAsset(tp).concat(this.#tradingBot.getClosedTransactionsByAsset(tp)).sort((t1, t2) => t1.timestamp - t2.timestamp); + } } /** diff --git a/src/js/tradingbot/config/BotConfig.js b/src/js/tradingbot/config/BotConfig.js index a9a384f..0e19873 100644 --- a/src/js/tradingbot/config/BotConfig.js +++ b/src/js/tradingbot/config/BotConfig.js @@ -32,7 +32,7 @@ export default class BotConfig extends Serializable { /** * @type {string} chrono unit string; Time until trading should start */ - #rampupTime = "15m"; + #rampupTime = "16m"; /** * @type {number} max amount of USD, that can be used for trading diff --git a/src/js/tradingbot/data/asset/AggregatedDataPoint.js b/src/js/tradingbot/data/asset/AggregatedDataPoint.js index c8d5aea..f41b37e 100644 --- a/src/js/tradingbot/data/asset/AggregatedDataPoint.js +++ b/src/js/tradingbot/data/asset/AggregatedDataPoint.js @@ -77,6 +77,26 @@ export default class AggregatedDataPoint { */ peakEnds = 0; + /** + * @type {number} + */ + sma15min = 0; + + /** + * @type {number} + */ + ema15min = 0; + + /** + * @type {number} + */ + sma5min = 0; + + /** + * @type {number} + */ + ema5min = 0; + /** * @type {number[]} */ @@ -95,6 +115,10 @@ export default class AggregatedDataPoint { * @param {OrderBookEntry} ask */ pushAsk(ask){ + if(ask == null){ + return; + } + if(this.bestAsk == null || this.bestAsk.price < ask.price){ this.bestAsk = ask; } @@ -104,6 +128,10 @@ export default class AggregatedDataPoint { * @param {OrderBookEntry} bid */ pushBid(bid){ + if(bid == null){ + return; + } + if(!this.hasValues){ this.startPrice = bid.price; this.minPrice = bid.price; diff --git a/src/js/tradingbot/data/asset/AssetData.js b/src/js/tradingbot/data/asset/AssetData.js index 2cec5be..4d8d146 100644 --- a/src/js/tradingbot/data/asset/AssetData.js +++ b/src/js/tradingbot/data/asset/AssetData.js @@ -179,7 +179,7 @@ export default class AssetData { this.#assetDataAggregator.pushTrade(time, price, quantity); } - getAggregatedData(duration, timeOffset = -1){ - return this.#assetDataAggregator.getData(duration, timeOffset); + getAggregatedData(duration, timeOffset = -1, returnUnfinished = false){ + return this.#assetDataAggregator.getData(duration, timeOffset, returnUnfinished); } } \ No newline at end of file diff --git a/src/js/tradingbot/data/asset/AssetDataAggregator.js b/src/js/tradingbot/data/asset/AssetDataAggregator.js index b7b8f30..f94fe08 100644 --- a/src/js/tradingbot/data/asset/AssetDataAggregator.js +++ b/src/js/tradingbot/data/asset/AssetDataAggregator.js @@ -1,4 +1,5 @@ import OrderBookEntry from "../../../apiwrapper/orderbook/OrderBookEntry.js"; +import AssetInstrumentCalculator from "../../util/AssetInstrumentCalculator.js"; import UnitHelper from "../../util/UnitHelper.js"; import AggregatedDataPoint from "./AggregatedDataPoint.js"; @@ -54,6 +55,14 @@ export default class AssetDataAggregator { tmpSlot.addTrades(dp.tradeCount, dp.tradeVolume); } tmpSlot.end(timestamp + AssetDataAggregator.#SLOT_1_MIN); + if(this.#perMinuteDataPoints.length > 0){ + tmpSlot.sma5min = AssetInstrumentCalculator.calculateSMA(this.#perMinuteDataPoints.slice(-1 * Math.min(5, this.#perMinuteDataPoints.length))); + tmpSlot.ema5min = AssetInstrumentCalculator.calculateEMA(this.#perMinuteDataPoints.slice(-1 * Math.min(5, this.#perMinuteDataPoints.length))); + tmpSlot.sma15min = AssetInstrumentCalculator.calculateSMA(this.#perMinuteDataPoints.slice(-1 * Math.min(15, this.#perMinuteDataPoints.length))); + tmpSlot.ema15min = AssetInstrumentCalculator.calculateEMA(this.#perMinuteDataPoints.slice(-1 * Math.min(15, this.#perMinuteDataPoints.length))); + } else { + tmpSlot.sma5min = tmpSlot.ema5min = tmpSlot.sma15min = tmpSlot.ema15min = tmpSlot.avgPrice; + } this.#lastOneMinUpdate = timestamp; this.#pushSlotAndFillUp(this.#perMinuteDataPoints, tmpSlot, AssetDataAggregator.#SLOT_1_MIN, timestamp); @@ -121,9 +130,10 @@ export default class AssetDataAggregator { * * @param {string} duration chrono unit (eg. 1000 or 1s or 15m) * @param {number} start start timestamp + * @param {boolean} returnUnfinished if true, unfinished timeslots will be returned (minutes only!) * @returns {AggregatedDataPoint[]} */ - getData(duration, start = -1){ + getData(duration, start = -1, returnUnfinished = false){ let result = []; const durationInMs = UnitHelper.formatedDurationToMs(duration); @@ -136,8 +146,9 @@ export default class AssetDataAggregator { } else { const dpCount = Math.round(durationInMs / AssetDataAggregator.#SLOT_1_MIN); const pointOffset = start == -1 ? 0 : Math.round(start / AssetDataAggregator.#SLOT_1_MIN); - if(pointOffset + dpCount <= this.#perMinuteDataPoints.length){ - result = this.#perMinuteDataPoints.slice(-1 * (pointOffset + dpCount), pointOffset > 0 ? -1 * pointOffset : this.#perMinuteDataPoints.length); + if(pointOffset + dpCount <= this.#perMinuteDataPoints.length || returnUnfinished){ + const start = pointOffset + dpCount <= this.#perMinuteDataPoints.length ? -1 * (pointOffset + dpCount) : -1 * this.#perMinuteDataPoints.length; + result = this.#perMinuteDataPoints.slice(start, pointOffset > 0 ? -1 * pointOffset : this.#perMinuteDataPoints.length); } } diff --git a/src/js/tradingbot/data/asset/PeakDetector.js b/src/js/tradingbot/data/asset/PeakDetector.js index f42d648..9437e42 100644 --- a/src/js/tradingbot/data/asset/PeakDetector.js +++ b/src/js/tradingbot/data/asset/PeakDetector.js @@ -47,6 +47,10 @@ export default class PeakDetector { return; } + if(entry == null || entry.price == null){ + return; + } + if(this.#min > entry.price){ this.#min = entry.price; this.#minTime = Date.now(); diff --git a/src/js/tradingbot/strategy/AbstractStrategy.js b/src/js/tradingbot/strategy/AbstractStrategy.js index e3cfb61..9e32749 100644 --- a/src/js/tradingbot/strategy/AbstractStrategy.js +++ b/src/js/tradingbot/strategy/AbstractStrategy.js @@ -1,4 +1,5 @@ import Transaction from "../../apiwrapper/transaction/Transaction.js"; +import TransactionPhase from "../../apiwrapper/transaction/TransactionPhase.js"; import Logger from "../../logging/Logger.js"; import Serializable from "../../util/Serializable.js"; import AssetData from "../data/asset/AssetData.js"; @@ -56,6 +57,21 @@ export default class AbstractStrategy extends Serializable { return false; } + /** + * @param {Transaction} transaction + * @param {number} winPercent + */ + isAllowedToSell(transaction, winPercent){ + return transaction != null && !transaction.lockedForUpdate && winPercent > 0.4; + } + + /** + * @param {Transaction} transaction + */ + isAllowedToCancel(transaction){ + return transaction != null && !transaction.lockedForUpdate && transaction.phase != TransactionPhase.CANCELED; + } + /** * @returns {boolean} */ diff --git a/src/js/tradingbot/strategy/AvgEmaStrategy.js b/src/js/tradingbot/strategy/AvgEmaStrategy.js new file mode 100644 index 0000000..f1fbc15 --- /dev/null +++ b/src/js/tradingbot/strategy/AvgEmaStrategy.js @@ -0,0 +1,138 @@ +import Transaction from "../../apiwrapper/transaction/Transaction.js"; +import TransactionPhase from "../../apiwrapper/transaction/TransactionPhase.js"; +import AbstractStrategy from "./AbstractStrategy.js"; + +export default class AvgEmaStrategy extends AbstractStrategy { + #lastBuySuggestionTime = 0; + #waitingMultiplier = 1; + + #buyRecomendationRequired = false; + #sellRecomendationRequired = false; + + constructor(){ + super(); + this.#lastBuySuggestionTime = Date.now(); + } + + /** + * @param {Transaction} transaction + * @returns {boolean} + */ + shouldSell(transaction) { + const winPercent = (this.getAssetData().getOrderBook().getBestBid().price * 100) / transaction.buySettings.price - 100; + const elapsedTime = Date.now() - transaction.timestamp; + + if(!this.isAllowedToSell(transaction, winPercent)){ + return false; + } + + if(elapsedTime < 600000){ + return true; + } + + if(winPercent > 2.5){ + return true; + } + + const aggDataPoints = this.getAssetData().getAggregatedData('15m'); + const price = this.getAssetData().getOrderBook().getBestBid().price; + + let isEmaHigher = false; + for(let i = 10; i < aggDataPoints.length; i++){ + isEmaHigher |= aggDataPoints[i].ema15min > aggDataPoints[i].avgPrice; + } + + if(isEmaHigher){ + this.#waitingMultiplier = 1; + this.getLogger().event("[SELL][" + this.constructor.name + "] recomendet sell for " + transaction.id + " for " + price.toFixed(6)); + return true; + } + + return false; + } + + /** + * @returns {boolean} + */ + shouldBuy() { + if(Date.now() < this.#lastBuySuggestionTime + (60000 * this.#waitingMultiplier)){ + return false; + } + + this.#waitingMultiplier = 1; + + const aggDataPoints = this.getAssetData().getAggregatedData('15m'); + const aggDataPoints1h = this.getAssetData().getAggregatedData('1h', -1, true); + + if(aggDataPoints == null || aggDataPoints.length < 10 || aggDataPoints1h.length < 30){ + return false; + } + + if(aggDataPoints1h[0].ema15min > aggDataPoints1h[aggDataPoints1h.length - 1].ema15min){ + return false; + } + + const price = this.getAssetData().getOrderBook().getBestBid().price; + const priceChange15Min = 100 - (aggDataPoints[aggDataPoints.length - 1].minPrice * 100) / aggDataPoints[0].maxPrice; + + let isEmaHigher = true; + let wasEmaHigher = false; + for(let i = 10; i < aggDataPoints.length; i++){ + if(!wasEmaHigher && aggDataPoints[i].ema15min > aggDataPoints[i].avgPrice){ + wasEmaHigher =true; + continue; + } + if(wasEmaHigher && aggDataPoints[i].ema15min < aggDataPoints[i].avgPrice){ + isEmaHigher = false; + break; + } + } + + if(priceChange15Min > 0.4 && !isEmaHigher){ + this.#lastBuySuggestionTime = Date.now(); + this.#waitingMultiplier = 15; + this.getLogger().event("[BUY][" + this.constructor.name + "][" + this.getAssetData().getTradingPair().getKey() + "] recomendet buy for " + price.toFixed(6)); + return true; + } + + return false; + } + + /** + * @param {Transaction} transaction + * @returns {boolean} + */ + shouldCancle(transaction) { + if(!this.isAllowedToCancel(transaction)){ + return false; + } + + const price = this.getAssetData().getOrderBook().getBestBid().price; + + if(transaction.buySettings != null && transaction.phase == TransactionPhase.BUY_IN_PROGRESS){ + if(transaction.buySettings.orderPrice * 1.002 < price){ + this.getLogger().event("[CANCEL][" + this.constructor.name + "] recomendet cancel for " + transaction.id); + return true; + } + } + + return false; + } + + /** + * @returns {string} string representation of this object + */ + toJson() { + const obj = JSON.parse(super.toJson()); + + return JSON.stringify(obj); + } + + /** + * @param {string} objString + * @returns {AvgEmaStrategy} + */ + static fromJson(objString) { + return new AvgEmaStrategy(); + } +} \ No newline at end of file diff --git a/src/js/tradingbot/strategy/AvgMinMaxStrategy.js b/src/js/tradingbot/strategy/AvgMinMaxStrategy.js index 0b67e84..24f3388 100644 --- a/src/js/tradingbot/strategy/AvgMinMaxStrategy.js +++ b/src/js/tradingbot/strategy/AvgMinMaxStrategy.js @@ -17,15 +17,24 @@ export default class AvgMinMaxStrategy extends AbstractStrategy { shouldSell(transaction) { const winPercent = (this.getAssetData().getOrderBook().getBestBid().price / transaction.buySettings.price) * 100 - 100; - if(winPercent < 0.6){ + if(!this.isAllowedToSell(transaction, winPercent)){ return false; } + if(winPercent > 2.5){ + return true; + } + const aggDataPoints = this.getAssetData().getAggregatedData('15m'); const price = this.getAssetData().getOrderBook().getBestBid().price; const avg15m = aggDataPoints.length > 0 ? aggDataPoints.map(v => v.avgPrice).reduce((a, b) => a+b, 0) / aggDataPoints.length : 0; - return avg15m != 0 && avg15m * 1.005 <= price; + if(avg15m != 0 && avg15m * 1.005 <= price){ + this.getLogger().event("[SELL][" + this.constructor.name + "] recomendet sell for " + transaction.id + " for " + price.toFixed(6)); + return true; + } + + return false; } /** @@ -42,6 +51,7 @@ export default class AvgMinMaxStrategy extends AbstractStrategy { if(avg15m != 0 && avg15m * 0.995 >= price){ this.#lastBuySuggestionTime = Date.now(); + this.getLogger().event("[BUY][" + this.constructor.name + "] recomendet buy for " + price.toFixed(6)); return true; } @@ -53,10 +63,17 @@ export default class AvgMinMaxStrategy extends AbstractStrategy { * @returns {boolean} */ shouldCancle(transaction) { + if(!this.isAllowedToCancel(transaction)){ + return false; + } + const price = this.getAssetData().getOrderBook().getBestBid().price; if(transaction.buySettings != null && transaction.phase == TransactionPhase.BUY_IN_PROGRESS){ - return transaction.buySettings.orderPrice * 1.002 < price; + if(transaction.buySettings.orderPrice * 1.002 < price){ + this.getLogger().event("[CANCEL][" + this.constructor.name + "] recomendet cancel for " + transaction.id); + return true; + } } return false; diff --git a/src/js/tradingbot/strategy/PossiblePeakStrategy.js b/src/js/tradingbot/strategy/PossiblePeakStrategy.js index 2bf4f4c..d3f8295 100644 --- a/src/js/tradingbot/strategy/PossiblePeakStrategy.js +++ b/src/js/tradingbot/strategy/PossiblePeakStrategy.js @@ -14,6 +14,12 @@ export default class PossiblePeakStrategy extends AbstractStrategy { * @returns {boolean} */ shouldSell(transaction) { + const winPercent = (this.getAssetData().getOrderBook().getBestBid().price / transaction.buySettings.price) * 100 - 100; + + if(!this.isAllowedToSell(transaction, winPercent)){ + return false; + } + return this.getAssetData().getOrderBook().getBestBid().price / transaction.buySettings.price >= 1.015; } diff --git a/src/js/tradingbot/strategy/Simple24hStrategy.js b/src/js/tradingbot/strategy/Simple24hStrategy.js index 720c733..2fd8ef1 100644 --- a/src/js/tradingbot/strategy/Simple24hStrategy.js +++ b/src/js/tradingbot/strategy/Simple24hStrategy.js @@ -15,14 +15,14 @@ export default class Simple24hStrategy extends AbstractStrategy { */ shouldSell(transaction) { const winPercent = (this.getAssetData().getOrderBook().getBestBid().price / transaction.buySettings.price) * 100 - 100; + + if(!this.isAllowedToSell(transaction, winPercent)){ + return false; + } if(winPercent >= 3){ return true; } - - if(winPercent < 0.6){ - return false; - } const grades = this.#getGrades(); diff --git a/src/js/tradingbot/strategy/TestStrategy.js b/src/js/tradingbot/strategy/TestStrategy.js index c56fb94..65f6b34 100644 --- a/src/js/tradingbot/strategy/TestStrategy.js +++ b/src/js/tradingbot/strategy/TestStrategy.js @@ -1,6 +1,7 @@ import Transaction from "../../apiwrapper/transaction/Transaction.js"; import AbstractStrategy from "./AbstractStrategy.js"; -import AvgMinMaxStrategy2 from "./AvgMinMaxStrategy.js"; +import AvgEmaStrategy from "./AvgEmaStrategy.js"; +import AvgMinMaxStrategy from "./AvgMinMaxStrategy.js"; export default class TestStrategy extends AbstractStrategy { @@ -21,7 +22,8 @@ export default class TestStrategy extends AbstractStrategy { constructor(){ super(); - this.#strategies.push(new AvgMinMaxStrategy2()); + this.#strategies.push(new AvgMinMaxStrategy()); + this.#strategies.push(new AvgEmaStrategy()); } /** diff --git a/src/js/tradingbot/util/AssetInstrumentCalculator.js b/src/js/tradingbot/util/AssetInstrumentCalculator.js index f5dbfee..b745d30 100644 --- a/src/js/tradingbot/util/AssetInstrumentCalculator.js +++ b/src/js/tradingbot/util/AssetInstrumentCalculator.js @@ -1,4 +1,4 @@ -import AggregatedDataPoint from "../data/asset/AggregatedDataPoint"; +import AggregatedDataPoint from "../data/asset/AggregatedDataPoint.js"; export default class AssetInstrumentCalculator{ @@ -24,10 +24,10 @@ export default class AssetInstrumentCalculator{ * @param {AggregatedDataPoint[]} dataPoints * @returns {number} */ - static caclulateEMA(dataPoints) { + static calculateEMA(dataPoints) { const sma = AssetInstrumentCalculator.calculateSMA(dataPoints); const multiplier = 2 / (dataPoints.length + 1); - return sma * (1 - multiplier) + dataPoints[dataPoints.length - 1] * multiplier; + return sma * (1 - multiplier) + dataPoints[dataPoints.length - 1].avgPrice * multiplier; } } \ No newline at end of file diff --git a/src/js/tradingbot/worker/DefaultWorker.js b/src/js/tradingbot/worker/DefaultWorker.js index 661b3ae..6372828 100644 --- a/src/js/tradingbot/worker/DefaultWorker.js +++ b/src/js/tradingbot/worker/DefaultWorker.js @@ -29,7 +29,13 @@ export default class DefaultWorker extends AbstractWorker{ */ onUpdate(assetData, transactions, transactionlimit){ this.getStrategy().setAssetData(assetData); - const price = parseFloat(this.getStrategy().getAssetData().getOrderBook().getBestBid().price); + const bestBid = this.getStrategy().getAssetData().getOrderBook().getBestBid(); + + if(bestBid == null) { + return; + } + + const price = parseFloat(bestBid.price); if(transactionlimit > transactions.length && this.getStrategy().shouldBuy()){ this.getApiProvider().requestBuy(this, Math.round(50 / price), price); diff --git a/src/js/tradingbot/worker/SingleTransactionTestWorker.js b/src/js/tradingbot/worker/SingleTransactionTestWorker.js new file mode 100644 index 0000000..32831fe --- /dev/null +++ b/src/js/tradingbot/worker/SingleTransactionTestWorker.js @@ -0,0 +1,75 @@ +import TradingPair from "../../apiwrapper/assets/TradingPair.js"; +import Transaction from "../../apiwrapper/transaction/Transaction.js"; +import TransactionPhase from "../../apiwrapper/transaction/TransactionPhase.js"; +import AssetData from "../data/asset/AssetData.js"; +import AbstractStrategy from "../strategy/AbstractStrategy.js"; +import AbstractWorker from "./AbstractWorker.js"; + +export default class SingleTransactionTestWorker extends AbstractWorker{ + buyDone = false; + sellDone = false; + starttime = -1; + + /** + * @param {string} name + * @param {TradingPair} tradingPair + * @param {AbstractStrategy} strategy + */ + constructor(name, tradingPair, strategy){ + super(name, tradingPair, strategy); + this.starttime = Date.now(); + } + + /** + * @param {string} dataPath path to data directory + */ + onInit(dataPath){ + this.getLogger().debug("Worker " + this.getName() + " initialized!"); + } + + /** + * @param {AssetData} assetData + * @param {Transaction[]} transactions + * @param {number} transactionlimit + */ + onUpdate(assetData, transactions, transactionlimit){ + if(Date.now() - this.starttime < 30000){ + return; + } + + this.getStrategy().setAssetData(assetData); + const bestBid = this.getStrategy().getAssetData().getOrderBook().getBestBid(); + + if(bestBid == null) { + return; + } + + const price = parseFloat(bestBid.price); + + if(!this.buyDone){ + this.getApiProvider().requestBuy(this, Math.round(10 / price), price); + this.starttime = Date.now(); + this.buyDone = true; + } + + if(!this.sellDone && transactions.length > 0 && transactions[0].phase == TransactionPhase.BUY_DONE){ + this.getApiProvider().requestSell(this, transactions[0], assetData.getOrderBook().getBestAsk().price); + this.sellDone = true; + } + } + + /** + * @returns {string} string representation of this object + */ + toJson() { + return super.toJson(); + } + + /** + * @param {string} objString + * @returns {AbstractWorker} + */ + static fromJson(objString) { + return super.fromJson(objString, new SingleTransactionTestWorker()); + } +} \ No newline at end of file diff --git a/src/js/tradingbot/worker/StatisticWorker.js b/src/js/tradingbot/worker/StatisticWorker.js index 78c60d5..f320d58 100644 --- a/src/js/tradingbot/worker/StatisticWorker.js +++ b/src/js/tradingbot/worker/StatisticWorker.js @@ -47,6 +47,26 @@ export default class StatisticWorker extends AbstractWorker { */ #sells = []; + /** + * @type {[number, number][]} + */ + #sma5Min = []; + + /** + * @type {[number, number][]} + */ + #sma15Min = []; + + /** + * @type {[number, number][]} + */ + #ema5Min = []; + + /** + * @type {[number, number][]} + */ + #ema15Min = []; + /** * @type {number} */ @@ -175,6 +195,10 @@ export default class StatisticWorker extends AbstractWorker { peakEnd: this.#peakEnd[i][1], buys: this.#buys[i][1], sells: this.#sells[i][1], + sma5min: this.#sma5Min[i][1], + sma15min: this.#sma15Min[i][1], + ema5min: this.#ema5Min[i][1], + ema15min: this.#ema15Min[i][1], }); } @@ -206,6 +230,10 @@ export default class StatisticWorker extends AbstractWorker { this.#peakStart.push([timestamp, false]); this.#buys.push([timestamp, this.#buyCount]); this.#sells.push([timestamp, this.#sellCount]); + this.#sma5Min.push([timestamp, lastMinAssetData.sma5min]); + this.#sma15Min.push([timestamp, lastMinAssetData.sma15min]); + this.#ema5Min.push([timestamp, lastMinAssetData.ema5min]); + this.#ema15Min.push([timestamp, lastMinAssetData.ema15min]); peaks.sort((p1, p2) => p1.startTime - p2.startTime); let lastPeakIndex = 0; diff --git a/src/js/util/Serializables.js b/src/js/util/Serializables.js index dee60f3..9c336c0 100644 --- a/src/js/util/Serializables.js +++ b/src/js/util/Serializables.js @@ -6,12 +6,14 @@ import Transaction from "../apiwrapper/transaction/Transaction.js"; import TransactionSettings from "../apiwrapper/transaction/TransactionSettings.js"; import BotConfig from "../tradingbot/config/BotConfig.js"; import AbstractStrategy from "../tradingbot/strategy/AbstractStrategy.js"; +import AvgEmaStrategy from "../tradingbot/strategy/AvgEmaStrategy.js"; import AvgMinMaxStrategy from "../tradingbot/strategy/AvgMinMaxStrategy.js"; import PossiblePeakStrategy from "../tradingbot/strategy/PossiblePeakStrategy.js"; import Simple24hStrategy from "../tradingbot/strategy/Simple24hStrategy.js"; import TestStrategy from "../tradingbot/strategy/TestStrategy.js"; import AbstractWorker from "../tradingbot/worker/AbstractWorker.js"; import DefaultWorker from "../tradingbot/worker/DefaultWorker.js"; +import SingleTransactionTestWorker from "../tradingbot/worker/SingleTransactionTestWorker.js"; import StatisticWorker from "../tradingbot/worker/StatisticWorker.js"; export default class Serializables { @@ -31,5 +33,7 @@ export default class Serializables { ['Simple24hStrategy', Simple24hStrategy], ['PossiblePeakStrategy', PossiblePeakStrategy], ['AvgMinMaxStrategy', AvgMinMaxStrategy], + ['AvgEmaStrategy', AvgEmaStrategy], + ['SingleTransactionTestWorker', SingleTransactionTestWorker], ]); } \ No newline at end of file diff --git a/src/js/webview/RestClient.js b/src/js/webview/RestClient.js index 7bc4a8f..95052f9 100644 --- a/src/js/webview/RestClient.js +++ b/src/js/webview/RestClient.js @@ -61,6 +61,14 @@ export default class RestClient { res.json(this.#getAllOrderBooks()); }); + this.#service.get("/assets/transactions", (req, res, next) => { + res.json(this.#getAllTransactions(req.query.type)); + }); + + this.#service.get("/assets/balance", (req, res, next) => { + res.json(this.#getBalances()); + }); + this.#service.get("/asset/:symbol/orderbook", (req, res, next) => { res.json(this.#getAssetOrderBook(req.params.symbol)); }); @@ -73,6 +81,10 @@ export default class RestClient { res.json(this.#getAggregatedAssetData(req.params.symbol, req.query.startTime, req.query.duration)); }); + this.#service.get("/asset/:symbol/balance", (req, res, next) => { + res.json(this.#getBalances(req.params.symbol)); + }); + this.#service.get("/worker/:name/attribute/:attr", (req, res, next) => { res.json(this.#getWorkerAttribute(req.params.name, req.params.attr)); }); @@ -99,6 +111,26 @@ export default class RestClient { return result; } + #getBalances(asset = null){ + /** + * @type {Map} + */ + const balances = this.#dataProvider.getBalances(); + const result = []; + + if(asset == null){ + balances.forEach((value, key) => result.push({symbol: key, balance: value})); + return result; + } else { + const balance = balances.get(asset); + if(balance == null){ + return null; + } else { + return {symbol: asset, balance: balance}; + } + } + } + #getAssetOrderBook(asset){ const orderBook = this.#dataProvider.getOrderBook(asset); @@ -113,6 +145,14 @@ export default class RestClient { return transactions.map(t => transactionConverter.toRestObject(t)); } + #getAllTransactions(type = null){ + const allTransactions = type == "all"; + const transactions = allTransactions ? this.#dataProvider.getAllTransactions() : this.#dataProvider.getActiveTransactions(); + const transactionConverter = new TransactionConverter(); + + return transactions.map(t => transactionConverter.toRestObject(t)); + } + #getAllOrderBooks(){ const orderBooks = []; diff --git a/src/js/webview/WebViewDataProvider.js b/src/js/webview/WebViewDataProvider.js index e7f39f5..5f10d85 100644 --- a/src/js/webview/WebViewDataProvider.js +++ b/src/js/webview/WebViewDataProvider.js @@ -37,7 +37,7 @@ export default class WebViewDataProvider { * @param {string} symbol * @returns {Transaction[]} */ - getActiveTransactions(symbol) { + getActiveTransactions(symbol = null) { return []; } @@ -45,7 +45,7 @@ export default class WebViewDataProvider { * @param {string} symbol * @returns {Transaction[]} */ - getAllTransactions(symbol) { + getAllTransactions(symbol = null) { return []; }