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"