Merge pull request #1265 from Tako88/feat/pairlistconfig

Pairlist configuration
This commit is contained in:
Matthias 2023-06-15 20:01:57 +02:00 committed by GitHub
commit 1dc608c87c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1061 additions and 3 deletions

View File

@ -0,0 +1,84 @@
<template>
<div class="w-100 d-flex">
<b-form-select
id="exchange-select"
v-model="exchangeModel.exchange"
size="sm"
:options="exchangeList"
>
</b-form-select>
<b-form-select
id="tradeMode-select"
v-model="exchangeModel.trade_mode"
size="sm"
:options="tradeModes"
:disabled="tradeModes.length < 2"
>
</b-form-select>
<div class="ms-2">
<b-button size="sm" @click="botStore.activeBot.getExchangeList">
<i-mdi-refresh />
</b-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useBotStore } from '@/stores/ftbotwrapper';
import { computed, onMounted, watch } from 'vue';
import { ExchangeSelection } from '@/types';
const exchangeModel = defineModel({
type: Object as () => ExchangeSelection,
required: true,
});
const botStore = useBotStore();
const exchangeList = computed(() => {
const supported = botStore.activeBot.exchangeList
.filter((ex) => ex.valid && ex.supported)
.sort((a, b) => a.name.localeCompare(b.name));
const unsupported = botStore.activeBot.exchangeList
.filter((ex) => ex.valid && !ex.supported)
.sort((a, b) => a.name.localeCompare(b.name));
return [
{ label: 'Supported', options: supported.map((e) => e.name) },
{ label: 'Unsupported', options: unsupported.map((e) => e.name) },
];
});
const tradeModesTyped = computed(() => {
const val = botStore.activeBot.exchangeList.find(
(ex) => ex.name === exchangeModel.value.exchange,
)?.trade_modes;
return val ?? [];
});
const tradeModes = computed<Record<string, unknown>[]>(() => {
return tradeModesTyped.value.map((tm) => {
return (
{
text: `${tm.margin_mode} ${tm.trading_mode}`,
value: tm,
} ?? []
);
}) as Record<string, unknown>[];
});
watch(
() => exchangeModel.value.exchange,
() => {
if (tradeModesTyped.value.length < 2) {
exchangeModel.value.trade_mode = tradeModesTyped.value[0];
}
},
);
onMounted(() => {
if (botStore.activeBot.exchangeList.length === 0) {
botStore.activeBot.getExchangeList();
}
});
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="d-flex flex-column flex-sm-row mb-2 gap-2">
<b-button
title="Save configuration"
size="sm"
variant="primary"
@click="pairlistStore.saveConfig(pairlistStore.config.name)"
>
<i-mdi-content-save />
</b-button>
<edit-value
v-model="pairlistStore.config.name"
editable-name="config"
:allow-add="true"
:allow-duplicate="true"
:allow-edit="true"
class="d-flex flex-grow-1"
@delete="pairlistStore.deleteConfig"
@duplicate="(oldName:string,newName:string) => pairlistStore.duplicateConfig(newName)"
@new="(name:string) => pairlistStore.newConfig(name)"
@rename="(oldName: string, newName:string) => pairlistStore.saveConfig(newName)"
>
<b-form-select
v-model="pairlistStore.configName"
size="sm"
:options="pairlistStore.savedConfigs.map((c) => c.name)"
@change="(config) => pairlistStore.selectOrCreateConfig(config)"
/>
</edit-value>
<b-button
title="Evaluate pairlist"
:disabled="pairlistStore.evaluating || !pairlistStore.pairlistValid"
variant="primary"
class="px-5"
size="sm"
@click="pairlistStore.startPairlistEvaluation()"
>
<b-spinner v-if="pairlistStore.evaluating" small></b-spinner>
<span>{{ pairlistStore.evaluating ? '' : 'Evaluate' }}</span>
</b-button>
</div>
</template>
<script setup lang="ts">
import { usePairlistConfigStore } from '@/stores/pairlistConfig';
import EditValue from '../general/EditValue.vue';
const pairlistStore = usePairlistConfigStore();
</script>

View File

@ -0,0 +1,62 @@
<template>
<b-card no-body class="mb-2">
<template #header>
<div
class="d-flex flex-row align-items-center justify-content-between"
role="button"
@click="visible = !visible"
>
<span class="fw-bold fd-italic">Blacklist</span>
<i-mdi-chevron-down
v-if="!visible"
:class="!visible ? 'visible' : 'invisible'"
role="button"
class="fs-4"
/>
<i-mdi-chevron-up
v-if="visible"
:class="visible ? 'visible' : 'invisible'"
role="button"
class="fs-4"
/>
</div>
</template>
<b-collapse v-model="visible">
<b-card-body>
<div class="d-flex mb-4 align-items-center gap-2">
<span class="col-auto">Copy from:</span
><b-form-select v-model="copyFromConfig" size="sm" :options="configNames" />
<b-button title="Copy" size="sm" @click="pairlistStore.duplicateBlacklist(copyFromConfig)"
><i-mdi-content-copy
/></b-button>
</div>
<b-input-group
v-for="(item, i) in pairlistStore.config.blacklist"
:key="i"
class="mb-2"
size="sm"
>
<b-form-input v-model="pairlistStore.config.blacklist[i]" />
<b-input-group-append>
<b-button size="sm" @click="pairlistStore.removeFromBlacklist(i)"
><i-mdi-close
/></b-button>
</b-input-group-append>
</b-input-group>
<b-button size="sm" @click="pairlistStore.addToBlacklist()">Add</b-button>
</b-card-body>
</b-collapse>
</b-card>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { usePairlistConfigStore } from '@/stores/pairlistConfig';
const pairlistStore = usePairlistConfigStore();
const copyFromConfig = ref('');
const visible = ref(false);
const configNames = computed(() =>
pairlistStore.savedConfigs.filter((c) => c.name !== pairlistStore.config.name).map((c) => c.name),
);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,80 @@
<template>
<b-card no-body class="mb-2">
<template #header>
<div class="d-flex text-start align-items-center">
<div class="d-flex flex-grow-1 align-items-center">
<i-mdi-reorder-horizontal
role="button"
class="handle me-2 fs-4 flex-shrink-0"
width="24"
height="24"
/>
<div
role="button"
class="d-flex flex-grow-1 align-items-start flex-column user-select-none"
@click="toggleVisible"
>
<span class="fw-bold">{{ pairlist.name }}</span>
<span class="text-small">{{ pairlist.description }}</span>
</div>
</div>
<i-mdi-close
role="button"
width="24"
height="24"
class="mx-2"
@click="pairlistStore.removeFromConfig(index)"
/>
<i-mdi-chevron-down
v-if="!pairlist.showParameters"
:class="hasParameters && !pairlist.showParameters ? 'visible' : 'invisible'"
role="button"
class="fs-4"
@click="toggleVisible"
/>
<i-mdi-chevron-up
v-if="pairlist.showParameters"
:class="hasParameters && pairlist.showParameters ? 'visible' : 'invisible'"
role="button"
class="fs-4"
@click="toggleVisible"
/>
</div>
</template>
<b-collapse v-model="pairlist.showParameters">
<b-card-body>
<PairlistConfigParameter
v-for="(parameter, key) in pairlist.params"
:key="key"
v-model="pairlist.params[key].value"
:param="parameter"
/>
</b-card-body>
</b-collapse>
</b-card>
</template>
<script setup lang="ts">
import PairlistConfigParameter from '@/components/ftbot/PairlistConfigParameter.vue';
import { usePairlistConfigStore } from '@/stores/pairlistConfig';
import { Pairlist } from '@/types';
import { computed } from 'vue';
const pairlistStore = usePairlistConfigStore();
defineProps<{
index: number;
}>();
const pairlist = defineModel<Pairlist>({ required: true });
const hasParameters = computed(() => Object.keys(pairlist.value.params).length > 0);
function toggleVisible() {
if (hasParameters.value) {
pairlist.value.showParameters = !pairlist.value.showParameters;
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,34 @@
<template>
<b-form-group label-cols="4" label-size="md" class="pb-1 text-start" :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';
defineProps<{
param: PairlistParameter;
}>();
const paramValue = defineModel<string>();
</script>

View File

@ -0,0 +1,65 @@
<template>
<div>
<div v-if="whitelist.length > 0" class="d-flex flex-column flex-lg-row px-2">
<!-- TODO: look into flexbox solution to have overflow scroll? -->
<b-list-group class="col-12 col-md-2 overflow-auto" style="height: calc(100vh - 135px)">
<b-list-group-item
v-for="(pair, i) in whitelist"
:key="pair.pair"
button
class="d-flex py-2"
:active="pair.pair === botStore.activeBot.selectedPair"
:title="pair.pair"
@click="botStore.activeBot.selectedPair = pair.pair"
>
<b-form-checkbox v-model="whitelist[i].enabled"></b-form-checkbox>
{{ pair.pair }}
</b-list-group-item>
</b-list-group>
<div class="flex-fill">
<ChartView />
</div>
<div class="col-12 col-md-2">
<CopyableTextfield
style="height: calc(100vh - 135px)"
class="overflow-auto"
:content="
JSON.stringify(
whitelist.filter((p) => p.enabled === true).map((p) => p.pair),
null,
2,
)
"
/>
</div>
</div>
<div v-else>
<p>Evaluation returned 0 pairs</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { usePairlistConfigStore } from '@/stores/pairlistConfig';
import ChartView from '@/views/ChartsView.vue';
import CopyableTextfield from '@/components/general/CopyableTextfield.vue';
const botStore = useBotStore();
const pairlistStore = usePairlistConfigStore();
const whitelist = ref<{ enabled: boolean; pair: string }[]>([]);
watch(
() => pairlistStore.whitelist,
() => {
whitelist.value = pairlistStore.whitelist.map((p) => {
return {
enabled: true,
pair: p,
};
});
},
);
</script>

View File

@ -0,0 +1,198 @@
<template>
<div class="d-flex px-3 mb-3 gap-3 flex-column flex-lg-row">
<b-list-group ref="availablePairlistsEl" class="available-pairlists">
<b-list-group-item
v-for="pairlist in availablePairlists"
:key="pairlist.name"
:class="{
'no-drag': pairlistStore.config.pairlists.length == 0 && !pairlist.is_pairlist_generator,
}"
class="pairlist d-flex text-start align-items-center py-2 px-3"
>
<div class="d-flex flex-grow-1 align-items-start flex-column">
<span class="fw-bold">{{ pairlist.name }}</span>
<span class="text-small">{{ pairlist.description }}</span>
</div>
<b-button
class="p-0 add-pairlist"
style="border: none"
variant="outline-light"
:disabled="pairlistStore.config.pairlists.length == 0 && !pairlist.is_pairlist_generator"
@click="pairlistStore.addToConfig(pairlist, pairlistStore.config.pairlists.length)"
>
<i-mdi-arrow-right-bold-box-outline class="fs-4" />
</b-button>
</b-list-group-item>
</b-list-group>
<div class="d-flex flex-column flex-fill">
<PairlistConfigActions />
<div class="border rounded-1 p-2 mb-2">
<div class="d-flex align-items-center gap-2 my-2">
<span class="col-auto">Stake currency: </span>
<b-form-input v-model="pairlistStore.stakeCurrency" size="sm" />
</div>
<div class="mb-2 border rounded-1 p-2 text-start">
<b-form-checkbox v-model="pairlistStore.customExchange" class="mb-2">
Custom Exchange
</b-form-checkbox>
<exchange-select
v-if="pairlistStore.customExchange"
v-model="pairlistStore.selectedExchange"
/>
</div>
</div>
<PairlistConfigBlacklist />
<b-alert
:model-value="
pairlistStore.config.pairlists.length > 0 && !pairlistStore.firstPairlistIsGenerator
"
variant="warning"
>
First entry in the pairlist must be a Generating pairlist, like StaticPairList or
VolumePairList.
</b-alert>
<div
ref="pairlistConfigsEl"
class="d-flex flex-column flex-grow-1 position-relative border rounded-1 p-1"
:class="{ empty: configEmpty }"
>
<PairlistConfigItem
v-for="(pairlist, i) in pairlistStore.config.pairlists"
:key="pairlist.id"
v-model="pairlistStore.config.pairlists[i]"
:index="i"
@remove="pairlistStore.removeFromConfig"
/>
</div>
</div>
<div class="d-flex flex-column col-12 col-lg-3">
<b-form-radio-group v-model="selectedView" class="mb-2" size="sm" buttons>
<b-form-radio button value="Config"> Config</b-form-radio>
<b-form-radio button value="Results" :disabled="pairlistStore.whitelist.length === 0">
Results</b-form-radio
>
</b-form-radio-group>
<div class="position-relative flex-fill overflow-auto">
<CopyableTextfield
v-if="selectedView === 'Config'"
class="position-lg-absolute w-100"
:content="pairlistStore.configJSON"
:is-valid="pairlistStore.pairlistValid"
/>
<CopyableTextfield
v-if="selectedView === 'Results'"
class="position-lg-absolute w-100"
:content="pairlistStore.whitelist"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { useBotStore } from '@/stores/ftbotwrapper';
import { usePairlistConfigStore } from '@/stores/pairlistConfig';
import PairlistConfigItem from './PairlistConfigItem.vue';
import PairlistConfigBlacklist from './PairlistConfigBlacklist.vue';
import PairlistConfigActions from './PairlistConfigActions.vue';
import { Pairlist } from '@/types';
import { useSortable, moveArrayElement } from '@vueuse/integrations/useSortable';
import CopyableTextfield from '@/components/general/CopyableTextfield.vue';
import ExchangeSelect from './ExchangeSelect.vue';
const botStore = useBotStore();
const pairlistStore = usePairlistConfigStore();
const availablePairlists = ref<Pairlist[]>([]);
const pairlistConfigsEl = ref<HTMLElement | null>(null);
const availablePairlistsEl = ref<HTMLElement | null>(null);
const selectedView = ref<'Config' | 'Results'>('Config');
const configEmpty = computed(() => {
return pairlistStore.config.pairlists.length == 0;
});
useSortable(availablePairlistsEl, availablePairlists.value, {
group: {
name: 'configurator',
pull: 'clone',
put: false,
},
sort: false,
filter: '.no-drag',
dragClass: 'dragging',
});
useSortable(pairlistConfigsEl, pairlistStore.config.pairlists, {
handle: '.handle',
group: 'configurator',
onUpdate: async (e) => {
moveArrayElement(pairlistStore.config.pairlists, e.oldIndex, e.newIndex);
},
onAdd: (e) => {
const pairlist = availablePairlists.value[e.oldIndex];
pairlistStore.addToConfig(pairlist, e.newIndex);
// quick fix from: https://github.com/SortableJS/Sortable/issues/1515
e.clone.replaceWith(e.item);
e.clone.remove();
},
});
onMounted(async () => {
availablePairlists.value = (await botStore.activeBot.getPairlists()).pairlists.sort((a, b) =>
// Sort by is_pairlist_generator (by name), then by name.
// TODO: this might need to be improved
a.is_pairlist_generator === b.is_pairlist_generator
? a.name.localeCompare(b.name)
: a.is_pairlist_generator
? -1
: 1,
);
pairlistStore.selectOrCreateConfig(
pairlistStore.isSavedConfig(pairlistStore.configName) ? pairlistStore.configName : 'default',
);
});
watch(
() => pairlistStore.whitelist,
() => {
selectedView.value = 'Results';
},
);
</script>
<style lang="scss" scoped>
.pairlist {
&:hover {
cursor: grab;
}
&.no-drag {
color: gray;
}
&.no-drag:hover {
cursor: default;
}
&.dragging {
border: 1px solid white;
border-radius: 0;
}
}
[data-bs-theme='light'] .add-pairlist {
color: black;
}
.empty {
&:after {
content: 'Drag pairlist here';
position: absolute;
align-self: center;
font-size: 1.1rem;
text-transform: uppercase;
line-height: 0;
top: 50%;
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="copy-container position-relative">
<i-mdi-content-copy
v-if="isSupported && isValid"
role="button"
class="copy-button position-absolute end-0 mt-1 me-2"
@click="copy(content)"
/>
<pre class="text-start border p-1 mb-0"><code>{{ content }}</code></pre>
</div>
</template>
<script setup lang="ts">
import { useClipboard } from '@vueuse/core';
defineProps({
content: { type: String, required: true },
isValid: { type: Boolean, default: true },
});
const { copy, isSupported } = useClipboard();
</script>
<style lang="scss" scoped>
.copy-container {
.copy-button {
opacity: 0;
}
&:hover {
.copy-button {
opacity: 1;
}
}
}
</style>

View File

@ -23,6 +23,15 @@
<router-link v-if="botStore.canRunBacktest" class="nav-link navbar-nav" to="/backtest"
>Backtest</router-link
>
<router-link
v-if="
(botStore.activeBot?.isWebserverMode ?? false) &&
botStore.activeBot.botApiVersion >= 2.3
"
class="nav-link navbar-nav"
to="/pairlist_config"
>Pairlist Config</router-link
>
<theme-select />
</b-navbar-nav>

View File

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

View File

@ -35,9 +35,14 @@ import {
TradeResponse,
ClosedTrade,
BotDescriptor,
BgTaskStarted,
BackgroundTaskStatus,
Exchange,
ExchangeListResult,
FreqAIModelListResult,
PairlistEvalResponse,
PairlistsPayload,
PairlistsResponse,
} from '@/types';
import axios, { AxiosResponse } from 'axios';
import { defineStore } from 'pinia';
@ -548,6 +553,45 @@ export function createBotSubStore(botId: string, botName: string) {
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 evaluatePairlist(payload: PairlistsPayload) {
try {
const { data } = await api.post<PairlistsPayload, AxiosResponse<BgTaskStarted>>(
'/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<PairlistEvalResponse>(`/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<BackgroundTaskStatus>(`/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() {

View File

@ -0,0 +1,260 @@
import { defineStore } from 'pinia';
import { useBotStore } from './ftbotwrapper';
import {
ExchangeSelection,
MarginMode,
Pairlist,
PairlistConfig,
PairlistParamType,
PairlistParamValue,
PairlistPayloadItem,
PairlistsPayload,
TradingMode,
} from '@/types';
import { computed, ref, toRaw, watch } from 'vue';
import { showAlert } from './alerts';
import { isNotUndefined } from '@/shared/formatters';
export const usePairlistConfigStore = defineStore(
'pairlistConfig',
() => {
const botStore = useBotStore();
const evaluating = ref<boolean>(false);
const intervalId = ref<number>();
const stakeCurrency = ref<string>(botStore.activeBot?.stakeCurrency ?? 'USDT');
const whitelist = ref<string[]>([]);
const customExchange = ref<boolean>(false);
const selectedExchange = ref<ExchangeSelection>({
exchange: botStore.activeBot?.botState.exchange ?? '',
trade_mode: {
trading_mode: botStore.activeBot?.botState.trading_mode ?? TradingMode.SPOT,
margin_mode:
botStore.activeBot?.botState.trading_mode === TradingMode.FUTURES
? MarginMode.ISOLATED
: MarginMode.NONE,
},
});
const config = ref<PairlistConfig>(makeConfig());
const savedConfigs = ref<PairlistConfig[]>([]);
const configName = ref<string>('');
const firstPairlistIsGenerator = computed<boolean>(() => {
// First pairlist must be a generator
if (config.value.pairlists[0]?.is_pairlist_generator) {
return true;
}
return false;
});
const pairlistValid = computed<boolean>(() => {
return firstPairlistIsGenerator.value && config.value.pairlists.length > 0;
});
const configJSON = computed(() => {
return JSON.stringify(configToPayloadItems(), null, 2);
});
const isSavedConfig = (name: string) =>
savedConfigs.value.findIndex((c) => c.name === name) > -1;
function addToConfig(pairlist: Pairlist, index: number) {
pairlist = structuredClone(toRaw(pairlist));
pairlist.showParameters = false;
if (!pairlist.id) {
pairlist.id = Date.now().toString(36) + Math.random().toString(36).substring(2);
}
for (const param in pairlist.params) {
pairlist.params[param].value = isNotUndefined(pairlist.params[param].default)
? pairlist.params[param].default
: '';
}
config.value.pairlists.splice(index, 0, pairlist);
}
function removeFromConfig(index: number) {
config.value.pairlists.splice(index, 1);
}
function saveConfig(name = '') {
const i = savedConfigs.value.findIndex((c) => c.name === config.value.name);
config.value.name = name;
if (i > -1) {
savedConfigs.value[i] = structuredClone(toRaw(config.value));
} else {
savedConfigs.value.push(structuredClone(toRaw(config.value)));
}
}
function newConfig(name: string) {
const c = makeConfig({ name });
savedConfigs.value.push(c);
config.value = structuredClone(c);
}
function duplicateConfig(name = '') {
const c = makeConfig({
name,
pairlists: toRaw(config.value.pairlists) as [],
blacklist: toRaw(config.value.blacklist) as [],
});
savedConfigs.value.push(c);
config.value = structuredClone(c);
}
function deleteConfig() {
const i = savedConfigs.value.findIndex((c) => c.name === config.value.name);
if (i > -1) {
savedConfigs.value.splice(i, 1);
selectOrCreateConfig(
savedConfigs.value.length > 0 ? savedConfigs.value[0].name : 'default',
);
}
}
function selectOrCreateConfig(name: string) {
const c = savedConfigs.value.find((c) => name === c.name);
if (c) {
config.value = structuredClone(toRaw(c));
} else {
newConfig(name);
}
}
function makeConfig({ name = '', pairlists = [], blacklist = [] } = {}): PairlistConfig {
return { name, pairlists, blacklist };
}
function addToBlacklist() {
config.value.blacklist.push('');
}
function removeFromBlacklist(index: number) {
config.value.blacklist.splice(index, 1);
}
function duplicateBlacklist(configName: string) {
const conf = savedConfigs.value.find((c) => c.name === configName);
if (conf) {
config.value.blacklist = structuredClone(toRaw(conf.blacklist));
}
}
async function startPairlistEvaluation() {
const payload: PairlistsPayload = configToPayload();
evaluating.value = true;
try {
const { job_id: jobId } = await botStore.activeBot.evaluatePairlist(payload);
console.log('jobId', jobId);
intervalId.value = setInterval(async () => {
const res = await botStore.activeBot.getBackgroundJobStatus(jobId);
if (!res.running) {
clearInterval(intervalId.value);
const wl = await botStore.activeBot.getPairlistEvalResult(jobId);
evaluating.value = false;
if (wl.status === 'success') {
whitelist.value = wl.result.whitelist;
} else if (wl.error) {
showAlert(wl.error, 'danger');
evaluating.value = false;
}
}
}, 1000);
} catch (error) {
showAlert('Evaluation failed', 'danger');
evaluating.value = false;
}
}
function convertToParamType(type: PairlistParamType, value: PairlistParamValue) {
if (type === PairlistParamType.number) {
return Number(value);
} else if (type === PairlistParamType.boolean) {
return Boolean(value);
} else {
return String(value);
}
}
function configToPayload(): PairlistsPayload {
const pairlists: PairlistPayloadItem[] = configToPayloadItems();
const c: PairlistsPayload = {
pairlists: pairlists,
stake_currency: stakeCurrency.value,
blacklist: config.value.blacklist,
};
if (customExchange.value) {
console.log('setting custom exchange props');
c.exchange = selectedExchange.value.exchange;
c.trading_mode = selectedExchange.value.trade_mode.trading_mode;
c.margin_mode = selectedExchange.value.trade_mode.margin_mode;
}
return c;
}
function configToPayloadItems() {
const pairlists: PairlistPayloadItem[] = [];
config.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);
});
return pairlists;
}
watch(
() => config.value,
() => {
configName.value = config.value.name;
},
{
deep: true,
},
);
return {
evaluating,
whitelist,
config,
configJSON,
savedConfigs,
configName,
startPairlistEvaluation,
addToConfig,
removeFromConfig,
saveConfig,
duplicateConfig,
deleteConfig,
newConfig,
selectOrCreateConfig,
addToBlacklist,
removeFromBlacklist,
duplicateBlacklist,
isSavedConfig,
firstPairlistIsGenerator,
pairlistValid,
stakeCurrency,
customExchange,
selectedExchange,
};
},
{
persist: {
key: 'ftPairlistConfig',
paths: ['savedConfigs', 'configName'],
},
},
);

View File

@ -37,6 +37,10 @@
transform: none !important;
}
.text-small {
font-size: 0.8rem;
}
[data-bs-theme="dark"] {
$bg-dark: rgb(18, 18, 18);
@ -51,6 +55,10 @@
color: $fg-color;
}
.border {
border-color: lighten($bg-dark, 40%) !important;
}
.card {
border-color: lighten($bg-dark, 10%);
background-color: $bg-dark;
@ -238,3 +246,9 @@ body.ft-theme-transition *:after {
;
transition-delay: 0 !important;
}
.position-lg-absolute{
@include media-breakpoint-up(lg){
position: absolute !important;
}
}

View File

@ -0,0 +1,16 @@
export interface BgTaskStarted {
job_id: string;
}
export interface BackgroundTaskStatus {
job_id: string;
job_category: string;
status: string;
running: boolean;
progress?: number;
}
export interface BackgroundTaskResult {
error?: string;
status: string;
}

View File

@ -16,3 +16,8 @@ export interface Exchange {
export interface ExchangeListResult {
exchanges: Exchange[];
}
export interface ExchangeSelection {
exchange: string;
trade_mode: Partial<TradeMode>;
}

View File

@ -1,14 +1,16 @@
export * from './auth';
export * from './backtest';
export * from './backgroundtasks';
export * from './balance';
export * from './blacklist';
export * from './botComparison';
export * from './chart';
export * from './exchange';
export * from './daily';
export * from './gridLayout';
export * from './locks';
export * from './pairlists';
export * from './plot';
export * from './profit';
export * from './trades';
export * from './types';
export * from './gridLayout';

86
src/types/pairlists.ts Normal file
View File

@ -0,0 +1,86 @@
import { BackgroundTaskResult } from './backgroundtasks';
import { WhitelistResponse } from './blacklist';
import { MarginMode, TradingMode } from './types';
export interface PairlistsResponse {
pairlists: Pairlist[];
}
export interface PairlistEvalResponse extends BackgroundTaskResult {
result: WhitelistResponse;
}
export interface Pairlist {
id?: string;
is_pairlist_generator: boolean;
name: string;
description: string;
showParameters: boolean;
params: Record<string, PairlistParameter>;
}
export interface PairlistConfig {
name: string;
blacklist: string[];
pairlists: Pairlist[];
}
export enum PairlistParamType {
string = 'string',
number = 'number',
boolean = 'boolean',
option = 'option',
}
export type PairlistParamValue = string | number | boolean;
interface PairlistParameterBase {
description: string;
help: string;
type: PairlistParamType;
}
export interface StringPairlistParameter extends PairlistParameterBase {
type: PairlistParamType.string;
value?: string;
default: string;
}
export interface NumberPairlistParameter extends PairlistParameterBase {
type: PairlistParamType.number;
value?: number;
default: number;
}
export interface BooleanPairlistParameter extends PairlistParameterBase {
type: PairlistParamType.boolean;
value?: boolean;
default: boolean;
}
export interface OptionPairlistParameter extends PairlistParameterBase {
type: PairlistParamType.option;
options: string[];
value?: string;
default: string;
}
export type PairlistParameter =
| StringPairlistParameter
| NumberPairlistParameter
| BooleanPairlistParameter
| OptionPairlistParameter;
export interface PairlistPayloadItem {
method: string;
[key: string]: string | number | boolean;
}
export interface PairlistsPayload {
pairlists: PairlistPayloadItem[];
blacklist: string[];
stake_currency: string;
exchange?: string;
trading_mode?: TradingMode;
margin_mode?: MarginMode;
}

View File

@ -81,7 +81,7 @@ export enum TradingMode {
}
export enum MarginMode {
NONE = 'none',
NONE = '',
ISOLATED = 'isolated',
// CROSS = 'cross',
}

View File

@ -0,0 +1,7 @@
<template>
<PairlistConfigurator class="pt-4" />
</template>
<script setup lang="ts">
import PairlistConfigurator from '@/components/ftbot/PairlistConfigurator.vue';
</script>

View File

@ -9,7 +9,11 @@ import { BootstrapVueNextResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
createVuePlugin({}),
createVuePlugin({
script: {
defineModel: true,
},
}),
Components({
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
dirs: [],