added pairlist configurator

This commit is contained in:
Tako 2023-05-23 18:27:33 +00:00
parent d4c6815eca
commit ac78a18414
8 changed files with 440 additions and 1 deletions

View File

@ -0,0 +1,68 @@
<template>
<b-card no-body class="mb-2">
<template #header>
<b-row align-v="center">
<b-col>
<b-row align-v="center">
<b-col cols="auto"><ReorderIcon class="handle me-2" /></b-col>
<b-col
><b-button variant="link" @click="visible = !visible">
<h6 class="mb-0">{{ pairlist.name }}</h6>
</b-button></b-col
>
</b-row>
</b-col>
<b-col cols="auto">
<b-button size="sm" @click="emit('remove', index)">x</b-button>
</b-col>
</b-row>
</template>
<b-card-body :class="{ hidden: !visible }">
<b-collapse v-model="visible">
<PairlistConfigParameter
v-for="(parameter, key) in pairlist.params"
:key="key"
v-model="pairlist.params[key].value"
:param="parameter"
/>
</b-collapse>
</b-card-body>
</b-card>
</template>
<script setup lang="ts">
import { Pairlist } from '@/types';
import { computed, ref } from 'vue';
import PairlistConfigParameter from '../general/PairlistConfigParameter.vue';
import ReorderIcon from 'vue-material-design-icons/ReorderHorizontal.vue';
const props = defineProps<{
modelValue: Pairlist;
index: number;
}>();
const emit = defineEmits(['update:modelValue', 'remove']);
const visible = ref(false);
const pairlist = computed({
get() {
return props.modelValue;
},
set(value: Pairlist) {
emit('update:modelValue', value);
},
});
</script>
<style>
.hidden {
padding: 0;
}
.collapsing {
-webkit-transition: none;
transition: none;
display: none;
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<b-modal
v-model="visible"
:scrollable="true"
:no-close-on-backdrop="true"
ok-title="Save"
title="Pairlist configurator"
@ok="save"
@show="configureConfig"
>
<div class="mb-2">
<b-form-input v-model="config.name" placeholder="Configuration name..."></b-form-input>
</div>
<b-form-select
v-model="selectedPairlist"
:options="pairlistSelectOptions"
class="mb-2"
@input="addToConfig"
></b-form-select>
<div ref="pairlistConfigsEl">
<PairlistConfigItem
v-for="(pairlist, i) in pairlists"
:key="pairlist.id"
v-model="config.pairlists[i]"
:index="i"
@remove="removeFromConfig"
/>
</div>
</b-modal>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, toRaw } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import PairlistConfigItem from './PairlistConfigItem.vue';
import { Pairlist, PairlistConfig } from '@/types';
import { useSortable, moveArrayElement } from '@vueuse/integrations/useSortable';
const emit = defineEmits(['update:modelValue', 'saveConfig']);
const props = defineProps<{
modelValue: boolean;
config: PairlistConfig;
}>();
const botStore = useBotStore();
const availablePairlists = ref<Pairlist[]>([]);
const selectedPairlist = ref<Pairlist>({} as Pairlist);
const config = ref<PairlistConfig>(props.config);
const pairlistConfigsEl = ref<HTMLElement | null>(null);
const visible = computed({
get() {
return props.modelValue;
},
set(value: boolean) {
emit('update:modelValue', value);
},
});
// v-for updates with sorting, deleting and adding items seem to get wonky without unique keys for every item
const pairlists = computed(() =>
config.value.pairlists.map((p) => {
if (p.id) {
return p;
} else {
return { id: Date.now().toString(36) + Math.random().toString(36).substring(2), ...p };
}
}),
);
const pairlistSelectOptions = computed(() => {
const pairlists = availablePairlists.value.map((p) => {
return { value: p, text: p.name };
});
return [{ text: 'Select a pairlist....', value: {} }, ...pairlists];
});
useSortable(pairlistConfigsEl, config.value.pairlists, {
handle: '.handle',
onUpdate: async (e) => {
moveArrayElement(config.value.pairlists, e.oldIndex, e.newIndex);
},
});
onMounted(async () => {
availablePairlists.value = (await botStore.activeBot.getPairlists()).pairlists;
});
const addToConfig = () => {
if (selectedPairlist.value) {
const pairlist = structuredClone(toRaw(selectedPairlist.value));
for (const param in pairlist.params) {
pairlist.params[param].value = pairlist.params[param].default
? pairlist.params[param].default.toString()
: '';
}
config.value.pairlists.push(pairlist);
selectedPairlist.value = {} as Pairlist;
}
};
const removeFromConfig = (index: number) => {
config.value.pairlists.splice(index, 1);
};
const save = async (e: Event) => {
e.preventDefault();
emit('saveConfig', config.value);
visible.value = false;
};
const configureConfig = () => {
config.value = structuredClone(toRaw(props.config));
};
</script>

View File

@ -0,0 +1,51 @@
<template>
<b-form-group
label-cols="4"
label-cols-lg="6"
label-size="md"
class="pb-1"
:description="param.help"
>
<b-form-input
v-if="param.type === PairlistParamType.string || param.type === PairlistParamType.number"
v-model="paramValue"
size="sm"
></b-form-input>
<b-form-checkbox
v-if="param.type === PairlistParamType.boolean"
v-model="paramValue"
></b-form-checkbox>
<b-form-select
v-if="param.type === PairlistParamType.option"
v-model="paramValue"
:options="param.options"
></b-form-select>
<template #label>
<label> {{ param.description }}</label>
</template>
</b-form-group>
</template>
<script setup lang="ts">
import { PairlistParameter, PairlistParamType } from '@/types';
import { computed } from 'vue';
const props = defineProps<{
param: PairlistParameter;
modelValue: string | undefined;
}>();
const emit = defineEmits(['update:modelValue']);
const paramValue = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
</script>

View File

@ -23,6 +23,9 @@
<router-link v-if="botStore.canRunBacktest" class="nav-link navbar-nav" to="/backtest" <router-link v-if="botStore.canRunBacktest" class="nav-link navbar-nav" to="/backtest"
>Backtest</router-link >Backtest</router-link
> >
<router-link class="nav-link navbar-nav" to="/pairlist_config"
>Pairlist Config</router-link
>
<theme-select /> <theme-select />
</b-navbar-nav> </b-navbar-nav>

View File

@ -67,6 +67,11 @@ const routes: Array<RouteRecordRaw> = [
allowAnonymous: true, allowAnonymous: true,
}, },
}, },
{
path: '/pairlist_config',
name: 'Pairlist Configuration',
component: () => import('@/views/PairlistConfigView.vue'),
},
{ {
path: '/(.*)*', path: '/(.*)*',
name: '404', name: '404',

View File

@ -42,7 +42,12 @@ import { showAlert } from './alerts';
import { useWebSocket } from '@vueuse/core'; import { useWebSocket } from '@vueuse/core';
import { FTWsMessage, FtWsMessageTypes } from '@/types/wsMessageTypes'; import { FTWsMessage, FtWsMessageTypes } from '@/types/wsMessageTypes';
import { showNotification } from '@/shared/notifications'; import { showNotification } from '@/shared/notifications';
import { FreqAIModelListResult } from '../types/types'; import {
FreqAIModelListResult,
PairlistEvalResponse,
PairlistsPayload,
PairlistsResponse,
} from '../types/types';
export function createBotSubStore(botId: string, botName: string) { export function createBotSubStore(botId: string, botName: string) {
const userService = useUserService(botId); const userService = useUserService(botId);
@ -535,6 +540,24 @@ export function createBotSubStore(botId: string, botName: string) {
return Promise.reject(error); return Promise.reject(error);
} }
}, },
async getPairlists() {
try {
const { data } = await api.get<PairlistsResponse>('/pairlists/available');
return Promise.resolve(data);
} catch (error) {
console.error(error);
return Promise.reject(error);
}
},
async getPairlistEvalStatus() {
try {
const { data } = await api.get<PairlistEvalResponse>('/pairlists/evaluate');
return Promise.resolve(data);
} catch (error) {
console.error(error);
return Promise.reject(error);
}
},
// // Post methods // // Post methods
// // TODO: Migrate calls to API to a seperate module unrelated to pinia? // // TODO: Migrate calls to API to a seperate module unrelated to pinia?
async startBot() { async startBot() {
@ -938,6 +961,21 @@ export function createBotSubStore(botId: string, botName: string) {
}, },
); );
}, },
async evaluatePairlist(payload: PairlistsPayload) {
try {
const { data } = await api.post<WhitelistResponse, AxiosResponse<StatusResponse>>(
'/pairlists/evaluate',
payload,
);
return Promise.resolve(data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error(error.response);
}
showAlert('Error testing pairlist', 'danger');
return Promise.reject(error);
}
},
}, },
}); });

View File

@ -271,3 +271,52 @@ export enum LoadingStatus {
success, success,
error, error,
} }
export interface PairlistsResponse {
pairlists: Pairlist[];
}
export interface PairlistEvalResponse {
detail?: string;
method?: string[];
whitelist?: string[];
}
export interface Pairlist {
id?: string;
is_pairlist_generator: boolean;
name: string;
params: Record<string, PairlistParameter>;
}
export interface PairlistConfig {
name: string;
pairlists: Pairlist[];
}
export enum PairlistParamType {
string = 'string',
number = 'number',
boolean = 'boolean',
option = 'option',
}
export interface PairlistParameter {
description: string;
help: string;
type: PairlistParamType;
value?: string;
default: string;
options?: string[];
}
export interface PairlistPayloadItem {
method: string;
[key: string]: string | number | boolean;
}
export interface PairlistsPayload {
pairlists: PairlistPayloadItem[];
blacklist: string[];
stake_currency: string;
}

View File

@ -0,0 +1,108 @@
<template>
<div>
<div>
<b-form-select v-model="selectedConfig" :options="configsSelectOptions"></b-form-select>
<b-button @click="showConfigurator = !showConfigurator">Configure</b-button>
<b-button :disabled="evaluating" @click="test">Test</b-button>
<PairlistConfigurator
v-model="showConfigurator"
:config="selectedConfig"
@save-config="addConfig"
/>
</div>
<p>{{ progressMessage }}</p>
<div class="mt-4">{{ whitelist }}</div>
</div>
</template>
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { ref, computed } from 'vue';
import PairlistConfigurator from '@/components/ftbot/PairlistConfigurator.vue';
import { PairlistConfig, PairlistParamType, PairlistPayloadItem } from '@/types';
const botStore = useBotStore();
const showConfigurator = ref(false);
const configs = ref<PairlistConfig[]>([]);
const selectedConfig = ref<PairlistConfig>({ name: '', pairlists: [] });
const whitelist = ref<string[]>([]);
const evaluating = ref(false);
const progressMessage = ref('');
const addConfig = (config: PairlistConfig) => {
const i = configs.value.findIndex((c) => c.name === config.name);
if (i > -1) {
configs.value[i] = config;
} else {
configs.value.push(config);
}
selectedConfig.value = config;
};
const configsSelectOptions = computed(() => {
const options = configs.value.map((c) => {
return { value: c, text: c.name };
});
const val: PairlistConfig = {
name: '',
pairlists: [],
};
return [{ text: 'New config...', value: val }, ...options];
});
const test = async () => {
if (!selectedConfig.value) return;
const pairlists: PairlistPayloadItem[] = [];
selectedConfig.value.pairlists.forEach((config) => {
const pairlist = {
method: config.name,
};
for (const key in config.params) {
const param = config.params[key];
if (param.value) {
pairlist[key] = convertToParamType(param.type, param.value);
}
}
pairlists.push(pairlist);
});
const payload = {
pairlists: pairlists,
stake_currency: botStore.activeBot.stakeCurrency,
blacklist: [],
};
evaluating.value = true;
whitelist.value = [];
const res = await botStore.activeBot.evaluatePairlist(payload);
console.log(res);
const evalIntervalId = setInterval(async () => {
const res = await botStore.activeBot.getPairlistEvalStatus();
if (res.whitelist) {
whitelist.value = res.whitelist;
clearInterval(evalIntervalId);
evaluating.value = false;
progressMessage.value = '';
} else if (res.detail) {
progressMessage.value = res.detail;
}
}, 1000);
};
const convertToParamType = (type: PairlistParamType, value: string) => {
if (type === PairlistParamType.number) {
return Number(value);
} else if (type === PairlistParamType.boolean) {
return Boolean(value);
} else {
return String(value);
}
};
</script>