mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-25 20:45:15 +00:00
Merge pull request #1265 from Tako88/feat/pairlistconfig
Pairlist configuration
This commit is contained in:
commit
1dc608c87c
84
src/components/ftbot/ExchangeSelect.vue
Normal file
84
src/components/ftbot/ExchangeSelect.vue
Normal 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>
|
47
src/components/ftbot/PairlistConfigActions.vue
Normal file
47
src/components/ftbot/PairlistConfigActions.vue
Normal 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>
|
62
src/components/ftbot/PairlistConfigBlacklist.vue
Normal file
62
src/components/ftbot/PairlistConfigBlacklist.vue
Normal 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>
|
80
src/components/ftbot/PairlistConfigItem.vue
Normal file
80
src/components/ftbot/PairlistConfigItem.vue
Normal 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>
|
34
src/components/ftbot/PairlistConfigParameter.vue
Normal file
34
src/components/ftbot/PairlistConfigParameter.vue
Normal 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>
|
65
src/components/ftbot/PairlistConfigResults.vue
Normal file
65
src/components/ftbot/PairlistConfigResults.vue
Normal 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>
|
198
src/components/ftbot/PairlistConfigurator.vue
Normal file
198
src/components/ftbot/PairlistConfigurator.vue
Normal 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>
|
36
src/components/general/CopyableTextfield.vue
Normal file
36
src/components/general/CopyableTextfield.vue
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -67,6 +67,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||
allowAnonymous: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/pairlist_config',
|
||||
name: 'Pairlist Configuration',
|
||||
component: () => import('@/views/PairlistConfigView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/(.*)*',
|
||||
name: '404',
|
||||
|
|
|
@ -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() {
|
||||
|
|
260
src/stores/pairlistConfig.ts
Normal file
260
src/stores/pairlistConfig.ts
Normal 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'],
|
||||
},
|
||||
},
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
}
|
16
src/types/backgroundtasks.ts
Normal file
16
src/types/backgroundtasks.ts
Normal 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;
|
||||
}
|
|
@ -16,3 +16,8 @@ export interface Exchange {
|
|||
export interface ExchangeListResult {
|
||||
exchanges: Exchange[];
|
||||
}
|
||||
|
||||
export interface ExchangeSelection {
|
||||
exchange: string;
|
||||
trade_mode: Partial<TradeMode>;
|
||||
}
|
||||
|
|
|
@ -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
86
src/types/pairlists.ts
Normal 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;
|
||||
}
|
|
@ -81,7 +81,7 @@ export enum TradingMode {
|
|||
}
|
||||
|
||||
export enum MarginMode {
|
||||
NONE = 'none',
|
||||
NONE = '',
|
||||
ISOLATED = 'isolated',
|
||||
// CROSS = 'cross',
|
||||
}
|
||||
|
|
7
src/views/PairlistConfigView.vue
Normal file
7
src/views/PairlistConfigView.vue
Normal file
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<PairlistConfigurator class="pt-4" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PairlistConfigurator from '@/components/ftbot/PairlistConfigurator.vue';
|
||||
</script>
|
|
@ -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: [],
|
||||
|
|
Loading…
Reference in New Issue
Block a user