From edd68e31d0a9e2cd25a845b34726b18aac383ffa Mon Sep 17 00:00:00 2001 From: darkeye Date: Fri, 14 Feb 2025 11:27:05 +0100 Subject: [PATCH] =?UTF-8?q?[BOT]=20Pool=20system=20eingef=C3=BChrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/tradingbot/TradingBot.js | 84 ++++++++++++------- .../data/asset/AggregatedDataPoint.js | 10 +++ .../data/asset/AssetDataAggregator.js | 4 +- src/js/tradingbot/worker/AbstractWorker.js | 57 +++++++++---- src/js/webview/RestClient.js | 39 +++++++-- src/js/webview/converter/ConverterFactory.js | 13 ++- .../converter/converters/WorkerConverter.js | 32 +++++++ 7 files changed, 182 insertions(+), 57 deletions(-) create mode 100644 src/js/webview/converter/converters/WorkerConverter.js diff --git a/src/js/tradingbot/TradingBot.js b/src/js/tradingbot/TradingBot.js index 894ccf7..f227290 100644 --- a/src/js/tradingbot/TradingBot.js +++ b/src/js/tradingbot/TradingBot.js @@ -45,6 +45,11 @@ export default class TradingBot { */ #transactions = []; + /** + * @type {Map} + */ + #activeTransactionPerPoolCounter = new Map(); + /** * @type {Transaction[]} */ @@ -100,7 +105,7 @@ export default class TradingBot { this.#logger = new Logger(this.#config.getLogDirectory()); this.#dataCollector = new AssetDataCollector(this.#logger, this.#config); this.#api = new BinanceApiClient(this.#config.getApiConfig(), this.#logger); - if(this.#config.isRestInterfaceEnabled()){ + if (this.#config.isRestInterfaceEnabled()) { this.#restInterfaceBinding = new TradingBotRestDataProvider(this); this.#restClient = new RestClient(this.#config.getRestInterfacePort()); this.#restClient.start(this.#restInterfaceBinding); @@ -108,7 +113,7 @@ export default class TradingBot { } start() { - if(this.#isRunning){ + if (this.#isRunning) { this.#isTradingStarted = true; return; } @@ -119,16 +124,16 @@ export default class TradingBot { this.#api.subscribeBalanceChanges(); this.#api.requestBalances(); this.#api.requestAccountInformation(); - this.#loadTransactions(); this.#loadWorker(); + this.#loadTransactions(); this.#worker.forEach(worker => { this.#registerTradingPair(worker.getTradingPair()); worker.init(this.#logger, this.#config.getDataDirectory(), this); }); this.#transactions.forEach(transaction => { - if(transaction.buySettings != null && (transaction.phase === TransactionPhase.INITIAL || transaction.phase === TransactionPhase.BUY_IN_PROGRESS)){ + if (transaction.buySettings != null && (transaction.phase === TransactionPhase.INITIAL || transaction.phase === TransactionPhase.BUY_IN_PROGRESS)) { this.#api.requestTransactionUpdate(transaction, 'buy'); - } else if(transaction.sellSettings != null){ + } else if (transaction.sellSettings != null) { this.#api.requestTransactionUpdate(transaction, 'sell'); } }); @@ -150,14 +155,14 @@ export default class TradingBot { /** * @returns {AbstractWorker[]} */ - getWorker(){ + getWorker() { return this.#worker; } /** * @returns {boolean} */ - isTradingUp(){ + isTradingUp() { return this.#isTradingStarted; } @@ -187,14 +192,14 @@ export default class TradingBot { /** * @returns {Transaction[]} */ - getTransactions(){ + getTransactions() { return this.#transactions; } /** * @returns {Transaction[]} */ - getClosedTransactions(){ + getClosedTransactions() { return this.#transactionHistory; } @@ -219,7 +224,7 @@ export default class TradingBot { this.#worker.set(worker.getId(), worker); this.#saveWorker(); - if(this.#isRunning){ + if (this.#isRunning) { worker.init(this.#logger, this.#config.getDataDirectory()); this.#registerTradingPair(worker.getTradingPair()); } @@ -268,10 +273,10 @@ export default class TradingBot { handleAssetUpdate(assetUpdateEvent) { this.#dataCollector.pushAssetEvent(assetUpdateEvent); - if(assetUpdateEvent.eventType == APIEventType.ORDER_BOOK_UPDATE + if (assetUpdateEvent.eventType == APIEventType.ORDER_BOOK_UPDATE && (!this.#orderBookUpdateTime.has(assetUpdateEvent.tradingPair.getKey()) - || this.#orderBookUpdateTime.get(assetUpdateEvent.tradingPair.getKey()) < Date.now() - 120000) - ){ + || this.#orderBookUpdateTime.get(assetUpdateEvent.tradingPair.getKey()) < Date.now() - 120000) + ) { this.#api.requestOrderBook(assetUpdateEvent.tradingPair); this.#orderBookUpdateTime.set(assetUpdateEvent.tradingPair.getKey(), Date.now()); } @@ -298,8 +303,8 @@ export default class TradingBot { let index = -1; this.#transactions.forEach((v, i) => { - if (v.id == event.transactionId - || (v.buySettings != null && v.buySettings.apiID == event.apiId) + if (v.id == event.transactionId + || (v.buySettings != null && v.buySettings.apiID == event.apiId) || (v.sellSettings != null && v.sellSettings.apiID == event.apiId) ) { transaction = v; @@ -317,12 +322,13 @@ export default class TradingBot { if (TransactionPhase.BUY_DONE === nextTransactionPhase) { this.#calculateTradeValues(transaction.symbol, transaction.buySettings, event.fills); } else if (TransactionPhase.isFinalPhase(nextTransactionPhase)) { - if(TransactionPhase.CANCELED != nextTransactionPhase) { + if (TransactionPhase.CANCELED != nextTransactionPhase) { 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); this.#transactions.splice(index, 1); + this.#updateTransactionsPerPoolCounter(this.#worker.get(transaction.initiator).getPool(), -1); } transaction.phase = nextTransactionPhase; @@ -348,9 +354,9 @@ export default class TradingBot { priceSum += fill.price * fill.quantity; quantity += fill.quantity; - if(fill.commissionAsset != null && fill.commissionAsset.toLowerCase() == tradingpair.getFirstCurrency().getSymbol().toLowerCase()){ + 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()){ + } else if (fill.commissionAsset != null && fill.commissionAsset.toLowerCase() == tradingpair.getSecondCurrency().getSymbol().toLowerCase()) { commissionAsset2 += fill.commission; } }); @@ -376,8 +382,12 @@ export default class TradingBot { * @param {number} quantity * @param {number} price */ - requestBuy(worker, quantity, price){ - if(this.#config.getUsdTradeLimit() / this.#config.getPerTradeLimit() <= this.#transactions.length){ + requestBuy(worker, quantity, price) { + console.log("Open transactions in pool '" + worker.getPool() + "': " + this.#activeTransactionPerPoolCounter.get(worker.getPool())); + if ( + this.#activeTransactionPerPoolCounter.get(worker.getPool()) != null + && this.#config.getUsdTradeLimit() / this.#config.getPerTradeLimit() <= this.#activeTransactionPerPoolCounter.get(worker.getPool()) + ) { return; } @@ -385,6 +395,7 @@ export default class TradingBot { transaction.buySettings = new TransactionSettings(price, this.#config.getPerTradeLimit() / price); transaction.lockedForUpdate = true; this.#transactions.push(transaction); + this.#updateTransactionsPerPoolCounter(worker.getPool(), 1); this.#saveTransactions(); this.#api.requestBuy(transaction); @@ -395,8 +406,8 @@ export default class TradingBot { * @param {Transaction} transaction * @param {number} price */ - requestSell(worker, transaction, price){ - if(transaction.lockedForUpdate){ + requestSell(worker, transaction, price) { + if (transaction.lockedForUpdate) { this.#logger.warn("Attempt to sell a locked transaction!", transaction.id); return; } @@ -409,27 +420,42 @@ export default class TradingBot { * @param {AbstractWorker} worker * @param {Transaction} transaction */ - requestCancle(worker, transaction){ - if(transaction.lockedForUpdate){ + requestCancle(worker, transaction) { + if (transaction.lockedForUpdate) { this.#logger.warn("Attempt to cancel a locked transaction!", transaction.id); return; } - if(transaction.buySettings != null && transaction.phase == TransactionPhase.BUY_IN_PROGRESS){ - transaction.lockedForUpdate = true; + if (transaction.buySettings != null && transaction.phase == TransactionPhase.BUY_IN_PROGRESS) { + transaction.lockedForUpdate = true; this.#api.requestBuyCancel(transaction); } - if(transaction.sellSettings != null && transaction.phase == TransactionPhase.SELL_IN_PROGRESS){ - transaction.lockedForUpdate = true; + if (transaction.sellSettings != null && transaction.phase == TransactionPhase.SELL_IN_PROGRESS) { + transaction.lockedForUpdate = true; this.#api.requestSellCancel(transaction); } } + /** + * @param {string} pool + * @param {number} changeValue + */ + #updateTransactionsPerPoolCounter(pool, changeValue) { + if (this.#activeTransactionPerPoolCounter.has(pool)) { + this.#activeTransactionPerPoolCounter.set(pool, Math.max(0, this.#activeTransactionPerPoolCounter.get(pool) + changeValue)); + } else { + this.#activeTransactionPerPoolCounter.set(pool, Math.max(0, changeValue)); + } + } + #loadTransactions() { try { this.#transactions = SerializableHelper.load(this.#config.getDataDirectory(), 'transactions.sjson', Transaction); this.#transactions = this.#transactions.filter(t => t.phase != TransactionPhase.INITIAL); - this.#transactions.forEach(t => t.lockedForUpdate = false); + this.#transactions.forEach(t => { + t.lockedForUpdate = false; + this.#updateTransactionsPerPoolCounter(this.#worker.get(t.initiator).getPool(), 1); + }); } catch (e) { this.#logger.warn("Bot was unable to load any transactions. Assuming no active transactions existing!", e); } diff --git a/src/js/tradingbot/data/asset/AggregatedDataPoint.js b/src/js/tradingbot/data/asset/AggregatedDataPoint.js index f41b37e..107a271 100644 --- a/src/js/tradingbot/data/asset/AggregatedDataPoint.js +++ b/src/js/tradingbot/data/asset/AggregatedDataPoint.js @@ -97,6 +97,16 @@ export default class AggregatedDataPoint { */ ema5min = 0; + /** + * @type {number} + */ + sma1h = 0; + + /** + * @type {number} + */ + ema1h = 0; + /** * @type {number[]} */ diff --git a/src/js/tradingbot/data/asset/AssetDataAggregator.js b/src/js/tradingbot/data/asset/AssetDataAggregator.js index f94fe08..848b36d 100644 --- a/src/js/tradingbot/data/asset/AssetDataAggregator.js +++ b/src/js/tradingbot/data/asset/AssetDataAggregator.js @@ -60,8 +60,10 @@ export default class AssetDataAggregator { 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))); + tmpSlot.sma1h = AssetInstrumentCalculator.calculateSMA(this.#perMinuteDataPoints.slice(-1 * Math.min(60, this.#perMinuteDataPoints.length))); + tmpSlot.ema1h = AssetInstrumentCalculator.calculateEMA(this.#perMinuteDataPoints.slice(-1 * Math.min(60, this.#perMinuteDataPoints.length))); } else { - tmpSlot.sma5min = tmpSlot.ema5min = tmpSlot.sma15min = tmpSlot.ema15min = tmpSlot.avgPrice; + tmpSlot.sma5min = tmpSlot.ema5min = tmpSlot.sma15min = tmpSlot.ema15min = tmpSlot.sma1h = tmpSlot.ema1h = tmpSlot.avgPrice; } this.#lastOneMinUpdate = timestamp; diff --git a/src/js/tradingbot/worker/AbstractWorker.js b/src/js/tradingbot/worker/AbstractWorker.js index 603ef87..cd49e3c 100644 --- a/src/js/tradingbot/worker/AbstractWorker.js +++ b/src/js/tradingbot/worker/AbstractWorker.js @@ -19,6 +19,11 @@ export default class AbstractWorker extends Serializable { */ #name = ""; + /** + * @type {string} pool name. All workes in same pool shares same balance + */ + #pool = "default"; + /** * @type {TradingPair} tradingpair */ @@ -44,7 +49,7 @@ export default class AbstractWorker extends Serializable { * @param {TradingPair} tradingPair * @param {AbstractStrategy} strategy */ - constructor(name, tradingPair, strategy){ + constructor(name, tradingPair, strategy) { super(); this.#name = name; this.#tradingpair = tradingPair; @@ -57,10 +62,10 @@ export default class AbstractWorker extends Serializable { * @param {string} dataPath * @param {WorkerTradingApiProvider} apiProvider */ - init(logger, dataPath, apiProvider){ + init(logger, dataPath, apiProvider) { this.#logger = logger; this.#apiProvider = apiProvider; - if(this.#strategy != null){ + if (this.#strategy != null) { this.#strategy.init(logger); } this.onInit(dataPath); @@ -69,70 +74,84 @@ export default class AbstractWorker extends Serializable { /** * @returns {string} */ - getName(){ + getName() { return this.#name; } /** * @param {string} name */ - setName(name){ + setName(name) { this.#name = name; } /** * @returns {string} */ - getId(){ + getPool() { + return this.#pool; + } + + /** + * @param {string} pool + */ + setPool(pool) { + this.#pool = pool; + } + + /** + * @returns {string} + */ + getId() { return this.#id; } /** * @returns {TradingPair} */ - getTradingPair(){ + getTradingPair() { return this.#tradingpair; } /** * @param {TradingPair} tradingPair */ - setTradingPair(tradingPair){ + setTradingPair(tradingPair) { this.#tradingpair = tradingPair; } /** * @returns {AbstractStrategy} */ - getStrategy(){ + getStrategy() { return this.#strategy; } /** * @param {AbstractStrategy} strategy */ - setStrategy(strategy){ + setStrategy(strategy) { this.#strategy = strategy; } /** * @returns {Logger} */ - getLogger(){ + getLogger() { return this.#logger; } /** * @returns {WorkerTradingApiProvider} */ - getApiProvider(){ + getApiProvider() { return this.#apiProvider; } /** * @param {string} dataPath path to data directory */ - onInit(dataPath){ + onInit(dataPath) { // TODO implement in specific worker } @@ -141,7 +160,7 @@ export default class AbstractWorker extends Serializable { * @param {Transaction[]} transactions * @param {number} transactionlimit */ - onUpdate(assetData, transactions, transactionlimit){ + onUpdate(assetData, transactions, transactionlimit) { // TODO implement update } @@ -152,12 +171,12 @@ export default class AbstractWorker extends Serializable { * @returns {any} attribute value or null * @throws {Error} if the requested attribute is not supported by the worker instance */ - getAttribute(attributeName){ + getAttribute(attributeName) { // implement in worker return {}; } - #generateId(){ + #generateId() { return "W-" + this.#name + "-" + Date.now() + "-" + Math.floor(Math.random() * 8999999 + 1000000); } @@ -168,9 +187,10 @@ export default class AbstractWorker extends Serializable { const obj = JSON.parse(super.toJson()); obj.id = this.#id; obj.name = this.#name; + obj.pool = this.#pool; obj.tradingPair = SerializableHelper.serialize(this.#tradingpair); obj.strategy = SerializableHelper.serialize(this.#strategy); - + return JSON.stringify(obj); } @@ -187,6 +207,9 @@ export default class AbstractWorker extends Serializable { worker.#name = obj.name; worker.#tradingpair = SerializableHelper.deserialize(obj.tradingPair); worker.#strategy = SerializableHelper.deserialize(obj.strategy); + if (obj.pool != null) { + worker.#pool = obj.pool; + } return worker; } diff --git a/src/js/webview/RestClient.js b/src/js/webview/RestClient.js index 95052f9..16fe8a7 100644 --- a/src/js/webview/RestClient.js +++ b/src/js/webview/RestClient.js @@ -5,6 +5,7 @@ import WebViewDataProvider from "./WebViewDataProvider.js"; import ConverterFactory from "./converter/ConverterFactory.js"; import AggregatedDataPointConverter from "./converter/converters/AggregatedDataPointConverter.js"; import TransactionConverter from "./converter/converters/TransactionConverter.js"; +import WorkerConverter from "./converter/converters/WorkerConverter.js"; export default class RestClient { @@ -78,31 +79,45 @@ export default class RestClient { }); this.#service.get("/asset/:symbol/aggregated", (req, res, next) => { - res.json(this.#getAggregatedAssetData(req.params.symbol, req.query.startTime, req.query.duration)); + res.json(this.#getAggregatedAssetData(req.params.symbol, req.query.startTime, req.query.duration, req.query.allowUnfilled)); }); this.#service.get("/asset/:symbol/balance", (req, res, next) => { res.json(this.#getBalances(req.params.symbol)); }); + this.#service.get("/worker", (req, res, next) => { + res.json(this.#getWorker()); + }); + this.#service.get("/worker/:name/attribute/:attr", (req, res, next) => { res.json(this.#getWorkerAttribute(req.params.name, req.params.attr)); }); - - /*this.#service.get("/transactions", (req, res, next) => { - res.json(this.#getAssetOrderBook(req.params.symbol)); - });*/ } #registerSetters(){ + this.#service.get("/asset/:symbol/transaction/:transactionID/sell", (req, res, next) => { + // TODO sell transaction if status still same as before (query parameter: old status) + //res.json(this.#getTransactions(req.params.symbol, req.query.type)); + }); + this.#service.get("/asset/:symbol/transaction/:transactionID/cancel", (req, res, next) => { + // TODO cancle transaction if status still same as before (query parameter: old status) + //res.json(this.#getTransactions(req.params.symbol, req.query.type)); + }); + + this.#service.get("/asset/:symbol/transaction/:transactionID/delete", (req, res, next) => { + // TODO sell transaction if status still same as before (query parameter: old status) + //res.json(this.#getTransactions(req.params.symbol, req.query.type)); + }); } - #getAggregatedAssetData(asset, starttime, duration){ + #getAggregatedAssetData(asset, starttime, duration, allowUnfilled){ const st = starttime != null ? starttime : -1; const d = duration != null ? duration : 3600000; + const allowUnfinished= allowUnfilled != null ? allowUnfilled == "true" : false; - const data = this.#dataProvider.getAssetData(asset).getAggregatedData(d, st); + const data = this.#dataProvider.getAssetData(asset).getAggregatedData(d, st, allowUnfinished); const converter = new AggregatedDataPointConverter(); const result = []; @@ -164,6 +179,16 @@ export default class RestClient { return orderBooks; } + #getWorker(){ + const workerConverter = new WorkerConverter(); + let result = []; + this.#dataProvider.getWorkers().forEach(w => { + result.push(workerConverter.toRestObject(w)); + }); + + return result; + } + #getWorkerAttribute(name, attributeName){ let result = null; this.#dataProvider.getWorkers().forEach(w => { diff --git a/src/js/webview/converter/ConverterFactory.js b/src/js/webview/converter/ConverterFactory.js index 5d09246..1eb3762 100644 --- a/src/js/webview/converter/ConverterFactory.js +++ b/src/js/webview/converter/ConverterFactory.js @@ -3,12 +3,14 @@ import OrderBookEntry from "../../apiwrapper/orderbook/OrderBookEntry.js"; import Transaction from "../../apiwrapper/transaction/Transaction.js"; import TransactionSettings from "../../apiwrapper/transaction/TransactionSettings.js"; import AggregatedDataPoint from "../../tradingbot/data/asset/AggregatedDataPoint.js"; +import AbstractWorker from "../../tradingbot/worker/AbstractWorker.js"; import AbstractConverter from "./AbstractConverter.js"; import AggregatedDataPointConverter from "./converters/AggregatedDataPointConverter.js"; import OrderBookConverter from "./converters/OrderBookConverter.js"; import OrderBookEntryConverter from "./converters/OrderBookEntryConverter.js"; import TransactionConverter from "./converters/TransactionConverter.js"; import TransactionSettingsConverter from "./converters/TransactionSettingsConverter.js"; +import WorkerConverter from "./converters/WorkerConverter.js"; export default class ConverterFactory { static OBJECT_TYPE_ORDER_BOOK = 'order_book'; @@ -16,6 +18,7 @@ export default class ConverterFactory { static OBJECT_TYPE_AGGREGATED_DATA_POINT = 'aggregated_data_point'; static OBJECT_TYPE_TRANSACTION = 'transaction'; static OBJECT_TYPE_TRANSACTION_SETTINGS = 'transaction_settings'; + static OBJECT_TYPE_ABSTRACT_WORKER = 'abstract_worker'; /** * @param {any} obj @@ -34,12 +37,14 @@ export default class ConverterFactory { return new OrderBookConverter(); } else if (obj instanceof OrderBookEntry) { return new OrderBookEntryConverter(); - } else if (obj instanceof AggregatedDataPoint){ + } else if (obj instanceof AggregatedDataPoint) { return new AggregatedDataPointConverter(); - } else if (obj instanceof Transaction){ + } else if (obj instanceof Transaction) { return new TransactionConverter(); - } else if (obj instanceof TransactionSettings){ + } else if (obj instanceof TransactionSettings) { return new TransactionSettingsConverter(); + } else if (obj instanceof AbstractWorker) { + return new WorkerConverter(); } throw new Error("No converter defined for: " + JSON.stringify(obj)); @@ -61,6 +66,8 @@ export default class ConverterFactory { return new TransactionConverter(); case this.OBJECT_TYPE_TRANSACTION_SETTINGS: return new TransactionSettingsConverter(); + case this.OBJECT_TYPE_ABSTRACT_WORKER: + return new WorkerConverter(); default: throw new Error("No converter defined for type: " + type); } diff --git a/src/js/webview/converter/converters/WorkerConverter.js b/src/js/webview/converter/converters/WorkerConverter.js new file mode 100644 index 0000000..a824a1d --- /dev/null +++ b/src/js/webview/converter/converters/WorkerConverter.js @@ -0,0 +1,32 @@ +import AbstractWorker from "../../../tradingbot/worker/AbstractWorker.js"; +import AbstractConverter from "../AbstractConverter.js"; +import ConverterFactory from "../ConverterFactory.js"; + +export default class WorkerConverter extends AbstractConverter { + + constructor(){ + super(); + } + + /** + * @param {AbstractWorker} obj + * @returns {object} + */ + toRestObject(obj) { + return { + _objectType: ConverterFactory.OBJECT_TYPE_ABSTRACT_WORKER, + id: obj.getId(), + name: obj.getName(), + symbol: obj.getTradingPair().getKey(), + strategy: obj.getStrategy().constructor.name + }; + } + + /** + * @param {object} obj + * @returns {AbstractWorker} + */ + fromRestObject(obj) { + return null; + } +} \ No newline at end of file