Merge pull request #1045 from freqtrade/websockets

Websockets
This commit is contained in:
Matthias 2022-12-08 14:09:14 +01:00 committed by GitHub
commit 36be5f9714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 278 additions and 38 deletions

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"@vueuse/core": "^9.1.1",
"axios": "^1.2.1", "axios": "^1.2.1",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"bootstrap-vue-3": "^0.4.11", "bootstrap-vue-3": "^0.4.11",

View File

@ -12,7 +12,12 @@
content-cols="6" content-cols="6"
size="sm" size="sm"
> >
<b-form-select id="input-bins" v-model="bins" size="sm" :options="binOptions"></b-form-select> <b-form-select
id="input-bins"
v-model="settingsStore.profitDistributionBins"
size="sm"
:options="binOptions"
></b-form-select>
</b-form-group> </b-form-group>
</div> </div>
</template> </template>
@ -67,11 +72,10 @@ export default defineComponent({
// console.log(profits); // console.log(profits);
// const data = [[]]; // const data = [[]];
const binOptions = [10, 15, 20, 25, 50]; const binOptions = [10, 15, 20, 25, 50];
const bins = ref<number>(20);
const data = computed(() => { const data = computed(() => {
const profits = props.trades.map((trade) => trade.profit_ratio); const profits = props.trades.map((trade) => trade.profit_ratio);
return binData(profits, bins.value); return binData(profits, settingsStore.profitDistributionBins);
}); });
const chartOptions = computed((): EChartsOption => { const chartOptions = computed((): EChartsOption => {
@ -137,7 +141,7 @@ export default defineComponent({
return chartOptionsLoc; return chartOptionsLoc;
}); });
console.log(chartOptions); console.log(chartOptions);
return { settingsStore, chartOptions, bins, binOptions }; return { settingsStore, chartOptions, binOptions };
}, },
}); });
</script> </script>

View File

@ -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.`);
}
}

View File

@ -189,6 +189,17 @@ export class UserService {
return `${baseURL}${APIBASE}`; 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 * Call on startup to migrate old login info to new login
*/ */

View File

@ -10,7 +10,7 @@ export const useAlertsStore = defineStore('alerts', {
this.activeMessages.push(message); this.activeMessages.push(message);
}, },
removeAlert(alert: AlertType) { removeAlert(alert: AlertType) {
console.log('dismissed'); console.log('dismissed', alert);
this.activeMessages = this.activeMessages.filter((v) => v !== alert); this.activeMessages = this.activeMessages.filter((v) => v !== alert);
}, },
}, },

View File

@ -38,6 +38,9 @@ import {
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { showAlert } from './alerts'; 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) { export function createBotSubStore(botId: string, botName: string) {
const userService = useUserService(botId); const userService = useUserService(botId);
@ -46,6 +49,7 @@ export function createBotSubStore(botId: string, botName: string) {
const useBotStore = defineStore(botId, { const useBotStore = defineStore(botId, {
state: () => { state: () => {
return { return {
websocketStarted: false,
isSelected: true, isSelected: true,
ping: '', ping: '',
botStatusAvailable: false, botStatusAvailable: false,
@ -501,6 +505,7 @@ export function createBotSubStore(botId: string, botName: string) {
const { data } = await api.get('/show_config'); const { data } = await api.get('/show_config');
this.botState = data; this.botState = data;
this.botStatusAvailable = true; this.botStatusAvailable = true;
this.startWebSocket();
return Promise.resolve(data); return Promise.resolve(data);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -806,7 +811,93 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(err); return Promise.reject(err);
} }
}, },
_handleWebsocketMessage(ws, event: MessageEvent<any>) {
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(); return useBotStore();
} }

View File

@ -3,6 +3,7 @@ import { defineStore } from 'pinia';
import { getCurrentTheme, getTheme } from '@/shared/themes'; import { getCurrentTheme, getTheme } from '@/shared/themes';
import axios from 'axios'; import axios from 'axios';
import { UiVersion } from '@/types'; import { UiVersion } from '@/types';
import { FtWsMessageTypes } from '@/types/wsMessageTypes';
const STORE_UI_SETTINGS = 'ftUISettings'; const STORE_UI_SETTINGS = 'ftUISettings';
@ -12,11 +13,12 @@ export enum OpenTradeVizOptions {
noOpenTrades = 'noOpenTrades', noOpenTrades = 'noOpenTrades',
} }
export interface SettingsType { const notificationDefaults = {
openTradesInTitle?: string; [FtWsMessageTypes.entryFill]: true,
timezone?: string; [FtWsMessageTypes.exitFill]: true,
backgroundSync?: boolean; [FtWsMessageTypes.entryCancel]: true,
} [FtWsMessageTypes.exitCancel]: true,
};
export const useSettingsStore = defineStore('uiSettings', { export const useSettingsStore = defineStore('uiSettings', {
// other options... // other options...
@ -28,6 +30,8 @@ export const useSettingsStore = defineStore('uiSettings', {
currentTheme: getCurrentTheme(), currentTheme: getCurrentTheme(),
uiVersion: 'dev', uiVersion: 'dev',
useHeikinAshiCandles: false, useHeikinAshiCandles: false,
notifications: notificationDefaults,
profitDistributionBins: 20,
}; };
}, },
getters: { getters: {

View File

@ -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;

View File

@ -37,45 +37,47 @@
>Use Heikin Ashi candles.</b-form-checkbox >Use Heikin Ashi candles.</b-form-checkbox
> >
</b-form-group> </b-form-group>
<b-form-group description="Notifications">
<b-form-checkbox v-model="settingsStore.notifications[FtWsMessageTypes.entryFill]"
>Entry notifications</b-form-checkbox
>
<b-form-checkbox v-model="settingsStore.notifications[FtWsMessageTypes.exitFill]"
>Exit notifications</b-form-checkbox
>
<b-form-checkbox v-model="settingsStore.notifications[FtWsMessageTypes.entryCancel]"
>Entry Cancel notifications</b-form-checkbox
>
<b-form-checkbox v-model="settingsStore.notifications[FtWsMessageTypes.exitCancel]"
>Exit Cancel notifications</b-form-checkbox
>
</b-form-group>
</div> </div>
</b-card> </b-card>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import { OpenTradeVizOptions, useSettingsStore } from '@/stores/settings'; import { OpenTradeVizOptions, useSettingsStore } from '@/stores/settings';
import { useLayoutStore } from '@/stores/layout'; import { useLayoutStore } from '@/stores/layout';
import { showAlert } from '@/stores/alerts'; import { showAlert } from '@/stores/alerts';
import { FtWsMessageTypes } from '@/types/wsMessageTypes';
export default defineComponent({ const settingsStore = useSettingsStore();
name: 'Settings', const layoutStore = useLayoutStore();
setup() {
const settingsStore = useSettingsStore();
const layoutStore = useLayoutStore();
const timezoneOptions = ['UTC', Intl.DateTimeFormat().resolvedOptions().timeZone]; const timezoneOptions = ['UTC', Intl.DateTimeFormat().resolvedOptions().timeZone];
const openTradesOptions = [ const openTradesOptions = [
{ value: OpenTradeVizOptions.showPill, text: 'Show pill in icon' }, { value: OpenTradeVizOptions.showPill, text: 'Show pill in icon' },
{ value: OpenTradeVizOptions.asTitle, text: 'Show in title' }, { value: OpenTradeVizOptions.asTitle, text: 'Show in title' },
{ value: OpenTradeVizOptions.noOpenTrades, text: "Don't show open trades in header" }, { value: OpenTradeVizOptions.noOpenTrades, text: "Don't show open trades in header" },
]; ];
// //
const resetDynamicLayout = () => { const resetDynamicLayout = () => {
layoutStore.resetTradingLayout(); layoutStore.resetTradingLayout();
layoutStore.resetDashboardLayout(); layoutStore.resetDashboardLayout();
showAlert('Layouts have been reset.'); showAlert('Layouts have been reset.');
}; };
return {
resetDynamicLayout,
settingsStore,
layoutStore,
timezoneOptions,
openTradesOptions,
};
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1228,6 +1228,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== 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@*": "@types/yargs-parser@*":
version "20.2.1" version "20.2.1"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" 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" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.2.6.tgz#23d85b81d05be36f12aa802459a7876457dec795"
integrity sha512-64zHtJZdG7V/U2L0j/z3Pt5bSygccI3xs+Kl7LB73AZK4MQ8WONJhqDQPK8leUFFA9CmmoJygeky7zcl2hX10A== 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: abab@^2.0.3, abab@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"