2023-05-23 18:27:33 +00:00
|
|
|
<template>
|
2023-05-24 18:19:46 +00:00
|
|
|
<b-container fluid>
|
|
|
|
<b-row align-v="stretch">
|
2023-05-29 10:28:24 +00:00
|
|
|
<b-col cols="12" md="3" class="overflow-auto">
|
2023-05-28 16:17:40 +00:00
|
|
|
<b-list-group ref="availablePairlistsEl" class="available-pairlists">
|
|
|
|
<b-list-group-item
|
2023-05-24 18:19:46 +00:00
|
|
|
v-for="pairlist in availablePairlists"
|
|
|
|
:key="pairlist.name"
|
|
|
|
align-v="center"
|
2023-05-29 09:57:37 +00:00
|
|
|
:class="{ 'no-drag': config.pairlists.length == 0 && !pairlist.is_pairlist_generator }"
|
2023-05-29 10:04:11 +00:00
|
|
|
class="pairlist d-flex text-start align-items-center py-2 px-3"
|
2023-05-24 18:19:46 +00:00
|
|
|
>
|
2023-05-28 16:17:40 +00:00
|
|
|
<div class="d-flex flex-grow-1 align-items-start flex-column">
|
|
|
|
<span class="fw-bold fd-italic">{{ pairlist.name }}</span>
|
|
|
|
<span class="fw-lighter">{{ pairlist.description }}</span>
|
|
|
|
</div>
|
|
|
|
<b-button
|
|
|
|
class="p-0"
|
|
|
|
style="border: none"
|
|
|
|
variant="outline-light"
|
2023-05-29 09:57:37 +00:00
|
|
|
:disabled="config.pairlists.length == 0 && !pairlist.is_pairlist_generator"
|
2023-05-28 16:17:40 +00:00
|
|
|
@click="addToConfig(pairlist, selectedConfig.pairlists.length)"
|
2023-05-29 06:29:30 +00:00
|
|
|
>
|
|
|
|
<i-mdi-arrow-right-bold-box-outline class="fs-4" />
|
|
|
|
</b-button>
|
2023-05-28 16:17:40 +00:00
|
|
|
</b-list-group-item>
|
|
|
|
</b-list-group>
|
2023-05-24 18:19:46 +00:00
|
|
|
</b-col>
|
|
|
|
<b-col>
|
|
|
|
<b-row>
|
|
|
|
<b-col>
|
|
|
|
<b-form-input
|
|
|
|
v-model="config.name"
|
|
|
|
class="mb-2"
|
|
|
|
placeholder="Configuration name..."
|
|
|
|
></b-form-input>
|
|
|
|
</b-col>
|
|
|
|
<b-col cols="auto">
|
|
|
|
<b-button @click="save">Save</b-button>
|
|
|
|
</b-col>
|
|
|
|
</b-row>
|
2023-05-29 10:28:24 +00:00
|
|
|
<b-button
|
|
|
|
:disabled="evaluating || !pairlistValid"
|
|
|
|
variant="primary"
|
|
|
|
size="lg"
|
|
|
|
squared
|
|
|
|
class="mb-2 evaluate"
|
|
|
|
@click="evaluateClick"
|
|
|
|
>
|
|
|
|
<b-spinner v-if="evaluating" small></b-spinner>
|
|
|
|
<span>{{ evaluating ? 'Evaluating...' : 'Evaluate' }}</span>
|
|
|
|
</b-button>
|
|
|
|
<b-alert
|
|
|
|
:model-value="config.pairlists.length > 0 && !firstPairlistIsGenerator"
|
|
|
|
variant="warning"
|
|
|
|
>
|
|
|
|
First entry in the pairlist must be a Generating pairlist, like StaticPairList or
|
|
|
|
VolumePairList.
|
|
|
|
</b-alert>
|
2023-05-28 16:41:18 +00:00
|
|
|
<div ref="pairlistConfigsEl" class="h-100">
|
2023-05-24 18:19:46 +00:00
|
|
|
<PairlistConfigItem
|
2023-05-26 11:12:40 +00:00
|
|
|
v-for="(pairlist, i) in pairlistsComp"
|
2023-05-24 18:19:46 +00:00
|
|
|
:key="pairlist.id"
|
|
|
|
v-model="config.pairlists[i]"
|
|
|
|
:index="i"
|
|
|
|
@remove="removeFromConfig"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</b-col>
|
2023-05-26 11:12:40 +00:00
|
|
|
<b-col cols="12" md="3">
|
2023-05-29 07:34:26 +00:00
|
|
|
<i-mdi-content-copy
|
2023-05-29 07:39:32 +00:00
|
|
|
v-if="isSupported && pairlistValid"
|
2023-05-29 07:34:26 +00:00
|
|
|
role="button"
|
2023-05-29 07:39:32 +00:00
|
|
|
class="position-absolute end-0 mt-1 me-3"
|
2023-05-29 07:34:26 +00:00
|
|
|
@click="copy(configJSON)"
|
|
|
|
/>
|
2023-05-29 07:39:32 +00:00
|
|
|
<pre class="text-start border p-1"><code>{{ configJSON }}</code></pre>
|
2023-05-26 11:12:40 +00:00
|
|
|
</b-col>
|
2023-05-24 18:19:46 +00:00
|
|
|
</b-row>
|
|
|
|
</b-container>
|
2023-05-23 18:27:33 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2023-05-24 18:19:46 +00:00
|
|
|
import { computed, onMounted, ref, toRaw, watch } from 'vue';
|
2023-05-23 18:27:33 +00:00
|
|
|
import { useBotStore } from '@/stores/ftbotwrapper';
|
|
|
|
import PairlistConfigItem from './PairlistConfigItem.vue';
|
2023-05-24 23:40:43 +00:00
|
|
|
import { Pairlist, PairlistConfig, PairlistParamType, PairlistPayloadItem } from '@/types';
|
2023-05-23 18:27:33 +00:00
|
|
|
import { useSortable, moveArrayElement } from '@vueuse/integrations/useSortable';
|
2023-05-29 07:34:26 +00:00
|
|
|
import { useClipboard } from '@vueuse/core';
|
2023-05-23 18:27:33 +00:00
|
|
|
|
2023-05-24 23:40:43 +00:00
|
|
|
const emit = defineEmits([
|
|
|
|
'update:modelValue',
|
|
|
|
'saveConfig',
|
|
|
|
'started',
|
|
|
|
'progress',
|
|
|
|
'done',
|
|
|
|
'error',
|
|
|
|
]);
|
2023-05-23 18:27:33 +00:00
|
|
|
const props = defineProps<{
|
2023-05-26 11:12:40 +00:00
|
|
|
selectedConfig: PairlistConfig;
|
2023-05-23 18:27:33 +00:00
|
|
|
}>();
|
|
|
|
|
|
|
|
const botStore = useBotStore();
|
|
|
|
|
|
|
|
const availablePairlists = ref<Pairlist[]>([]);
|
2023-05-26 11:12:40 +00:00
|
|
|
const config = ref<PairlistConfig>(props.selectedConfig);
|
2023-05-23 18:27:33 +00:00
|
|
|
const pairlistConfigsEl = ref<HTMLElement | null>(null);
|
2023-05-24 18:19:46 +00:00
|
|
|
const availablePairlistsEl = ref<HTMLElement | null>(null);
|
2023-05-24 23:40:43 +00:00
|
|
|
const evaluating = ref(false);
|
2023-05-23 18:27:33 +00:00
|
|
|
|
2023-05-29 06:40:24 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2023-05-23 18:27:33 +00:00
|
|
|
// v-for updates with sorting, deleting and adding items seem to get wonky without unique keys for every item
|
2023-05-26 11:12:40 +00:00
|
|
|
const pairlistsComp = computed(() =>
|
2023-05-23 18:27:33 +00:00
|
|
|
config.value.pairlists.map((p) => {
|
|
|
|
if (p.id) {
|
|
|
|
return p;
|
|
|
|
} else {
|
|
|
|
return { id: Date.now().toString(36) + Math.random().toString(36).substring(2), ...p };
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
2023-05-26 11:12:40 +00:00
|
|
|
const configJSON = computed(() => {
|
2023-05-26 11:43:15 +00:00
|
|
|
return JSON.stringify(configToPayloadItems(), null, 2);
|
2023-05-26 11:12:40 +00:00
|
|
|
});
|
|
|
|
|
2023-05-24 18:19:46 +00:00
|
|
|
useSortable(availablePairlistsEl, availablePairlists.value, {
|
|
|
|
group: {
|
|
|
|
name: 'configurator',
|
|
|
|
pull: 'clone',
|
|
|
|
put: false,
|
|
|
|
},
|
|
|
|
sort: false,
|
2023-05-29 09:57:37 +00:00
|
|
|
filter: '.no-drag',
|
2023-05-23 18:27:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
useSortable(pairlistConfigsEl, config.value.pairlists, {
|
|
|
|
handle: '.handle',
|
2023-05-24 18:19:46 +00:00
|
|
|
group: 'configurator',
|
2023-05-23 18:27:33 +00:00
|
|
|
onUpdate: async (e) => {
|
|
|
|
moveArrayElement(config.value.pairlists, e.oldIndex, e.newIndex);
|
|
|
|
},
|
2023-05-24 18:19:46 +00:00
|
|
|
onAdd: (e) => {
|
|
|
|
const pairlist = availablePairlists.value[e.oldIndex];
|
|
|
|
addToConfig(pairlist, e.newIndex);
|
2023-05-24 21:53:10 +00:00
|
|
|
// quick fix from: https://github.com/SortableJS/Sortable/issues/1515
|
|
|
|
e.clone.replaceWith(e.item);
|
|
|
|
e.clone.remove();
|
2023-05-24 18:19:46 +00:00
|
|
|
},
|
2023-05-23 18:27:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
onMounted(async () => {
|
2023-05-28 17:39:06 +00:00
|
|
|
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,
|
|
|
|
);
|
2023-05-23 18:27:33 +00:00
|
|
|
});
|
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
function addToConfig(pairlist: Pairlist, index: number) {
|
2023-05-24 18:19:46 +00:00
|
|
|
pairlist = structuredClone(toRaw(pairlist));
|
|
|
|
for (const param in pairlist.params) {
|
|
|
|
pairlist.params[param].value = pairlist.params[param].default
|
|
|
|
? pairlist.params[param].default.toString()
|
|
|
|
: '';
|
2023-05-23 18:27:33 +00:00
|
|
|
}
|
2023-05-24 18:19:46 +00:00
|
|
|
config.value.pairlists.splice(index, 0, pairlist);
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-23 18:27:33 +00:00
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
function removeFromConfig(index: number) {
|
2023-05-23 18:27:33 +00:00
|
|
|
config.value.pairlists.splice(index, 1);
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-23 18:27:33 +00:00
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
async function save() {
|
2023-05-23 18:27:33 +00:00
|
|
|
emit('saveConfig', config.value);
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-23 18:27:33 +00:00
|
|
|
|
2023-05-29 07:34:26 +00:00
|
|
|
const { copy, isSupported } = useClipboard();
|
|
|
|
|
|
|
|
async function evaluateClick() {
|
2023-05-24 23:40:43 +00:00
|
|
|
const payload = configToPayload();
|
|
|
|
|
|
|
|
evaluating.value = true;
|
|
|
|
const res = await botStore.activeBot.evaluatePairlist(payload);
|
|
|
|
emit('started', res);
|
|
|
|
const evalIntervalId = setInterval(async () => {
|
2023-05-26 11:12:40 +00:00
|
|
|
const res = await botStore.activeBot.getPairlistEvalStatus(evalIntervalId);
|
2023-05-24 23:40:43 +00:00
|
|
|
if (res.status === 'success' && res.result) {
|
|
|
|
emit('done', res);
|
|
|
|
clearInterval(evalIntervalId);
|
|
|
|
evaluating.value = false;
|
|
|
|
} else if (res.error) {
|
|
|
|
emit('error', res);
|
|
|
|
clearInterval(evalIntervalId);
|
|
|
|
evaluating.value = false;
|
|
|
|
}
|
|
|
|
}, 1000);
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-24 23:40:43 +00:00
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
function convertToParamType(type: PairlistParamType, value: string) {
|
2023-05-24 23:40:43 +00:00
|
|
|
if (type === PairlistParamType.number) {
|
|
|
|
return Number(value);
|
|
|
|
} else if (type === PairlistParamType.boolean) {
|
|
|
|
return Boolean(value);
|
|
|
|
} else {
|
|
|
|
return String(value);
|
|
|
|
}
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-24 23:40:43 +00:00
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
function configToPayload() {
|
2023-05-26 11:12:40 +00:00
|
|
|
const pairlists: PairlistPayloadItem[] = configToPayloadItems();
|
|
|
|
return {
|
|
|
|
pairlists: pairlists,
|
|
|
|
stake_currency: botStore.activeBot.stakeCurrency,
|
|
|
|
blacklist: [],
|
|
|
|
};
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-24 23:40:43 +00:00
|
|
|
|
2023-05-29 07:04:34 +00:00
|
|
|
function configToPayloadItems() {
|
2023-05-26 11:12:40 +00:00
|
|
|
const pairlists: PairlistPayloadItem[] = [];
|
2023-05-24 23:40:43 +00:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2023-05-26 11:12:40 +00:00
|
|
|
return pairlists;
|
2023-05-29 07:04:34 +00:00
|
|
|
}
|
2023-05-24 23:40:43 +00:00
|
|
|
|
2023-05-24 18:19:46 +00:00
|
|
|
watch(
|
2023-05-26 11:12:40 +00:00
|
|
|
() => props.selectedConfig,
|
2023-05-24 18:19:46 +00:00
|
|
|
() => {
|
2023-05-26 11:12:40 +00:00
|
|
|
config.value = structuredClone(toRaw(props.selectedConfig));
|
2023-05-24 18:19:46 +00:00
|
|
|
},
|
|
|
|
);
|
2023-05-23 18:27:33 +00:00
|
|
|
</script>
|
2023-05-24 18:19:46 +00:00
|
|
|
|
2023-05-29 09:57:37 +00:00
|
|
|
<style lang="scss" scoped>
|
2023-05-24 18:19:46 +00:00
|
|
|
.pairlist {
|
|
|
|
border: 1px solid white;
|
|
|
|
}
|
|
|
|
|
2023-05-29 09:57:37 +00:00
|
|
|
.pairlist.no-drag {
|
|
|
|
color: gray;
|
|
|
|
}
|
|
|
|
|
|
|
|
.pairlist.no-drag:hover {
|
|
|
|
cursor: default;
|
|
|
|
}
|
|
|
|
|
2023-05-24 18:19:46 +00:00
|
|
|
.pairlist:hover {
|
|
|
|
cursor: grab;
|
|
|
|
}
|
|
|
|
|
|
|
|
[data-theme='light'] .pairlist {
|
|
|
|
border-color: black;
|
|
|
|
}
|
|
|
|
</style>
|