import { useApi } from '@/shared/apiService'; import { useUserService } from '@/shared/userService'; import { BotState, Trade, PlotConfig, StrategyResult, BalanceInterface, DailyReturnValue, LockResponse, ProfitInterface, BacktestResult, StrategyBacktestResult, BacktestSteps, LogLine, SysInfoResponse, LoadingStatus, BacktestHistoryEntry, RunModes, DailyPayload, BlacklistResponse, WhitelistResponse, StrategyListResult, AvailablePairPayload, AvailablePairResult, PairHistoryPayload, PairCandlePayload, StatusResponse, ForceSellPayload, DeleteTradeResponse, BacktestStatus, BacktestPayload, BlacklistPayload, ForceEnterPayload, TradeResponse, ClosedTrade, BotDescriptor, BgTaskStarted, BackgroundTaskStatus, Exchange, ExchangeListResult, FreqAIModelListResult, PairlistEvalResponse, PairlistsPayload, PairlistsResponse, } from '@/types'; import axios, { AxiosResponse } from 'axios'; import { defineStore } from 'pinia'; import { showAlert } from './alerts'; import { useWebSocket } from '@vueuse/core'; import { FTWsMessage, FtWsMessageTypes } from '@/types/wsMessageTypes'; import { showNotification } from '@/shared/notifications'; export function createBotSubStore(botId: string, botName: string) { const userService = useUserService(botId); const { api } = useApi(userService, botId); const useBotStore = defineStore(botId, { state: () => { return { websocketStarted: false, isSelected: true, ping: '', botStatusAvailable: false, isBotOnline: false, isBotLoggedIn: true, autoRefresh: false, refreshing: false, versionState: '', lastLogs: [] as LogLine[], refreshRequired: true, trades: [] as ClosedTrade[], openTrades: [] as Trade[], tradeCount: 0, performanceStats: [] as Performance[], whitelist: [] as string[], blacklist: [] as string[], profit: {} as ProfitInterface, botState: {} as BotState, balance: {} as BalanceInterface, dailyStats: {} as DailyReturnValue, pairlistMethods: [] as string[], detailTradeId: null as number | null, selectedPair: '', // TODO: type me candleData: {}, candleDataStatus: LoadingStatus.loading, // TODO: type me history: {}, historyStatus: LoadingStatus.loading, strategyPlotConfig: undefined as PlotConfig | undefined, strategyList: [] as string[], freqaiModelList: [] as string[], exchangeList: [] as Exchange[], strategy: {} as StrategyResult, pairlist: [] as string[], currentLocks: undefined as LockResponse | undefined, // backtesting backtestRunning: false, backtestProgress: 0.0, backtestStep: BacktestSteps.none, backtestTradeCount: 0, backtestResult: undefined as BacktestResult | undefined, selectedBacktestResultKey: '', backtestHistory: {} as Record, backtestHistoryList: [] as BacktestHistoryEntry[], sysInfo: {} as SysInfoResponse, }; }, getters: { version: (state) => state.botState?.version || state.versionState, botApiVersion: (state) => state.botState?.api_version || 1.0, stakeCurrency: (state) => state.botState?.stake_currency || '', stakeCurrencyDecimals: (state) => state.botState?.stake_currency_decimals || 3, canRunBacktest: (state) => state.botState?.runmode === RunModes.WEBSERVER, isWebserverMode: (state) => state.botState?.runmode === RunModes.WEBSERVER, selectedBacktestResult: (state) => state.backtestHistory[state.selectedBacktestResultKey], shortAllowed: (state) => state.botState?.short_allowed || false, openTradeCount: (state) => state.openTrades.length, isTrading: (state) => state.botState?.runmode === RunModes.LIVE || state.botState?.runmode === RunModes.DRY_RUN, timeframe: (state) => state.botState?.timeframe || '', closedTrades: (state) => { return state.trades .filter((item) => !item.is_open) .sort((a, b) => // Sort by close timestamp, then by tradeid b.close_timestamp && a.close_timestamp ? b.close_timestamp - a.close_timestamp : b.trade_id - a.trade_id, ); }, tradeDetail: (state): Trade | undefined => { // console.log('tradeDetail', state.openTrades.length, state.openTrades); let dTrade = state.openTrades.find((item) => item.trade_id === state.detailTradeId); if (!dTrade) { dTrade = state.trades.find((item) => item.trade_id === state.detailTradeId); } return dTrade; }, refreshNow: (state) => { if ( state.autoRefresh && state.isBotOnline && state.botState?.runmode !== RunModes.WEBSERVER ) { return true; } return false; }, botName: (state) => state.botState?.bot_name || 'freqtrade', allTrades: (state) => [...state.openTrades, ...state.trades] as Trade[], activeLocks: (state) => state.currentLocks?.locks || [], dailyStatsSorted: (state): DailyReturnValue => { return { ...state.dailyStats, data: state.dailyStats.data ? Object.values(state.dailyStats.data).sort((a, b) => (a.date > b.date ? 1 : -1)) : [], }; }, }, actions: { botAdded() { this.autoRefresh = userService.getAutoRefresh(); }, async fetchPing() { try { const result = await api.get('/ping'); const now = Date.now(); this.ping = `${result.data.status} ${now.toString()}`; this.setIsBotOnline(true); return Promise.resolve(); } catch (error) { console.log('ping fail'); this.setIsBotOnline(false); return Promise.reject(); } }, logout() { userService.logout(); }, getLoginInfo() { return userService.getLoginInfo(); }, updateBot(updatedBotInfo: Partial) { userService.updateBot(updatedBotInfo); }, setAutoRefresh(newRefreshValue) { this.autoRefresh = newRefreshValue; // TODO: Investigate this - // this ONLY works if ReloadControl is only visible once,otherwise it triggers twice if (newRefreshValue) { this.refreshFrequent(); this.refreshSlow(true); } userService.setAutoRefresh(newRefreshValue); }, setIsBotOnline(isBotOnline: boolean) { this.isBotOnline = isBotOnline; }, async refreshSlow(forceUpdate = false) { if (this.refreshing && !forceUpdate) { return; } // Refresh data only when needed if (forceUpdate || this.refreshRequired) { try { this.refreshing = true; // TODO: Should be AxiosInstance const updates: Promise[] = []; updates.push(this.getState()); updates.push(this.getPerformance()); updates.push(this.getProfit()); updates.push(this.getTrades()); updates.push(this.getBalance()); // /* white/blacklist might be refreshed more often as they are not expensive on the backend */ updates.push(this.getWhitelist()); updates.push(this.getBlacklist()); await Promise.all(updates); this.refreshRequired = false; } finally { this.refreshing = false; } } return Promise.resolve(); }, async refreshFrequent() { // Refresh data that's needed in near realtime await this.getOpenTrades(); await this.getLocks(); }, setDetailTrade(trade: Trade | null) { this.detailTradeId = trade?.trade_id || null; this.selectedPair = trade ? trade.pair : this.selectedPair; }, async getTrades() { try { let totalTrades = 0; const pageLength = 500; const fetchTrades = async (limit: number, offset: number) => { return api.get('/trades', { params: { limit, offset }, }); }; const res = await fetchTrades(pageLength, 0); const result: TradeResponse = res.data; let { trades } = result; if (Array.isArray(trades)) { if (trades.length !== result.total_trades) { // Pagination necessary // Don't use Promise.all - this would fire all requests at once, which can // cause problems for big sqlite databases do { // eslint-disable-next-line no-await-in-loop const res = await fetchTrades(pageLength, trades.length); const result: TradeResponse = res.data; trades = trades.concat(result.trades); totalTrades = res.data.total_trades; } while (trades.length !== totalTrades); } const tradesCount = trades.length; // Add botId to all trades trades = trades.map((t) => ({ ...t, botId, botName, botTradeId: `${botId}__${t.trade_id}`, })); this.trades = trades; this.tradeCount = tradesCount; } return Promise.resolve(); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } return Promise.reject(error); } }, async getOpenTrades() { try { const { data } = await api.get>('/status'); // Check if trade-id's are different in this call, then trigger a full refresh if ( Array.isArray(this.openTrades) && Array.isArray(data) && (this.openTrades.length !== data.length || !this.openTrades.every((val, index) => val.trade_id === data[index].trade_id)) ) { // Open trades changed, so we should refresh now. this.refreshRequired = true; this.refreshSlow(false); } if (Array.isArray(data)) { const openTrades = data.map((t) => ({ ...t, botId, botName, botTradeId: `${botId}__${t.trade_id}`, profit_ratio: t.profit_ratio ?? -1, })); // TODO Don't force-patch profit_ratio but handle null values properly this.openTrades = openTrades; if (this.selectedPair === '') { this.selectedPair = openTrades[0]?.pair || ''; } } } catch (data) { return console.error(data); } }, getLocks() { return api .get('/locks') .then((result) => (this.currentLocks = result.data)) .catch(console.error); }, async deleteLock(lockid: string) { try { const res = await api.delete(`/locks/${lockid}`); showAlert(`Deleted Lock ${lockid}.`); this.currentLocks = res.data; return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert(`Failed to delete lock ${lockid}`, 'danger'); return Promise.reject(error); } }, getPairCandles(payload: PairCandlePayload) { if (payload.pair && payload.timeframe) { this.candleDataStatus = LoadingStatus.loading; return api .get('/pair_candles', { params: { ...payload }, }) .then((result) => { this.candleData = { ...this.candleData, [`${payload.pair}__${payload.timeframe}`]: { pair: payload.pair, timeframe: payload.timeframe, data: result.data, }, }; this.candleDataStatus = LoadingStatus.success; }) .catch((err) => { console.error(err); this.candleDataStatus = LoadingStatus.error; }); } // Error branchs const error = 'pair or timeframe not specified'; console.error(error); return new Promise((resolve, reject) => { reject(error); }); }, getPairHistory(payload: PairHistoryPayload) { if (payload.pair && payload.timeframe) { this.historyStatus = LoadingStatus.loading; return api .get('/pair_history', { params: { ...payload }, timeout: 50000, }) .then((result) => { this.history = { [`${payload.pair}__${payload.timeframe}`]: { pair: payload.pair, timeframe: payload.timeframe, timerange: payload.timerange, data: result.data, }, }; this.historyStatus = LoadingStatus.success; }) .catch((err) => { console.error(err); this.historyStatus = LoadingStatus.error; }); } // Error branchs const error = 'pair or timeframe or timerange not specified'; console.error(error); return new Promise((resolve, reject) => { reject(error); }); }, async getStrategyPlotConfig() { try { const payload = {}; if (this.isWebserverMode) { if (!this.strategy.strategy) { return Promise.reject({ data: 'No strategy selected' }); } payload['strategy'] = this.strategy.strategy; } const { data: plotConfig } = await api.get('/plot_config', { params: { ...payload }, }); if (plotConfig.subplots === null) { // Subplots should not be null but an empty object // TODO: Remove this fix when fix in freqtrade is populated further. plotConfig.subplots = {}; } this.strategyPlotConfig = plotConfig; return Promise.resolve(); } catch (data) { console.error(data); return Promise.reject(data); } }, async getStrategyList() { try { const { data } = await api.get('/strategies'); this.strategyList = data.strategies; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getStrategy(strategy: string) { try { const { data } = await api.get(`/strategy/${strategy}`, {}); this.strategy = data; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getFreqAIModelList() { try { const { data } = await api.get('/freqaimodels'); this.freqaiModelList = data.freqaimodels; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getExchangeList() { try { const { data } = await api.get('/exchanges'); this.exchangeList = data.exchanges; return Promise.resolve(data.exchanges); } catch (error) { console.error(error); return Promise.reject(error); } }, async getAvailablePairs(payload: AvailablePairPayload) { try { const { data } = await api.get('/available_pairs', { params: { ...payload }, }); // result is of type AvailablePairResult this.pairlist = data.pairs; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getPerformance() { try { const { data } = await api.get('/performance'); this.performanceStats = data; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getWhitelist() { try { const { data } = await api.get('/whitelist'); this.whitelist = data.whitelist; this.pairlistMethods = data.method; return Promise.resolve(data); } catch (error) { return Promise.reject(error); } }, async getBlacklist() { try { const { data } = await api.get('/blacklist'); this.blacklist = data.blacklist; return Promise.resolve(data); } catch (error) { return Promise.reject(error); } }, async getProfit() { try { const { data } = await api.get('/profit'); this.profit = data; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getBalance() { try { const { data } = await api.get('/balance'); this.balance = data; return Promise.resolve(data); } catch (error) { return Promise.reject(error); } }, async getDaily(payload: DailyPayload = {}) { const { timescale = 20 } = payload; try { const { data } = await api.get('/daily', { params: { timescale } }); this.dailyStats = data; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getState() { try { const { data } = await api.get('/show_config'); this.botState = data; this.botStatusAvailable = true; this.startWebSocket(); return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getLogs() { try { const { data } = await api.get('/logs'); this.lastLogs = data.logs; return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getPairlists() { try { const { data } = await api.get('/pairlists/available'); return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async evaluatePairlist(payload: PairlistsPayload) { try { const { data } = await api.post>( '/pairlists/evaluate', payload, ); return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getPairlistEvalResult(jobId: string) { try { const { data } = await api.get(`/pairlists/evaluate/${jobId}`); return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, async getBackgroundJobStatus(jobId: string) { try { const { data } = await api.get(`/background/${jobId}`); return Promise.resolve(data); } catch (error) { console.error(error); return Promise.reject(error); } }, // // Post methods // // TODO: Migrate calls to API to a seperate module unrelated to pinia? async startBot() { try { const { data } = await api.post, AxiosResponse>( '/start', {}, ); console.log(data); showAlert(data.status); return Promise.resolve(data); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert('Error starting bot.', 'danger'); return Promise.reject(error); } }, async stopBot() { try { const res = await api.post, AxiosResponse>( '/stop', {}, ); showAlert(res.data.status); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert('Error stopping bot.', 'danger'); return Promise.reject(error); } }, async stopBuy() { try { const res = await api.post, AxiosResponse>( '/stopbuy', {}, ); showAlert(res.data.status); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert('Error calling stopbuy.', 'danger'); return Promise.reject(error); } }, async reloadConfig() { try { const res = await api.post, AxiosResponse>( '/reload_config', {}, ); console.log(res.data); showAlert(res.data.status); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert('Error reloading.', 'danger'); return Promise.reject(error); } }, async deleteTrade(tradeid: string) { try { const res = await api.delete(`/trades/${tradeid}`); showAlert(res.data.result_msg ? res.data.result_msg : `Deleted Trade ${tradeid}`); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert(`Failed to delete trade ${tradeid}`, 'danger'); return Promise.reject(error); } }, async cancelOpenOrder(tradeid: string) { try { const res = await api.delete(`/trades/${tradeid}/open-order`); showAlert( res.data.result_msg ? res.data.result_msg : `Canceled open order for ${tradeid}`, ); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert(`Failed to cancel open order ${tradeid}`, 'danger'); return Promise.reject(error); } }, async reloadTrade(tradeid: string) { try { const res = await api.post>(`/trades/${tradeid}/reload`); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert(`Failed to reload trade ${tradeid}`, 'danger'); return Promise.reject(error); } }, async startTrade() { try { const res = await api.post('/start_trade', {}); return Promise.resolve(res); } catch (error) { return Promise.reject(error); } }, async forceexit(payload: ForceSellPayload) { try { const res = await api.post>( '/forcesell', payload, ); showAlert(`Exit order for ${payload.tradeid} created`, 'success'); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); } showAlert(`Failed to create exit order for ${payload.tradeid}`, 'danger'); return Promise.reject(error); } }, async forceentry(payload: ForceEnterPayload) { if (payload && payload.pair) { try { // TODO: Update forcebuy to forceenter ... const res = await api.post< ForceEnterPayload, AxiosResponse >('/forcebuy', payload); showAlert(`Order for ${payload.pair} created.`, 'success'); return Promise.resolve(res); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); showAlert(`Error occured entering: '${error.response?.data?.error}'`, 'danger'); } return Promise.reject(error); } } // Error branchs const error = 'Pair is empty'; console.error(error); return Promise.reject(error); }, async addBlacklist(payload: BlacklistPayload) { console.log(`Adding ${payload} to blacklist`); if (payload && payload.blacklist) { try { const result = await api.post>( '/blacklist', payload, ); this.blacklist = result.data.blacklist; if (result.data.errors && Object.keys(result.data.errors).length !== 0) { const { errors } = result.data; Object.keys(errors).forEach((pair) => { showAlert( `Error while adding pair ${pair} to Blacklist: ${errors[pair].error_msg}`, 'danger', ); }); } else { showAlert(`Pair ${payload.blacklist} added.`); } return Promise.resolve(result.data); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); showAlert( `Error occured while adding pairs to Blacklist: '${error.response?.data?.error}'`, 'danger', ); } return Promise.reject(error); } } // Error branchs const error = 'Pair is empty'; console.error(error); return Promise.reject(error); }, async deleteBlacklist(blacklistPairs: Array) { console.log(`Deleting ${blacklistPairs} from blacklist.`); if (blacklistPairs) { try { const result = await api.delete>( '/blacklist', { params: { pairs_to_delete: blacklistPairs, }, paramsSerializer: { indexes: null, }, }, ); this.blacklist = result.data.blacklist; if (result.data.errors && Object.keys(result.data.errors).length !== 0) { const { errors } = result.data; Object.keys(errors).forEach((pair) => { showAlert( `Error while removing pair ${pair} from Blacklist: ${errors[pair].error_msg}`, 'danger', ); }); } else { showAlert(`Pair ${blacklistPairs} removed.`); } return Promise.resolve(result.data); } catch (error) { if (axios.isAxiosError(error)) { console.error(error.response); showAlert( `Error occured while removing pairs from Blacklist: '${error.response?.data?.error}'`, 'danger', ); } return Promise.reject(error); } } // Error branchs const error = 'Pair is empty'; console.error(error); return Promise.reject(error); }, async startBacktest(payload: BacktestPayload) { try { const result = await api.post>( '/backtest', payload, ); this.updateBacktestRunning(result.data); } catch (err) { console.log(err); } }, async pollBacktest() { const { data } = await api.get('/backtest'); this.updateBacktestRunning(data); if (data.running === false && data.backtest_result) { this.updateBacktestResult(data.backtest_result); } if (data.status === 'error') { showAlert(`Backtest failed: ${data.status_msg}.`, 'danger'); } }, async removeBacktest() { this.backtestHistory = {}; try { const { data } = await api.delete('/backtest'); this.updateBacktestRunning(data); return Promise.resolve(data); } catch (err) { return Promise.reject(err); } }, updateBacktestRunning(backtestStatus: BacktestStatus) { this.backtestRunning = backtestStatus.running; this.backtestProgress = backtestStatus.progress; this.backtestStep = backtestStatus.step; this.backtestTradeCount = backtestStatus.trade_count || 0; }, async stopBacktest() { try { const { data } = await api.get('/backtest/abort'); this.updateBacktestRunning(data); return Promise.resolve(data); } catch (err) { return Promise.reject(err); } }, async getBacktestHistory() { const { data } = await api.get('/backtest/history'); this.backtestHistoryList = data; }, updateBacktestResult(backtestResult: BacktestResult) { this.backtestResult = backtestResult; // TODO: Properly identify duplicates to avoid pushing the same multiple times Object.entries(backtestResult.strategy).forEach(([key, strat]) => { console.log(key, strat); const stratKey = `${key}_${strat.total_trades}_${strat.profit_total.toFixed(3)}`; // this.backtestHistory[stratKey] = strat; this.backtestHistory = { ...this.backtestHistory, ...{ [stratKey]: strat } }; this.selectedBacktestResultKey = stratKey; }); }, async getBacktestHistoryResult(payload: BacktestHistoryEntry) { const result = await api.get('/backtest/history/result', { params: { filename: payload.filename, strategy: payload.strategy }, }); if (result.data.backtest_result) { this.updateBacktestResult(result.data.backtest_result); } }, async deleteBacktestHistoryResult(btHistoryEntry: BacktestHistoryEntry) { try { const { data } = await api.delete( `/backtest/history/${btHistoryEntry.filename}`, ); this.backtestHistoryList = data; } catch (err) { console.error(err); return Promise.reject(err); } }, setBacktestResultKey(key: string) { this.selectedBacktestResultKey = key; }, removeBacktestResultFromMemory(key: string) { if (this.selectedBacktestResultKey === key) { // Get first key from backtestHistory that is not the key to be deleted const keys = Object.keys(this.backtestHistory); const index = keys.findIndex((k) => k !== key); if (index !== -1) { this.selectedBacktestResultKey = keys[index]; } } delete this.backtestHistory[key]; }, async getSysInfo() { try { const { data } = await api.get('/sysinfo'); this.sysInfo = data; return Promise.resolve(data); } catch (err) { return Promise.reject(err); } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any _handleWebsocketMessage(ws, event: MessageEvent) { const msg: FTWsMessage = JSON.parse(event.data); switch (msg.type) { case FtWsMessageTypes.exception: showAlert(`WSException: ${msg.data}`, 'danger'); break; case FtWsMessageTypes.whitelist: this.whitelist = msg.data; break; case FtWsMessageTypes.entryFill: case FtWsMessageTypes.exitFill: case FtWsMessageTypes.exitCancel: case FtWsMessageTypes.entryCancel: showNotification(msg, botName); break; case FtWsMessageTypes.newCandle: { const [pair, timeframe] = msg.data; // TODO: check for active bot ... if (pair === this.selectedPair) { // Reload pair candles this.getPairCandles({ pair, timeframe, limit: 500 }); } break; } default: // Unhandled events ... // eslint-disable-next-line @typescript-eslint/no-explicit-any console.log(`Received event ${(msg as any).type}`); break; } }, startWebSocket() { if ( this.websocketStarted === true || this.botStatusAvailable === false || this.botApiVersion < 2.2 || this.isWebserverMode === true ) { return; } const { send, close } = useWebSocket( // 'ws://localhost:8080/api/v1/message/ws?token=testtoken', `${userService.getBaseWsUrl()}/message/ws?token=${userService.getAccessToken()}`, { autoReconnect: { delay: 10000, // retries: 10 }, // heartbeat: { // message: JSON.stringify({ type: 'ping' }), // interval: 10000, // }, onError: (ws, event) => { console.log('onError', event, ws); this.websocketStarted = false; close(); }, onMessage: this._handleWebsocketMessage, onConnected: () => { console.log('subscribing'); if (this.isWebserverMode !== true) { // this.websocketStarted = true; const subscriptions = [ FtWsMessageTypes.whitelist, FtWsMessageTypes.entryFill, FtWsMessageTypes.exitFill, FtWsMessageTypes.entryCancel, FtWsMessageTypes.exitCancel, /*'new_candle' /*'analyzed_df'*/ ]; if (this.botApiVersion >= 2.21) { subscriptions.push(FtWsMessageTypes.newCandle); } send( JSON.stringify({ type: 'subscribe', data: subscriptions, }), ); send( JSON.stringify({ type: FtWsMessageTypes.whitelist, data: '', }), ); } }, }, ); }, }, }); return useBotStore(); }