diff --git a/package.json b/package.json index db0e52ee..246fcea8 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@popperjs/core": "^2.11.6", + "@vueuse/core": "^9.1.1", "axios": "^1.2.1", "bootstrap": "^5.2.3", "bootstrap-vue-3": "^0.4.11", diff --git a/src/components/charts/ProfitDistributionChart.vue b/src/components/charts/ProfitDistributionChart.vue index 69b75ea2..2c33462b 100644 --- a/src/components/charts/ProfitDistributionChart.vue +++ b/src/components/charts/ProfitDistributionChart.vue @@ -12,7 +12,12 @@ content-cols="6" size="sm" > - + @@ -67,11 +72,10 @@ export default defineComponent({ // console.log(profits); // const data = [[]]; const binOptions = [10, 15, 20, 25, 50]; - const bins = ref(20); const data = computed(() => { const profits = props.trades.map((trade) => trade.profit_ratio); - return binData(profits, bins.value); + return binData(profits, settingsStore.profitDistributionBins); }); const chartOptions = computed((): EChartsOption => { @@ -137,7 +141,7 @@ export default defineComponent({ return chartOptionsLoc; }); console.log(chartOptions); - return { settingsStore, chartOptions, bins, binOptions }; + return { settingsStore, chartOptions, binOptions }; }, }); diff --git a/src/shared/notifications.ts b/src/shared/notifications.ts new file mode 100644 index 00000000..3879da2c --- /dev/null +++ b/src/shared/notifications.ts @@ -0,0 +1,35 @@ +import { showAlert } from '@/stores/alerts'; +import { useSettingsStore } from '@/stores/settings'; +import { FTWsMessage, FtWsMessageTypes } from '@/types/wsMessageTypes'; + +export function showNotification(msg: FTWsMessage, botname: string) { + const settingsStore = useSettingsStore(); + if (settingsStore.notifications && settingsStore.notifications[msg.type]) { + switch (msg.type) { + case FtWsMessageTypes.entryFill: + console.log('entryFill', msg); + showAlert(`${botname}: Entry fill for ${msg.pair} at ${msg.open_rate}`, 'success'); + break; + case FtWsMessageTypes.exitFill: + console.log('exitFill', msg); + showAlert(`${botname}: Exit fill for ${msg.pair} at ${msg.open_rate}`, 'success'); + break; + case FtWsMessageTypes.exitCancel: + console.log('exitCancel', msg); + showAlert( + `${botname}: Exit order cancelled for ${msg.pair} due to ${msg.reason}`, + 'warning', + ); + break; + case FtWsMessageTypes.entryCancel: + console.log('entryCancel', msg); + showAlert( + `${botname}: Entry order cancelled for ${msg.pair} due to ${msg.reason}`, + 'warning', + ); + break; + } + } else { + console.log(`${botname}: Message ${msg.type} not shown.`); + } +} diff --git a/src/shared/userService.ts b/src/shared/userService.ts index 66b46d5a..18ef4dba 100644 --- a/src/shared/userService.ts +++ b/src/shared/userService.ts @@ -189,6 +189,17 @@ export class UserService { return `${baseURL}${APIBASE}`; } + public getBaseWsUrl(): string { + const baseUrl = this.getBaseUrl(); + if (baseUrl.startsWith('http://')) { + return baseUrl.replace('http://', 'ws://'); + } + if (baseUrl.startsWith('https://')) { + return baseUrl.replace('https://', 'wss://'); + } + return ''; + } + /** * Call on startup to migrate old login info to new login */ diff --git a/src/stores/alerts.ts b/src/stores/alerts.ts index 026d32dd..90fd6002 100644 --- a/src/stores/alerts.ts +++ b/src/stores/alerts.ts @@ -10,7 +10,7 @@ export const useAlertsStore = defineStore('alerts', { this.activeMessages.push(message); }, removeAlert(alert: AlertType) { - console.log('dismissed'); + console.log('dismissed', alert); this.activeMessages = this.activeMessages.filter((v) => v !== alert); }, }, diff --git a/src/stores/ftbot.ts b/src/stores/ftbot.ts index 665e8ea8..445bed1c 100644 --- a/src/stores/ftbot.ts +++ b/src/stores/ftbot.ts @@ -38,6 +38,9 @@ import { 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); @@ -46,6 +49,7 @@ export function createBotSubStore(botId: string, botName: string) { const useBotStore = defineStore(botId, { state: () => { return { + websocketStarted: false, isSelected: true, ping: '', botStatusAvailable: false, @@ -501,6 +505,7 @@ export function createBotSubStore(botId: string, botName: string) { const { data } = await api.get('/show_config'); this.botState = data; this.botStatusAvailable = true; + this.startWebSocket(); return Promise.resolve(data); } catch (error) { console.error(error); @@ -806,7 +811,93 @@ export function createBotSubStore(botId: string, botName: string) { return Promise.reject(err); } }, + _handleWebsocketMessage(ws, event: MessageEvent) { + const msg: FTWsMessage = JSON.parse(event.data); + switch (msg.type) { + 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: + console.log('exitFill', msg); + 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 ... + console.log(`Received event ${(msg as any).type}`); + break; + } + }, + startWebSocket() { + if ( + this.websocketStarted === true || + this.botStatusAvailable === false || + this.botApiVersion < 2.2 + ) { + return; + } + const { status, data, send, open, close, ws } = 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'); + 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(); } diff --git a/src/stores/settings.ts b/src/stores/settings.ts index cc39e50c..a2b40b10 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -3,6 +3,7 @@ import { defineStore } from 'pinia'; import { getCurrentTheme, getTheme } from '@/shared/themes'; import axios from 'axios'; import { UiVersion } from '@/types'; +import { FtWsMessageTypes } from '@/types/wsMessageTypes'; const STORE_UI_SETTINGS = 'ftUISettings'; @@ -12,11 +13,12 @@ export enum OpenTradeVizOptions { noOpenTrades = 'noOpenTrades', } -export interface SettingsType { - openTradesInTitle?: string; - timezone?: string; - backgroundSync?: boolean; -} +const notificationDefaults = { + [FtWsMessageTypes.entryFill]: true, + [FtWsMessageTypes.exitFill]: true, + [FtWsMessageTypes.entryCancel]: true, + [FtWsMessageTypes.exitCancel]: true, +}; export const useSettingsStore = defineStore('uiSettings', { // other options... @@ -28,6 +30,8 @@ export const useSettingsStore = defineStore('uiSettings', { currentTheme: getCurrentTheme(), uiVersion: 'dev', useHeikinAshiCandles: false, + notifications: notificationDefaults, + profitDistributionBins: 20, }; }, getters: { diff --git a/src/types/wsMessageTypes.ts b/src/types/wsMessageTypes.ts new file mode 100644 index 00000000..b17c9a76 --- /dev/null +++ b/src/types/wsMessageTypes.ts @@ -0,0 +1,65 @@ +export enum FtWsMessageTypes { + whitelist = 'whitelist', + entryFill = 'entry_fill', + entryCancel = 'entry_cancel', + + exitFill = 'exit_fill', + exitCancel = 'exit_cancel', + newCandle = 'new_candle', +} + +export interface FtBaseWsMessage { + type: FtWsMessageTypes; +} + +export interface FtWhitelistMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.whitelist; + data: string[]; +} + +export interface FtEntryFillMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.entryFill; + pair: string; + open_rate: number; + amount: number; + // ... +} + +export interface FtExitFillMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.exitFill; + pair: string; + open_rate: number; + amount: number; + // ... +} + +export interface FTEntryCancelMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.entryCancel; + pair: string; + reason: string; + direction: string; + // ... +} + +export interface FTExitCancelMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.exitCancel; + pair: string; + reason: string; + direction: string; + // ... +} + +export interface FtNewCandleMessage extends FtBaseWsMessage { + type: FtWsMessageTypes.newCandle; + /** Pair, timeframe, candletype*/ + data: [string, string, string]; + // ... +} + +export type FTWsMessage = + | FtWhitelistMessage + | FtEntryFillMessage + | FTEntryCancelMessage + | FtExitFillMessage + | FTExitCancelMessage + | FtNewCandleMessage; diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 577c5b96..3629cc0e 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -37,45 +37,47 @@ >Use Heikin Ashi candles. + + Entry notifications + Exit notifications + Entry Cancel notifications + Exit Cancel notifications + - diff --git a/yarn.lock b/yarn.lock index 3bb88e85..25b3ed9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/web-bluetooth@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72" + integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" @@ -1665,6 +1670,28 @@ resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.2.6.tgz#23d85b81d05be36f12aa802459a7876457dec795" integrity sha512-64zHtJZdG7V/U2L0j/z3Pt5bSygccI3xs+Kl7LB73AZK4MQ8WONJhqDQPK8leUFFA9CmmoJygeky7zcl2hX10A== +"@vueuse/core@^9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.1.1.tgz#a5c09c33ccee58cfd53bc3ec2d5a0d304155529e" + integrity sha512-QfuaNWRDMQcCUwXylCyYhPC3ScS9Tiiz4J0chdwr3vOemBwRToSywq8MP+ZegKYFnbETzRY8G/5zC+ca30wrRQ== + dependencies: + "@types/web-bluetooth" "^0.0.15" + "@vueuse/metadata" "9.1.1" + "@vueuse/shared" "9.1.1" + vue-demi "*" + +"@vueuse/metadata@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.1.1.tgz#b3fe4b97e62096f7566cd8eb107c503998b2c9a6" + integrity sha512-XZ2KtSW+85LLHB/IdGILPAtbIVHasPsAW7aqz3BRMzJdAQWRiM/FGa1OKBwLbXtUw/AmjKYFlZJo7eOFIBXRog== + +"@vueuse/shared@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.1.1.tgz#811f47629e281a19013ae6dcdf11ed3e1e91e023" + integrity sha512-c+IfcOYmHiHqoEa3ED1Tbpue5GHmoUmTp8PtO4YbczthtY155Rt6DmWhjxMLXBF1Bcidagxljmp/7xtAzEHXLw== + dependencies: + vue-demi "*" + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"