mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-10 10:21:55 +00:00
commit
9fa15b72f3
82
src/components/BotEntry.vue
Normal file
82
src/components/BotEntry.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="d-flex align-items-center justify-content-between w-100">
|
||||
<span class="mr-2">{{ bot.botName || bot.botId }}</span>
|
||||
|
||||
<div class="align-items-center d-flex">
|
||||
<span class="ml-2 align-middle">{{
|
||||
allIsBotOnline[bot.botId] ? '🟢' : '🔴'
|
||||
}}</span>
|
||||
<b-form-checkbox
|
||||
v-model="autoRefreshLoc"
|
||||
class="ml-auto float-right mr-2 my-auto"
|
||||
title="AutoRefresh"
|
||||
variant="secondary"
|
||||
@change="changeEvent"
|
||||
>
|
||||
R
|
||||
</b-form-checkbox>
|
||||
<div v-if="!noButtons" class="d-flex flex-align-cent">
|
||||
<!-- <b-button class="ml-1" size="sm" title="Edit bot">
|
||||
<EditIcon :size="16" title="Edit Button" />
|
||||
</b-button> -->
|
||||
<b-button class="ml-1" size="sm" title="Delete bot" @click.prevent="clickRemoveBot(bot)">
|
||||
<DeleteIcon :size="16" title="Delete Bot" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import EditIcon from 'vue-material-design-icons/Cog.vue';
|
||||
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||
import { BotDescriptor, BotDescriptors } from '@/types';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({ components: { LoginModal, DeleteIcon, EditIcon } })
|
||||
export default class BotList extends Vue {
|
||||
@Prop({ default: false, type: Object }) bot!: BotDescriptor;
|
||||
|
||||
@Prop({ default: false, type: Boolean }) noButtons!: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allIsBotOnline];
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAutoRefresh];
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]: BotDescriptors;
|
||||
|
||||
@ftbot.Action removeBot;
|
||||
|
||||
@ftbot.Action selectBot;
|
||||
|
||||
get autoRefreshLoc() {
|
||||
return this.allAutoRefresh[this.bot.botId];
|
||||
}
|
||||
|
||||
set autoRefreshLoc(v) {
|
||||
// Dummy setter - Set via change event to avoid bouncing
|
||||
}
|
||||
|
||||
changeEvent(v) {
|
||||
this.$store.dispatch(`ftbot/${this.bot.botId}/setAutoRefresh`, v);
|
||||
}
|
||||
|
||||
clickRemoveBot(bot: BotDescriptor) {
|
||||
//
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(`Really remove (logout) from '${bot.botName}' (${bot.botId})?`)
|
||||
.then((value: boolean) => {
|
||||
if (value) {
|
||||
this.removeBot(bot.botId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
46
src/components/BotList.vue
Normal file
46
src/components/BotList.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div v-if="botCount > 0">
|
||||
<h3 v-if="!small">Available bots</h3>
|
||||
<b-list-group>
|
||||
<b-list-group-item
|
||||
v-for="bot in allAvailableBots"
|
||||
:key="bot.botId"
|
||||
:active="bot.botId === selectedBot"
|
||||
button
|
||||
:title="`${bot.botId} - ${bot.botName} - ${bot.botUrl}`"
|
||||
@click="selectBot(bot.botId)"
|
||||
>
|
||||
<bot-entry :bot="bot" :no-buttons="small" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
<LoginModal v-if="!small" class="mt-2" login-text="Add new bot" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import BotEntry from '@/components/BotEntry.vue';
|
||||
import { BotDescriptors } from '@/types';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({ components: { LoginModal, BotEntry } })
|
||||
export default class BotList extends Vue {
|
||||
@Prop({ default: false, type: Boolean }) small!: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]: number;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBot]: string;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allIsBotOnline]: Record<string, boolean>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]: BotDescriptors;
|
||||
|
||||
@ftbot.Action selectBot;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,6 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<form ref="form" novalidate @submit.stop.prevent="handleSubmit" @reset="handleReset">
|
||||
<b-form-group label="Bot Name" label-for="name-input">
|
||||
<b-form-input
|
||||
id="name-input"
|
||||
v-model="auth.botName"
|
||||
placeholder="Bot Name"
|
||||
@keydown.enter.native="handleOk"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
:state="urlState"
|
||||
label="API Url"
|
||||
|
@ -26,6 +34,7 @@
|
|||
v-model="auth.username"
|
||||
:state="nameState"
|
||||
required
|
||||
placeholder="Freqtrader"
|
||||
@keydown.enter.native="handleOk"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
|
@ -60,18 +69,29 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit, Prop } from 'vue-property-decorator';
|
||||
import { Action } from 'vuex-class';
|
||||
import userService from '@/shared/userService';
|
||||
import { setBaseUrl } from '@/shared/apiService';
|
||||
import { Action, namespace } from 'vuex-class';
|
||||
import { useUserService } from '@/shared/userService';
|
||||
|
||||
import { AuthPayload } from '@/types';
|
||||
import { AuthPayload, BotDescriptor } from '@/types';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
|
||||
const defaultURL = window.location.origin || 'http://localhost:8080';
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({})
|
||||
export default class Login extends Vue {
|
||||
@Action setLoggedIn;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.nextBotId]: string;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBot]: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action addBot!: (payload: BotDescriptor) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action selectBot!: (botId: string) => void;
|
||||
|
||||
@Prop({ default: false }) inModal!: boolean;
|
||||
|
||||
$refs!: {
|
||||
|
@ -79,6 +99,7 @@ export default class Login extends Vue {
|
|||
};
|
||||
|
||||
auth: AuthPayload = {
|
||||
botName: '',
|
||||
url: defaultURL,
|
||||
username: '',
|
||||
password: '',
|
||||
|
@ -127,12 +148,22 @@ export default class Login extends Vue {
|
|||
return;
|
||||
}
|
||||
this.errorMessage = '';
|
||||
const userService = useUserService(this.nextBotId);
|
||||
// Push the name to submitted names
|
||||
userService
|
||||
.login(this.auth)
|
||||
.then(() => {
|
||||
this.setLoggedIn(true);
|
||||
setBaseUrl(userService.getAPIUrl());
|
||||
const botId = this.nextBotId;
|
||||
this.addBot({
|
||||
botName: this.auth.botName,
|
||||
botId,
|
||||
botUrl: this.auth.url,
|
||||
});
|
||||
if (this.selectedBot === '') {
|
||||
console.log(`selecting bot ${botId}`);
|
||||
this.selectBot(botId);
|
||||
}
|
||||
|
||||
this.emitLoginResult(true);
|
||||
if (this.inModal === false) {
|
||||
if (typeof this.$route.query.redirect === 'string') {
|
||||
|
|
|
@ -24,8 +24,8 @@ import {
|
|||
LegendComponent,
|
||||
TimelineComponent,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
VisualMapPiecewiseComponent,
|
||||
} from 'echarts/components';
|
||||
|
@ -37,9 +37,9 @@ use([
|
|||
DataZoomComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
TimelineComponent,
|
||||
TitleComponent,
|
||||
ToolboxComponent,
|
||||
TooltipComponent,
|
||||
VisualMapComponent,
|
||||
VisualMapPiecewiseComponent,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-chart v-if="trades.length > 0" :option="chartOptions" autoresize :theme="getChartTheme" />
|
||||
<v-chart v-if="trades" :option="chartOptions" autoresize :theme="getChartTheme" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -20,7 +20,7 @@ import {
|
|||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
|
||||
import { ClosedTrade, CumProfitData } from '@/types';
|
||||
import { ClosedTrade, CumProfitData, CumProfitDataPerDate } from '@/types';
|
||||
|
||||
use([
|
||||
BarChart,
|
||||
|
@ -53,24 +53,54 @@ export default class CumProfitChart extends Vue {
|
|||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
botList: string[] = [];
|
||||
|
||||
get cumulativeData() {
|
||||
this.botList = [];
|
||||
const res: CumProfitData[] = [];
|
||||
const resD: CumProfitDataPerDate = {};
|
||||
const closedTrades = this.trades
|
||||
.slice()
|
||||
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
|
||||
let profit = 0.0;
|
||||
|
||||
for (let i = 0, len = closedTrades.length; i < len; i += 1) {
|
||||
const trade = closedTrades[i];
|
||||
|
||||
if (trade.close_timestamp && trade[this.profitColumn]) {
|
||||
profit += trade[this.profitColumn];
|
||||
res.push({ date: trade.close_timestamp, profit });
|
||||
if (!resD[trade.close_timestamp]) {
|
||||
// New timestamp
|
||||
resD[trade.close_timestamp] = { profit, [trade.botId]: profit };
|
||||
} else {
|
||||
// Add to existing profit
|
||||
resD[trade.close_timestamp].profit += profit;
|
||||
if (resD[trade.close_timestamp][trade.botId]) {
|
||||
resD[trade.close_timestamp][trade.botId] += profit;
|
||||
} else {
|
||||
resD[trade.close_timestamp][trade.botId] = profit;
|
||||
}
|
||||
}
|
||||
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
||||
if (!this.botList.includes(trade.botId)) {
|
||||
this.botList.push(trade.botId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
// console.log(resD);
|
||||
|
||||
return Object.entries(resD).map(([k, v]) => {
|
||||
const obj = { date: parseInt(k, 10), profit: v.profit };
|
||||
// TODO: The below could allow "lines" per bot"
|
||||
// this.botList.forEach((botId) => {
|
||||
// obj[botId] = v[botId];
|
||||
// });
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
get chartOptions(): EChartsOption {
|
||||
return {
|
||||
const chartOptionsLoc: EChartsOption = {
|
||||
title: {
|
||||
text: 'Cumulative Profit',
|
||||
show: this.showTitle,
|
||||
|
@ -151,6 +181,24 @@ export default class CumProfitChart extends Vue {
|
|||
},
|
||||
],
|
||||
};
|
||||
// TODO: maybe have profit lines per bot?
|
||||
// this.botList.forEach((botId: string) => {
|
||||
// console.log('bot', botId);
|
||||
// chartOptionsLoc.series.push({
|
||||
// type: 'line',
|
||||
// name: botId,
|
||||
// animation: true,
|
||||
// step: 'end',
|
||||
// lineStyle: {
|
||||
// color: this.getChartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// itemStyle: {
|
||||
// color: this.getChartTheme === 'dark' ? '#c2c2c2' : 'black',
|
||||
// },
|
||||
// // symbol: 'none',
|
||||
// });
|
||||
// });
|
||||
return chartOptionsLoc;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -52,6 +52,7 @@ const CHART_TRADE_COUNT = 'Trade Count';
|
|||
},
|
||||
})
|
||||
export default class HourlyChart extends Vue {
|
||||
// TODO: This chart is not used at the moment!
|
||||
@Prop({ required: true }) trades!: Trade[];
|
||||
|
||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
||||
|
|
|
@ -124,9 +124,12 @@ import { PlotConfig, EMPTY_PLOTCONFIG, IndicatorConfig } from '@/types';
|
|||
import { getCustomPlotConfig } from '@/shared/storage';
|
||||
import PlotIndicator from '@/components/charts/PlotIndicator.vue';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { AlertActions } from '@/store/modules/alerts';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
const alerts = namespace('alerts');
|
||||
|
||||
@Component({
|
||||
components: { PlotIndicator },
|
||||
})
|
||||
|
@ -202,6 +205,8 @@ export default class PlotConfigurator extends Vue {
|
|||
|
||||
@ftbot.Getter [BotStoreGetters.plotConfigName]!: string;
|
||||
|
||||
@alerts.Action [AlertActions.addAlert];
|
||||
|
||||
get plotConfigJson() {
|
||||
return JSON.stringify(this.plotConfig, null, 2);
|
||||
}
|
||||
|
@ -334,9 +339,14 @@ export default class PlotConfigurator extends Vue {
|
|||
}
|
||||
|
||||
async loadPlotConfigFromStrategy() {
|
||||
await this.getStrategyPlotConfig();
|
||||
this.plotConfig = this.strategyPlotConfig;
|
||||
this.emitPlotConfig();
|
||||
try {
|
||||
await this.getStrategyPlotConfig();
|
||||
this.plotConfig = this.strategyPlotConfig;
|
||||
this.emitPlotConfig();
|
||||
} catch (data) {
|
||||
//
|
||||
this.addAlert({ message: 'Failed to load Plot configuration from Strategy.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class TradesLogChart extends Vue {
|
|||
.sort((a, b) => (a.close_timestamp > b.close_timestamp ? 1 : -1));
|
||||
for (let i = 0, len = sortedTrades.length; i < len; i += 1) {
|
||||
const trade = sortedTrades[i];
|
||||
const entry = [i, (trade.profit_ratio * 100).toFixed(2), trade.pair];
|
||||
const entry = [i, (trade.profit_ratio * 100).toFixed(2), trade.pair, trade.botName];
|
||||
res.push(entry);
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ export default class TradesLogChart extends Vue {
|
|||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: (params) => {
|
||||
return `Profit %<br>${params[0].data[1]} % ${params[0].data[2]}`;
|
||||
return `Profit %<br>${params[0].data[1]} % ${params[0].data[2]} | ${params[0].data[3]}`;
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
|
|
119
src/components/ftbot/BotComarisonList.vue
Normal file
119
src/components/ftbot/BotComarisonList.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<b-table
|
||||
ref="tradesTable"
|
||||
small
|
||||
hover
|
||||
show-empty
|
||||
primary-key="botId"
|
||||
:items="tableItems"
|
||||
:fields="tableFields"
|
||||
>
|
||||
<template #cell(profitClosed)="row">
|
||||
<profit-pill
|
||||
v-if="row.item.profitClosed"
|
||||
:profit-ratio="row.item.profitClosedRatio"
|
||||
:profit-abs="row.item.profitClosed"
|
||||
:stake-currency="row.item.stakeCurrency"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(profitOpen)="row">
|
||||
<profit-pill
|
||||
v-if="row.item.profitClosed"
|
||||
:profit-ratio="row.item.profitOpenRatio"
|
||||
:profit-abs="row.item.profitOpen"
|
||||
:stake-currency="row.item.stakeCurrency"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(balance)="row">
|
||||
<div v-if="row.item.balance">
|
||||
<span :title="row.item.stakeCurrency"
|
||||
>{{ formatPrice(row.item.balance, row.item.stakeCurrencyDecimals) }}
|
||||
</span>
|
||||
<span clas="text-small">{{ row.item.stakeCurrency }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(winVsLoss)="row">
|
||||
<div v-if="row.item.losses !== undefined">
|
||||
<span class="text-profit">{{ row.item.wins }}</span> /
|
||||
<span class="text-loss">{{ row.item.losses }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import { BalanceInterface, BotDescriptors, BotState, ProfitInterface } from '@/types';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({ components: { ProfitPill } })
|
||||
export default class BotComparisonList extends Vue {
|
||||
@ftbot.Getter [MultiBotStoreGetters.allProfit]!: Record<string, ProfitInterface>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allOpenTradeCount]!: Record<string, number>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allBotState]!: Record<string, BotState>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allBalance]!: Record<string, BalanceInterface>;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.allAvailableBots]!: BotDescriptors;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
get tableItems() {
|
||||
const val: any[] = [];
|
||||
const summary = {
|
||||
botId: 'Summary',
|
||||
profitClosed: 0,
|
||||
profitClosedRatio: undefined,
|
||||
profitOpen: 0,
|
||||
profitOpenRatio: undefined,
|
||||
stakeCurrency: 'USDT',
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
};
|
||||
|
||||
Object.entries(this.allProfit).forEach(([k, v]) => {
|
||||
// TODO: handle one inactive bot ...
|
||||
val.push({
|
||||
botId: this.allAvailableBots[k].botName,
|
||||
trades: `${this.allOpenTradeCount[k]} / ${this.allBotState[k]?.max_open_trades || 'N/A'}`,
|
||||
profitClosed: v.profit_closed_coin,
|
||||
profitClosedRatio: v.profit_closed_ratio_sum || 0,
|
||||
stakeCurrency: this.allBotState[k]?.stake_currency || '',
|
||||
profitOpenRatio: v.profit_all_ratio_sum - v.profit_closed_ratio_sum,
|
||||
profitOpen: v.profit_all_coin - v.profit_closed_coin,
|
||||
wins: v.winning_trades,
|
||||
losses: v.losing_trades,
|
||||
balance: this.allBalance[k]?.total,
|
||||
stakeCurrencyDecimals: this.allBotState[k]?.stake_currency_decimals || 3,
|
||||
});
|
||||
if (v.profit_closed_coin !== undefined) {
|
||||
summary.profitClosed += v.profit_closed_coin;
|
||||
summary.profitOpen += v.profit_all_coin;
|
||||
summary.wins += v.winning_trades;
|
||||
summary.losses += v.losing_trades;
|
||||
// summary.decimals = this.allBotState[k]?.stake_currency_decimals || summary.decimals;
|
||||
}
|
||||
});
|
||||
val.push(summary);
|
||||
return val;
|
||||
}
|
||||
|
||||
tableFields: Record<string, string | Function>[] = [
|
||||
{ key: 'botId', label: 'Bot' },
|
||||
{ key: 'trades', label: 'Trades' },
|
||||
{ key: 'profitOpen', label: 'Open Profit' },
|
||||
{ key: 'profitClosed', label: 'Closed Profit' },
|
||||
{ key: 'balance', label: 'Balance' },
|
||||
{ key: 'winVsLoss', label: 'W/L' },
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,58 +1,56 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || isRunning"
|
||||
title="Start Trading"
|
||||
@click="startBot()"
|
||||
>
|
||||
<PlayIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="Stop Trading - Also stops handling open trades."
|
||||
@click="handleStopBot()"
|
||||
>
|
||||
<StopIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="StopBuy - Stops buying, but still handles open trades"
|
||||
@click="handleStopBuy()"
|
||||
>
|
||||
<PauseIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading"
|
||||
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
|
||||
@click="handleReloadConfig()"
|
||||
>
|
||||
<ReloadIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="botState && botState.forcebuy_enabled"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="Force Buy - Immediately buy an asset at an optional price. Sells are then handled according to strategy rules."
|
||||
@click="initiateForcebuy"
|
||||
>
|
||||
<ForceBuyIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="isWebserverMode && false"
|
||||
:disabled="isTrading"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
title="Start Trading mode"
|
||||
@click="startTrade()"
|
||||
>
|
||||
<PlayIcon />
|
||||
</button>
|
||||
<ForceBuyForm :modal-show="forcebuyShow" @close="$bvModal.hide('forcebuy-modal')" />
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || isRunning"
|
||||
title="Start Trading"
|
||||
@click="startBot()"
|
||||
>
|
||||
<PlayIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="Stop Trading - Also stops handling open trades."
|
||||
@click="handleStopBot()"
|
||||
>
|
||||
<StopIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="StopBuy - Stops buying, but still handles open trades"
|
||||
@click="handleStopBuy()"
|
||||
>
|
||||
<PauseIcon />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading"
|
||||
title="Reload Config - reloads configuration including strategy, resetting all settings changed on the fly."
|
||||
@click="handleReloadConfig()"
|
||||
>
|
||||
<ReloadIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="botState && botState.forcebuy_enabled"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
:disabled="!isTrading || !isRunning"
|
||||
title="Force Buy - Immediately buy an asset at an optional price. Sells are then handled according to strategy rules."
|
||||
@click="initiateForcebuy"
|
||||
>
|
||||
<ForceBuyIcon />
|
||||
</button>
|
||||
<button
|
||||
v-if="isWebserverMode && false"
|
||||
:disabled="isTrading"
|
||||
class="btn btn-secondary btn-sm ml-1"
|
||||
title="Start Trading mode"
|
||||
@click="startTrade()"
|
||||
>
|
||||
<PlayIcon />
|
||||
</button>
|
||||
<ForceBuyForm :modal-show="forcebuyShow" @close="$bvModal.hide('forcebuy-modal')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
||||
import { formatPrice } from '@/shared/formatters';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'DailyStats',
|
||||
|
@ -25,7 +26,7 @@ export default Vue.extend({
|
|||
DailyChart,
|
||||
},
|
||||
computed: {
|
||||
...mapState('ftbot', ['dailyStats']),
|
||||
...mapGetters('ftbot', [BotStoreGetters.dailyStats]),
|
||||
dailyFields() {
|
||||
return [
|
||||
{ key: 'date', label: 'Day' },
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
{{ comb.pair }}
|
||||
<span v-if="comb.locks" :title="comb.lockReason"> 🔒 </span>
|
||||
</div>
|
||||
<b-badge :variant="comb.profit > 0 ? 'success' : 'danger'" pill :title="comb.profitString">{{
|
||||
comb.profit ? formatPercent(comb.profit) : ''
|
||||
}}</b-badge>
|
||||
<TradeProfit v-if="comb.trade" :trade="comb.trade" />
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</template>
|
||||
|
@ -27,6 +25,7 @@ import { BotStoreGetters } from '@/store/modules/ftbot';
|
|||
import { Lock, Trade } from '@/types';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
|
@ -39,7 +38,7 @@ interface CombinedPairList {
|
|||
profit: number;
|
||||
}
|
||||
|
||||
@Component({})
|
||||
@Component({ components: { TradeProfit } })
|
||||
export default class PairSummary extends Vue {
|
||||
@Prop({ required: true }) pairlist!: string[];
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// TODO: evaluate if this is still used
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Trade } from '@/types';
|
||||
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<button class="m-1 btn btn-primary" @click="refreshAll(true)"><RefreshIcon /></button>
|
||||
|
||||
<b-form-checkbox
|
||||
v-model="autoRefreshLoc"
|
||||
class="ml-auto float-right mr-2 my-auto"
|
||||
title="AutoRefresh"
|
||||
switch
|
||||
>AutoRefresh</b-form-checkbox
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex flex-align-center ml-2">
|
||||
<b-form-checkbox
|
||||
v-model="autoRefreshLoc"
|
||||
class="ml-auto float-right my-auto"
|
||||
title="AutoRefresh"
|
||||
></b-form-checkbox>
|
||||
<b-button
|
||||
class="m-1"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
title="Auto Refresh All bots"
|
||||
@click="allRefreshFull"
|
||||
>
|
||||
<RefreshIcon :size="16" />
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Action, State } from 'vuex-class';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { namespace } from 'vuex-class';
|
||||
import RefreshIcon from 'vue-material-design-icons/Refresh.vue';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({ components: { RefreshIcon } })
|
||||
export default class ReloadControl extends Vue {
|
||||
|
@ -25,85 +31,19 @@ export default class ReloadControl extends Vue {
|
|||
|
||||
refreshIntervalSlow: number | null = null;
|
||||
|
||||
created() {
|
||||
if (this.loggedIn) {
|
||||
this.refreshOnce();
|
||||
this.refreshAll(true);
|
||||
}
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.startRefresh(false);
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
this.stopRefresh();
|
||||
}
|
||||
|
||||
@State loggedIn;
|
||||
|
||||
@State autoRefresh!: boolean;
|
||||
@ftbot.Getter [MultiBotStoreGetters.globalAutoRefresh]!: boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@Action setAutoRefresh!: (newValue: boolean) => void;
|
||||
@ftbot.Action setGlobalAutoRefresh!: (newValue: boolean) => void;
|
||||
|
||||
@Action refreshSlow;
|
||||
|
||||
@Action refreshFrequent;
|
||||
|
||||
@Action refreshAll;
|
||||
|
||||
@Action refreshOnce;
|
||||
@ftbot.Action allRefreshFull;
|
||||
|
||||
get autoRefreshLoc() {
|
||||
return this.autoRefresh;
|
||||
return this.globalAutoRefresh;
|
||||
}
|
||||
|
||||
set autoRefreshLoc(newValue: boolean) {
|
||||
this.setAutoRefresh(newValue);
|
||||
}
|
||||
|
||||
startRefresh(runNow: boolean) {
|
||||
if (this.loggedIn !== true) {
|
||||
console.log('Not logged in.');
|
||||
return;
|
||||
}
|
||||
console.log('Starting automatic refresh.');
|
||||
if (runNow) {
|
||||
this.refreshFrequent(false);
|
||||
}
|
||||
if (this.autoRefresh) {
|
||||
this.refreshInterval = window.setInterval(() => {
|
||||
this.refreshFrequent();
|
||||
}, 5000);
|
||||
}
|
||||
if (runNow) {
|
||||
this.refreshSlow(true);
|
||||
}
|
||||
if (this.autoRefresh) {
|
||||
this.refreshIntervalSlow = window.setInterval(() => {
|
||||
this.refreshSlow(false);
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
stopRefresh() {
|
||||
console.log('Stopping automatic refresh.');
|
||||
if (this.refreshInterval) {
|
||||
window.clearInterval(this.refreshInterval);
|
||||
}
|
||||
if (this.refreshIntervalSlow) {
|
||||
window.clearInterval(this.refreshIntervalSlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('autoRefresh')
|
||||
watchAutoRefresh(val) {
|
||||
if (val) {
|
||||
this.startRefresh(true);
|
||||
} else {
|
||||
this.stopRefresh();
|
||||
}
|
||||
this.setGlobalAutoRefresh(newValue);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
:empty-text="emptyText"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
primary-key="trade_id"
|
||||
primary-key="botTradeId"
|
||||
selectable
|
||||
select-mode="single"
|
||||
:filter="filterText"
|
||||
|
@ -33,7 +33,6 @@
|
|||
</b-button>
|
||||
</template>
|
||||
<template #cell(pair)="row">
|
||||
<ProfitSymbol :trade="row.item" />
|
||||
<span>
|
||||
{{
|
||||
`${row.item.pair}${
|
||||
|
@ -43,10 +42,7 @@
|
|||
</span>
|
||||
</template>
|
||||
<template #cell(profit)="row">
|
||||
{{ formatPercent(row.item.profit_ratio, 2) }}
|
||||
<small :title="row.item.stake_currency || stakeCurrency">
|
||||
{{ `(${formatPriceWithDecimals(row.item.profit_abs)})` }}
|
||||
</small>
|
||||
<trade-profit :trade="row.item" />
|
||||
</template>
|
||||
<template #cell(open_timestamp)="row">
|
||||
<DateTimeTZ :date="row.item.open_timestamp" />
|
||||
|
@ -87,11 +83,12 @@ import ForceSellIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
|||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import ProfitSymbol from './ProfitSymbol.vue';
|
||||
import TradeProfit from './TradeProfit.vue';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
|
||||
@Component({
|
||||
components: { ProfitSymbol, DeleteIcon, ForceSellIcon, DateTimeTZ },
|
||||
components: { ProfitSymbol, DeleteIcon, ForceSellIcon, DateTimeTZ, TradeProfit },
|
||||
})
|
||||
export default class TradeList extends Vue {
|
||||
$refs!: {
|
||||
|
@ -112,6 +109,8 @@ export default class TradeList extends Vue {
|
|||
|
||||
@Prop({ default: false }) showFilter!: boolean;
|
||||
|
||||
@Prop({ default: false, type: Boolean }) multiBotView!: boolean;
|
||||
|
||||
@Prop({ default: 'No Trades to show.' }) emptyText!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.detailTradeId]?: number;
|
||||
|
@ -157,6 +156,7 @@ export default class TradeList extends Vue {
|
|||
];
|
||||
|
||||
tableFields: Record<string, string | Function>[] = [
|
||||
this.multiBotView ? { key: 'botName', label: 'Bot' } : {},
|
||||
{ key: 'trade_id', label: 'ID' },
|
||||
{ key: 'pair', label: 'Pair' },
|
||||
{ key: 'amount', label: 'Amount' },
|
||||
|
|
34
src/components/ftbot/TradeProfit.vue
Normal file
34
src/components/ftbot/TradeProfit.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<profit-pill
|
||||
:profit-ratio="trade.profit_ratio"
|
||||
:profit-abs="trade.profit_abs"
|
||||
:profit-desc="profitDesc"
|
||||
stake-currency="USDT"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatPercent, timestampms } from '@/shared/formatters';
|
||||
import { Trade } from '@/types';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import ProfitPill from '@/components/general/ProfitPill.vue';
|
||||
|
||||
@Component({ components: { ProfitPill } })
|
||||
export default class TradeProfit extends Vue {
|
||||
@Prop({ required: true, type: Object }) trade!: Trade;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
get profitDesc(): string {
|
||||
let profit = `Current profit: ${formatPercent(this.trade.profit_ratio)} (${
|
||||
this.trade.profit_abs
|
||||
})`;
|
||||
profit += `\nOpen since: ${timestampms(this.trade.open_timestamp)}`;
|
||||
return profit;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
52
src/components/general/ProfitPill.vue
Normal file
52
src/components/general/ProfitPill.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div
|
||||
class="d-flex justify-content-center align-items-center profit-pill px-2"
|
||||
:class="isProfitable ? 'profit-pill-profit' : ''"
|
||||
:title="profitDesc"
|
||||
>
|
||||
{{ profitRatio ? formatPercent(profitRatio, 2) : '' }}
|
||||
<span class="ml-1" :class="profitRatio ? 'small' : ''" :title="stakeCurrency">
|
||||
{{ profitRatio ? '(' : '' }}{{ `${formatPrice(profitAbs, 3)}`
|
||||
}}{{ profitRatio ? ')' : ` ${stakeCurrency}` }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatPercent, formatPrice, timestampms } from '@/shared/formatters';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component({})
|
||||
export default class ProfitPill extends Vue {
|
||||
@Prop({ required: false, default: undefined, type: Number }) profitRatio?: number;
|
||||
|
||||
@Prop({ required: true, type: Number }) profitAbs!: number;
|
||||
|
||||
@Prop({ required: true, type: String }) stakeCurrency!: string;
|
||||
|
||||
@Prop({ required: false, default: '', type: String }) profitDesc!: string;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
get isProfitable() {
|
||||
return (
|
||||
(this.profitRatio !== undefined && this.profitRatio > 0) ||
|
||||
(this.profitRatio === undefined && this.profitAbs > 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profit-pill {
|
||||
background: $color-loss;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.profit-pill-profit {
|
||||
background: $color-profit;
|
||||
}
|
||||
</style>
|
|
@ -6,49 +6,101 @@
|
|||
<span class="navbar-brand-title d-sm-none d-md-inline">Freqtrade UI</span>
|
||||
</router-link>
|
||||
|
||||
<!-- TODO: For XS breakpoint, this should be here... -->
|
||||
<!-- <ReloadControl class="mr-3" /> -->
|
||||
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||
|
||||
<b-collapse id="nav-collapse" is-nav>
|
||||
<b-collapse id="nav-collapse" class="text-right text-md-center" is-nav>
|
||||
<b-navbar-nav>
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav navbar-dark" to="/trade"
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/trade"
|
||||
>Trade</router-link
|
||||
>
|
||||
<router-link
|
||||
v-if="!canRunBacktest"
|
||||
class="nav-link navbar-nav navbar-dark"
|
||||
to="/dashboard"
|
||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
||||
>Dashboard</router-link
|
||||
>
|
||||
<router-link class="nav-link navbar-nav navbar-dark" to="/graph">Graph</router-link>
|
||||
<router-link class="nav-link navbar-nav navbar-dark" to="/logs">Logs</router-link>
|
||||
<router-link v-if="canRunBacktest" class="nav-link navbar-nav navbar-dark" to="/backtest"
|
||||
<router-link class="nav-link navbar-nav" to="/graph">Graph</router-link>
|
||||
<router-link class="nav-link navbar-nav" to="/logs">Logs</router-link>
|
||||
<router-link v-if="canRunBacktest" class="nav-link navbar-nav" to="/backtest"
|
||||
>Backtest</router-link
|
||||
>
|
||||
<BootswatchThemeSelect />
|
||||
</b-navbar-nav>
|
||||
|
||||
<!-- Right aligned nav items -->
|
||||
<b-navbar-nav class="ml-auto">
|
||||
<li class="nav-item text-secondary mr-2">
|
||||
<b-navbar-nav class="ml-auto" menu-class="w-100">
|
||||
<!-- TODO This should show outside of the dropdown in XS mode -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<b-dropdown
|
||||
v-if="botCount > 1"
|
||||
size="sm"
|
||||
class="m-1"
|
||||
no-caret
|
||||
variant="info"
|
||||
toggle-class="d-flex align-items-center "
|
||||
menu-class="my-0 py-0"
|
||||
>
|
||||
<template #button-content>
|
||||
<BotEntry :bot="selectedBotObj" :no-buttons="true" />
|
||||
</template>
|
||||
<BotList :small="true" />
|
||||
</b-dropdown>
|
||||
<ReloadControl class="mr-3" />
|
||||
</div>
|
||||
<li class="d-none d-sm-block nav-item text-secondary mr-2">
|
||||
<b-nav-text class="verticalCenter small mr-2">
|
||||
{{ botName }}
|
||||
{{ botName || 'No bot selected' }}
|
||||
</b-nav-text>
|
||||
<b-nav-text class="verticalCenter">
|
||||
{{ isBotOnline ? 'Online' : 'Offline' }}
|
||||
</b-nav-text>
|
||||
</li>
|
||||
<li v-if="loggedIn" class="nav-item">
|
||||
<b-nav-item-dropdown right>
|
||||
<b-dropdown-item>V: {{ getUiVersion }}</b-dropdown-item>
|
||||
<li v-if="hasBots" class="nav-item">
|
||||
<!-- Hide dropdown on xs, instead show below -->
|
||||
<b-nav-item-dropdown right class="d-none d-sm-block">
|
||||
<template #button-content>
|
||||
<b-avatar size="2em" button>FT</b-avatar>
|
||||
</template>
|
||||
<b-dropdown-item>V: {{ getUiVersion }}</b-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/settings">Settings</router-link>
|
||||
<b-checkbox v-model="layoutLockedLocal" class="pl-5">Lock layout</b-checkbox>
|
||||
<b-dropdown-item @click="resetDynamicLayout">Reset Layout</b-dropdown-item>
|
||||
<router-link class="dropdown-item" to="/" @click.native="logout()"
|
||||
<router-link
|
||||
v-if="botCount === 1"
|
||||
class="dropdown-item"
|
||||
to="/"
|
||||
@click.native="clickLogout()"
|
||||
>Sign Out</router-link
|
||||
>
|
||||
</b-nav-item-dropdown>
|
||||
<div class="d-block d-sm-none">
|
||||
<!-- Visible only on XS -->
|
||||
<li class="nav-item text-secondary ml-2 d-sm-none d-flex justify-content-between">
|
||||
<span class="nav-link navbar-nav">V: {{ getUiVersion }}</span>
|
||||
<div class="d-flex">
|
||||
<b-nav-text class="verticalCenter small mr-2">
|
||||
{{ botName || 'No bot selected' }}
|
||||
</b-nav-text>
|
||||
<b-nav-text class="verticalCenter">
|
||||
{{ isBotOnline ? 'Online' : 'Offline' }}
|
||||
</b-nav-text>
|
||||
</div>
|
||||
</li>
|
||||
<router-link class="nav-link navbar-nav" to="/settings">Settings</router-link>
|
||||
<div class="d-flex nav-link justify-content-end">
|
||||
<b-checkbox v-model="layoutLockedLocal" class="ml-2"> Lock layout</b-checkbox>
|
||||
</div>
|
||||
|
||||
<b-nav-item class="nav-link navbar-nav" @click="resetDynamicLayout"
|
||||
>Reset Layout</b-nav-item
|
||||
>
|
||||
<router-link
|
||||
v-if="botCount === 1"
|
||||
class="nav-link navbar-nav"
|
||||
to="/"
|
||||
@click.native="clickLogout()"
|
||||
>Sign Out</router-link
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
<li v-else>
|
||||
<!-- should open Modal window! -->
|
||||
|
@ -63,27 +115,29 @@
|
|||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||
import LoginModal from '@/views/LoginModal.vue';
|
||||
import { State, Action, namespace, Getter } from 'vuex-class';
|
||||
import userService from '@/shared/userService';
|
||||
import { Action, namespace, Getter } from 'vuex-class';
|
||||
import BootswatchThemeSelect from '@/components/BootswatchThemeSelect.vue';
|
||||
import { LayoutActions, LayoutGetters } from '@/store/modules/layout';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import Favico from 'favico.js';
|
||||
import { OpenTradeVizOptions, SettingsGetters } from '@/store/modules/settings';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
import ReloadControl from '@/components/ftbot/ReloadControl.vue';
|
||||
import BotEntry from '@/components/BotEntry.vue';
|
||||
import BotList from '@/components/BotList.vue';
|
||||
import { BotDescriptor } from '@/types';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
const layoutNs = namespace('layout');
|
||||
const uiSettingsNs = namespace('uiSettings');
|
||||
|
||||
@Component({
|
||||
components: { LoginModal, BootswatchThemeSelect },
|
||||
components: { LoginModal, BootswatchThemeSelect, ReloadControl, BotEntry, BotList },
|
||||
})
|
||||
export default class NavBar extends Vue {
|
||||
pingInterval: number | null = null;
|
||||
|
||||
@State loggedIn!: boolean;
|
||||
|
||||
@State isBotOnline!: boolean;
|
||||
botSelectOpen = false;
|
||||
|
||||
@Action setLoggedIn;
|
||||
|
||||
|
@ -91,9 +145,17 @@ export default class NavBar extends Vue {
|
|||
|
||||
@Getter getUiVersion!: string;
|
||||
|
||||
@ftbot.Action ping;
|
||||
@ftbot.Action pingAll;
|
||||
|
||||
@ftbot.Action getState;
|
||||
@ftbot.Action allGetState;
|
||||
|
||||
@ftbot.Action logout;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.isBotOnline]!: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.hasBots]: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botName]: string;
|
||||
|
||||
|
@ -101,6 +163,8 @@ export default class NavBar extends Vue {
|
|||
|
||||
@ftbot.Getter [BotStoreGetters.canRunBacktest]!: boolean;
|
||||
|
||||
@ftbot.Getter [MultiBotStoreGetters.selectedBotObj]!: BotDescriptor;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
@layoutNs.Action [LayoutActions.resetDashboardLayout];
|
||||
|
@ -114,13 +178,13 @@ export default class NavBar extends Vue {
|
|||
favicon: Favico | undefined = undefined;
|
||||
|
||||
mounted() {
|
||||
this.ping();
|
||||
this.pingAll();
|
||||
this.loadUIVersion();
|
||||
this.pingInterval = window.setInterval(this.ping, 60000);
|
||||
this.pingInterval = window.setInterval(this.pingAll, 60000);
|
||||
|
||||
if (this.loggedIn) {
|
||||
if (this.hasBots) {
|
||||
// Query botstate - this will enable / disable certain modes
|
||||
this.getState();
|
||||
this.allGetState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,8 +194,9 @@ export default class NavBar extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
userService.logout();
|
||||
clickLogout(): void {
|
||||
this.logout();
|
||||
// TODO: This should be per bot
|
||||
this.setLoggedIn(false);
|
||||
}
|
||||
|
||||
|
@ -144,7 +209,6 @@ export default class NavBar extends Vue {
|
|||
}
|
||||
|
||||
setOpenTradesAsPill(tradeCount: number) {
|
||||
console.log('setPill', tradeCount);
|
||||
if (!this.favicon) {
|
||||
this.favicon = new Favico({
|
||||
animation: 'none',
|
||||
|
|
|
@ -3,9 +3,9 @@ import './plugins/bootstrap-vue';
|
|||
import App from './App.vue';
|
||||
import store from './store';
|
||||
import router from './router';
|
||||
import { init } from './shared/apiService';
|
||||
import { initApi } from './shared/apiService';
|
||||
|
||||
init(store);
|
||||
initApi(store);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
|
|
@ -2,8 +2,7 @@ import Vue from 'vue';
|
|||
import VueRouter, { RouteConfig } from 'vue-router';
|
||||
import Home from '@/views/Home.vue';
|
||||
import Error404 from '@/views/Error404.vue';
|
||||
|
||||
import userService from '@/shared/userService';
|
||||
import store from '@/store';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
|
@ -68,11 +67,8 @@ const router = new VueRouter({
|
|||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.name === 'Login' && userService.loggedIn()) {
|
||||
// No login if already logged in
|
||||
next({ path: '/' });
|
||||
}
|
||||
if (!to.meta?.allowAnonymous && !userService.loggedIn()) {
|
||||
const hasBots = store.getters['ftbot/hasBots'];
|
||||
if (!to.meta?.allowAnonymous && !hasBots) {
|
||||
// Forward to login if login is required
|
||||
next({
|
||||
path: '/login',
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import axios from 'axios';
|
||||
import userService from './userService';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: userService.apiBase,
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
});
|
||||
import { UserService } from './userService';
|
||||
|
||||
/**
|
||||
* Initialize api so store is accessible.
|
||||
* @param store Vuex store
|
||||
* Global store variable - keep a reference here to be able to emmit alerts
|
||||
*/
|
||||
export function init(store) {
|
||||
let globalStore;
|
||||
|
||||
export function useApi(userService: UserService, botId: string) {
|
||||
const api = axios.create({
|
||||
baseURL: userService.getBaseUrl(),
|
||||
timeout: 10000,
|
||||
withCredentials: true,
|
||||
});
|
||||
// Sent auth headers interceptor
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const custconfig = config;
|
||||
|
@ -59,7 +60,7 @@ export function init(store) {
|
|||
}
|
||||
if ((err.response && err.response.status === 500) || err.message === 'Network Error') {
|
||||
console.log('Bot not running...');
|
||||
store.dispatch('setIsBotOnline', false);
|
||||
globalStore.dispatch(`ftbot/${botId}/setIsBotOnline`, false);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -67,17 +68,17 @@ export function init(store) {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
};
|
||||
}
|
||||
|
||||
export function setBaseUrl(baseURL: string) {
|
||||
if (baseURL === null) {
|
||||
// Reset to "local" baseurl
|
||||
api.defaults.baseURL = userService.apiBase;
|
||||
} else if (!baseURL.endsWith(userService.apiBase)) {
|
||||
api.defaults.baseURL = `${baseURL}${userService.apiBase}`;
|
||||
} else {
|
||||
api.defaults.baseURL = `${baseURL}${userService.apiBase}`;
|
||||
}
|
||||
/**
|
||||
* Initialize api so store is accessible.
|
||||
* @param store Vuex store
|
||||
*/
|
||||
export function initApi(store) {
|
||||
globalStore = store;
|
||||
//
|
||||
}
|
||||
|
||||
setBaseUrl(userService.getAPIUrl());
|
||||
|
|
|
@ -1,60 +1,151 @@
|
|||
import axios from 'axios';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
|
||||
import { AuthPayload } from '@/types';
|
||||
import { AuthPayload, AuthResponse, BotDescriptors, AuthStorage, AuthStorageMulti } from '@/types';
|
||||
|
||||
const AUTH_REFRESH_TOKEN = 'auth_ref_token';
|
||||
const AUTH_ACCESS_TOKEN = 'auth_access_token';
|
||||
const AUTH_API_URL = 'auth_api_url';
|
||||
const apiBase = '/api/v1';
|
||||
const AUTH_LOGIN_INFO = 'ftAuthLoginInfo';
|
||||
const APIBASE = '/api/v1';
|
||||
|
||||
export default {
|
||||
apiBase,
|
||||
AUTH_API_URL,
|
||||
setAPIUrl(apiurl: string): void {
|
||||
localStorage.setItem(AUTH_API_URL, JSON.stringify(apiurl));
|
||||
},
|
||||
export class UserService {
|
||||
private botId: string;
|
||||
|
||||
setAccessToken(token: string): void {
|
||||
localStorage.setItem(AUTH_ACCESS_TOKEN, JSON.stringify(token));
|
||||
},
|
||||
constructor(botId: string) {
|
||||
console.log('botId', botId);
|
||||
this.botId = botId;
|
||||
}
|
||||
|
||||
setRefreshTokens(refreshToken: string): void {
|
||||
localStorage.setItem(AUTH_REFRESH_TOKEN, JSON.stringify(refreshToken));
|
||||
},
|
||||
/**
|
||||
* Stores info for current botId in the object of all bots.
|
||||
*/
|
||||
private storeLoginInfo(loginInfo: AuthStorage): void {
|
||||
const allInfo = UserService.getAllLoginInfos();
|
||||
allInfo[this.botId] = loginInfo;
|
||||
localStorage.setItem(AUTH_LOGIN_INFO, JSON.stringify(allInfo));
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
/**
|
||||
* Logout - removing info for this particular bot.
|
||||
*/
|
||||
private removeLoginInfo(): void {
|
||||
const info = UserService.getAllLoginInfos();
|
||||
delete info[this.botId];
|
||||
localStorage.setItem(AUTH_LOGIN_INFO, JSON.stringify(info));
|
||||
}
|
||||
|
||||
private setAccessToken(token: string): void {
|
||||
const loginInfo = this.getLoginInfo();
|
||||
loginInfo.accessToken = token;
|
||||
this.storeLoginInfo(loginInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store autorefresh preference for this bot instance
|
||||
* @param autoRefresh new autoRefresh value
|
||||
*/
|
||||
public setAutoRefresh(autoRefresh: boolean): void {
|
||||
const loginInfo = this.getLoginInfo();
|
||||
loginInfo.autoRefresh = autoRefresh;
|
||||
this.storeLoginInfo(loginInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve full logininfo object (for all registered bots)
|
||||
* @returns
|
||||
*/
|
||||
private static getAllLoginInfos(): AuthStorageMulti {
|
||||
const info = JSON.parse(localStorage.getItem(AUTH_LOGIN_INFO) || '{}');
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Login info object for the given bot
|
||||
* @returns Login Info object
|
||||
*/
|
||||
private getLoginInfo(): AuthStorage {
|
||||
const info = UserService.getAllLoginInfos();
|
||||
if (this.botId in info && 'apiUrl' in info[this.botId] && 'refreshToken' in info[this.botId]) {
|
||||
return info[this.botId];
|
||||
}
|
||||
return {
|
||||
botName: '',
|
||||
apiUrl: '',
|
||||
refreshToken: '',
|
||||
accessToken: '',
|
||||
autoRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
public static getAvailableBots(): BotDescriptors {
|
||||
const allInfo = UserService.getAllLoginInfos();
|
||||
const response: BotDescriptors = {};
|
||||
Object.entries(allInfo).forEach(([k, v]) => {
|
||||
response[k] = {
|
||||
botId: k,
|
||||
botName: v.botName,
|
||||
botUrl: v.apiUrl,
|
||||
};
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
public static getAvailableBotList(): string[] {
|
||||
const allInfo = UserService.getAllLoginInfos();
|
||||
return Object.keys(allInfo);
|
||||
}
|
||||
|
||||
public getAutoRefresh(): boolean {
|
||||
return this.getLoginInfo().autoRefresh;
|
||||
}
|
||||
|
||||
public getAccessToken(): string {
|
||||
return this.getLoginInfo().accessToken;
|
||||
}
|
||||
|
||||
private getRefreshToken() {
|
||||
return this.getLoginInfo().refreshToken;
|
||||
}
|
||||
|
||||
public loggedIn() {
|
||||
return this.getLoginInfo().refreshToken !== '';
|
||||
}
|
||||
|
||||
private getAPIUrl(): string {
|
||||
return this.getLoginInfo().apiUrl;
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
console.log('Logging out');
|
||||
localStorage.removeItem(AUTH_REFRESH_TOKEN);
|
||||
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||
localStorage.removeItem(AUTH_API_URL);
|
||||
},
|
||||
|
||||
async login(auth: AuthPayload) {
|
||||
this.removeLoginInfo();
|
||||
}
|
||||
|
||||
public async login(auth: AuthPayload) {
|
||||
// Login using username / password
|
||||
const result = await axios.post(
|
||||
const { data } = await axios.post<{}, AxiosResponse<AuthResponse>>(
|
||||
`${auth.url}/api/v1/token/login`,
|
||||
{},
|
||||
{
|
||||
auth: { ...auth },
|
||||
},
|
||||
);
|
||||
console.log(result.data);
|
||||
this.setAPIUrl(auth.url);
|
||||
if (result.data.access_token) {
|
||||
this.setAccessToken(result.data.access_token);
|
||||
if (data.access_token && data.refresh_token) {
|
||||
const obj: AuthStorage = {
|
||||
botName: auth.botName,
|
||||
apiUrl: auth.url,
|
||||
accessToken: data.access_token || '',
|
||||
refreshToken: data.refresh_token || '',
|
||||
autoRefresh: true,
|
||||
};
|
||||
this.storeLoginInfo(obj);
|
||||
}
|
||||
if (result.data.refresh_token) {
|
||||
this.setRefreshTokens(result.data.refresh_token);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
refreshToken(): Promise<string> {
|
||||
public refreshToken(): Promise<string> {
|
||||
console.log('Refreshing token...');
|
||||
const token = JSON.parse(localStorage.getItem(AUTH_REFRESH_TOKEN) || '{}');
|
||||
const token = this.getRefreshToken();
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(
|
||||
`${this.getAPIUrl()}${apiBase}/token/refresh`,
|
||||
.post<{}, AxiosResponse<AuthResponse>>(
|
||||
`${this.getAPIUrl()}${APIBASE}/token/refresh`,
|
||||
{},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
|
@ -78,22 +169,58 @@ export default {
|
|||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
loggedIn() {
|
||||
return localStorage.getItem(AUTH_ACCESS_TOKEN) !== null;
|
||||
},
|
||||
public getBaseUrl(): string {
|
||||
const baseURL = this.getAPIUrl();
|
||||
if (baseURL === null) {
|
||||
// Relative url
|
||||
return APIBASE;
|
||||
}
|
||||
if (!baseURL.endsWith(APIBASE)) {
|
||||
return `${baseURL}${APIBASE}`;
|
||||
}
|
||||
return `${baseURL}${APIBASE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call on startup to migrate old login info to new login
|
||||
*/
|
||||
public static migrateLogin() {
|
||||
// TODO: this is actually never called!
|
||||
const AUTH_REFRESH_TOKEN = 'auth_ref_token'; // Legacy key - do not use
|
||||
const AUTH_ACCESS_TOKEN = 'auth_access_token';
|
||||
const AUTH_API_URL = 'auth_api_url';
|
||||
const AUTO_REFRESH = 'ft_auto_refresh';
|
||||
|
||||
getAPIUrl(): string {
|
||||
const apiUrl = JSON.parse(localStorage.getItem(AUTH_API_URL) || '{}');
|
||||
return typeof apiUrl === 'object' ? '' : apiUrl;
|
||||
},
|
||||
const refreshToken = JSON.parse(localStorage.getItem(AUTH_REFRESH_TOKEN) || '{}');
|
||||
const accessToken = JSON.parse(localStorage.getItem(AUTH_ACCESS_TOKEN) || '{}');
|
||||
const autoRefresh: boolean = JSON.parse(localStorage.getItem(AUTO_REFRESH) || '{}');
|
||||
if (
|
||||
typeof apiUrl === 'string' &&
|
||||
typeof refreshToken === 'string' &&
|
||||
typeof accessToken === 'string'
|
||||
) {
|
||||
const loginInfo: AuthStorage = {
|
||||
botName: '',
|
||||
apiUrl,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
autoRefresh,
|
||||
};
|
||||
const x = new UserService('ftbot.0');
|
||||
x.storeLoginInfo(loginInfo);
|
||||
}
|
||||
|
||||
getAccessToken(): string {
|
||||
return JSON.parse(localStorage.getItem(AUTH_ACCESS_TOKEN) || '{}');
|
||||
},
|
||||
localStorage.removeItem(AUTH_REFRESH_TOKEN);
|
||||
localStorage.removeItem(AUTH_ACCESS_TOKEN);
|
||||
localStorage.removeItem(AUTH_API_URL);
|
||||
localStorage.removeItem(AUTO_REFRESH);
|
||||
}
|
||||
}
|
||||
|
||||
getRefreshToken() {
|
||||
return JSON.parse(localStorage.getItem(AUTH_REFRESH_TOKEN) || '{}');
|
||||
},
|
||||
};
|
||||
export function useUserService(botId: string) {
|
||||
const userservice = new UserService(botId);
|
||||
return userservice;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import userService from '@/shared/userService';
|
||||
import { getCurrentTheme, getTheme, storeCurrentTheme } from '@/shared/themes';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import ftbotModule, { BotStoreGetters } from './modules/ftbot';
|
||||
import axios from 'axios';
|
||||
import { UserService } from '@/shared/userService';
|
||||
import createBotStore from './modules/botStoreWrapper';
|
||||
import alertsModule from './modules/alerts';
|
||||
import layoutModule from './modules/layout';
|
||||
import settingsModule from './modules/settings';
|
||||
|
||||
const AUTO_REFRESH = 'ft_auto_refresh';
|
||||
|
||||
Vue.use(Vuex);
|
||||
const initCurrentTheme = getCurrentTheme();
|
||||
|
||||
export default new Vuex.Store({
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
alerts: alertsModule,
|
||||
layout: layoutModule,
|
||||
uiSettings: settingsModule,
|
||||
},
|
||||
state: {
|
||||
ping: '',
|
||||
loggedIn: userService.loggedIn(),
|
||||
refreshing: false,
|
||||
autoRefresh: JSON.parse(localStorage.getItem(AUTO_REFRESH) || '{}'),
|
||||
isBotOnline: false,
|
||||
currentTheme: initCurrentTheme,
|
||||
uiVersion: 'dev',
|
||||
},
|
||||
|
@ -38,31 +36,11 @@ export default new Vuex.Store({
|
|||
getUiVersion(state) {
|
||||
return state.uiVersion;
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
ftbot: ftbotModule,
|
||||
alerts: alertsModule,
|
||||
layout: layoutModule,
|
||||
uiSettings: settingsModule,
|
||||
loggedIn(state, getters) {
|
||||
return getters['ftbot/hasBots'];
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setPing(state, ping) {
|
||||
// console.log(ping);
|
||||
const now = Date.now();
|
||||
state.ping = `${ping.status} ${now.toString()}`;
|
||||
},
|
||||
setLoggedIn(state, loggedin: boolean) {
|
||||
state.loggedIn = loggedin;
|
||||
},
|
||||
setAutoRefresh(state, newRefreshValue: boolean) {
|
||||
state.autoRefresh = newRefreshValue;
|
||||
},
|
||||
setRefreshing(state, refreshing: boolean) {
|
||||
state.refreshing = refreshing;
|
||||
},
|
||||
setIsBotOnline(state, isBotOnline: boolean) {
|
||||
state.isBotOnline = isBotOnline;
|
||||
},
|
||||
mutateCurrentTheme(state, newTheme: string) {
|
||||
storeCurrentTheme(newTheme);
|
||||
state.currentTheme = newTheme;
|
||||
|
@ -75,23 +53,10 @@ export default new Vuex.Store({
|
|||
setCurrentTheme({ commit }, newTheme: string) {
|
||||
commit('mutateCurrentTheme', newTheme);
|
||||
},
|
||||
setAutoRefresh({ commit }, newRefreshValue) {
|
||||
commit('setAutoRefresh', newRefreshValue);
|
||||
localStorage.setItem(AUTO_REFRESH, JSON.stringify(newRefreshValue));
|
||||
},
|
||||
|
||||
setLoggedIn({ commit }, loggedin: boolean) {
|
||||
commit('setLoggedIn', loggedin);
|
||||
},
|
||||
setIsBotOnline({ commit, dispatch }, isOnline) {
|
||||
commit('setIsBotOnline', isOnline);
|
||||
if (isOnline === false) {
|
||||
console.log('disabling autorun');
|
||||
dispatch('setAutoRefresh', false);
|
||||
}
|
||||
},
|
||||
refreshOnce({ dispatch }) {
|
||||
dispatch('ftbot/getVersion');
|
||||
},
|
||||
async loadUIVersion({ commit }) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
try {
|
||||
|
@ -104,50 +69,15 @@ export default new Vuex.Store({
|
|||
}
|
||||
}
|
||||
},
|
||||
async refreshAll({ dispatch, state, commit }, forceUpdate = false) {
|
||||
if (state.refreshing) {
|
||||
return;
|
||||
}
|
||||
commit('setRefreshing', true);
|
||||
try {
|
||||
const updates: Promise<AxiosInstance>[] = [];
|
||||
updates.push(dispatch('refreshFrequent', false));
|
||||
updates.push(dispatch('refreshSlow', forceUpdate));
|
||||
updates.push(dispatch('ftbot/getDaily'));
|
||||
updates.push(dispatch('ftbot/getBalance'));
|
||||
|
||||
await Promise.all(updates);
|
||||
console.log('refreshing_end');
|
||||
} finally {
|
||||
commit('setRefreshing', false);
|
||||
}
|
||||
},
|
||||
async refreshSlow({ dispatch, commit, getters, state }, forceUpdate = false) {
|
||||
if (state.refreshing && !forceUpdate) {
|
||||
return;
|
||||
}
|
||||
// Refresh data only when needed
|
||||
if (forceUpdate || getters[`ftbot/${BotStoreGetters.refreshRequired}`]) {
|
||||
const updates: Promise<AxiosInstance>[] = [];
|
||||
updates.push(dispatch('ftbot/getPerformance'));
|
||||
updates.push(dispatch('ftbot/getProfit'));
|
||||
updates.push(dispatch('ftbot/getTrades'));
|
||||
/* white/blacklist might be refreshed more often as they are not expensive on the backend */
|
||||
updates.push(dispatch('ftbot/getWhitelist'));
|
||||
updates.push(dispatch('ftbot/getBlacklist'));
|
||||
|
||||
await Promise.all(updates);
|
||||
commit('ftbot/updateRefreshRequired', false);
|
||||
}
|
||||
},
|
||||
refreshFrequent({ dispatch }, slow = true) {
|
||||
if (slow) {
|
||||
dispatch('refreshSlow', false);
|
||||
}
|
||||
// Refresh data that's needed in near realtime
|
||||
dispatch('ftbot/getOpenTrades');
|
||||
dispatch('ftbot/getState');
|
||||
dispatch('ftbot/getLocks');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
UserService.migrateLogin();
|
||||
|
||||
store.registerModule('ftbot', createBotStore(store));
|
||||
Object.entries(UserService.getAvailableBots()).forEach(([k, v]) => {
|
||||
store.dispatch('ftbot/addBot', v);
|
||||
});
|
||||
store.dispatch('ftbot/selectFirstBot');
|
||||
store.dispatch('ftbot/startRefresh');
|
||||
export default store;
|
||||
|
|
350
src/store/modules/botStoreWrapper.ts
Normal file
350
src/store/modules/botStoreWrapper.ts
Normal file
|
@ -0,0 +1,350 @@
|
|||
import {
|
||||
BotDescriptor,
|
||||
BotDescriptors,
|
||||
DailyPayload,
|
||||
DailyRecord,
|
||||
DailyReturnValue,
|
||||
Trade,
|
||||
} from '@/types';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { BotStoreActions, BotStoreGetters, createBotSubStore } from './ftbot';
|
||||
|
||||
const AUTH_SELECTED_BOT = 'ftSelectedBot';
|
||||
|
||||
interface FTMultiBotState {
|
||||
selectedBot: string;
|
||||
availableBots: BotDescriptors;
|
||||
globalAutoRefresh: boolean;
|
||||
refreshing: boolean;
|
||||
refreshInterval: number | null;
|
||||
refreshIntervalSlow: number | null;
|
||||
}
|
||||
|
||||
export enum MultiBotStoreGetters {
|
||||
hasBots = 'hasBots',
|
||||
botCount = 'botCount',
|
||||
nextBotId = 'nextBotId',
|
||||
selectedBot = 'selectedBot',
|
||||
selectedBotObj = 'selectedBotObj',
|
||||
globalAutoRefresh = 'globalAutoRefresh',
|
||||
allAvailableBots = 'allAvailableBots',
|
||||
allAvailableBotsList = 'allAvailableBotsList',
|
||||
allTradesAllBots = 'allTradesAllBots',
|
||||
allOpenTradesAllBots = 'allOpenTradesAllBots',
|
||||
allDailyStatsAllBots = 'allDailyStatsAllBots',
|
||||
// Automatically created entries
|
||||
allIsBotOnline = 'allIsBotOnline',
|
||||
allAutoRefresh = 'allAutoRefresh',
|
||||
allProfit = 'allProfit',
|
||||
allOpenTrades = 'allOpenTrades',
|
||||
allOpenTradeCount = 'allOpenTradeCount',
|
||||
allClosedTrades = 'allClosedTrades',
|
||||
allBotState = 'allBotState',
|
||||
allBalance = 'allBalance',
|
||||
}
|
||||
|
||||
const createAllGetters = [
|
||||
'isBotOnline',
|
||||
'autoRefresh',
|
||||
'closedTrades',
|
||||
'profit',
|
||||
'openTrades',
|
||||
'openTradeCount',
|
||||
'closedTrades',
|
||||
'botState',
|
||||
'balance',
|
||||
];
|
||||
|
||||
export default function createBotStore(store) {
|
||||
const state: FTMultiBotState = {
|
||||
selectedBot: '',
|
||||
availableBots: {},
|
||||
globalAutoRefresh: true,
|
||||
refreshing: false,
|
||||
refreshInterval: null,
|
||||
refreshIntervalSlow: null,
|
||||
};
|
||||
|
||||
// All getters working on all bots should be prefixed with all.
|
||||
const getters = {
|
||||
[MultiBotStoreGetters.hasBots](state: FTMultiBotState): boolean {
|
||||
return Object.keys(state.availableBots).length > 0;
|
||||
},
|
||||
[MultiBotStoreGetters.botCount](state: FTMultiBotState): number {
|
||||
return Object.keys(state.availableBots).length;
|
||||
},
|
||||
[MultiBotStoreGetters.nextBotId](state: FTMultiBotState): string {
|
||||
let botCount = Object.keys(state.availableBots).length;
|
||||
|
||||
while (`ftbot.${botCount}` in state.availableBots) {
|
||||
botCount += 1;
|
||||
}
|
||||
return `ftbot.${botCount}`;
|
||||
},
|
||||
[MultiBotStoreGetters.selectedBot](state: FTMultiBotState): string {
|
||||
return state.selectedBot;
|
||||
},
|
||||
[MultiBotStoreGetters.selectedBotObj](state: FTMultiBotState): BotDescriptor {
|
||||
return state.availableBots[state.selectedBot];
|
||||
},
|
||||
[MultiBotStoreGetters.globalAutoRefresh](state: FTMultiBotState): boolean {
|
||||
return state.globalAutoRefresh;
|
||||
},
|
||||
[MultiBotStoreGetters.allAvailableBots](state: FTMultiBotState): BotDescriptors {
|
||||
return state.availableBots;
|
||||
},
|
||||
[MultiBotStoreGetters.allAvailableBotsList](state: FTMultiBotState): string[] {
|
||||
return Object.keys(state.availableBots);
|
||||
},
|
||||
[MultiBotStoreGetters.allTradesAllBots](state: FTMultiBotState, getters): Trade[] {
|
||||
let resp: Trade[] = [];
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
const trades = getters[`${botId}/${BotStoreGetters.trades}`].map((t) => ({ ...t, botId }));
|
||||
|
||||
resp = resp.concat(trades);
|
||||
});
|
||||
return resp;
|
||||
},
|
||||
[MultiBotStoreGetters.allOpenTradesAllBots](state: FTMultiBotState, getters): Trade[] {
|
||||
let resp: Trade[] = [];
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
const trades = getters[`${botId}/${BotStoreGetters.openTrades}`].map((t) => ({
|
||||
...t,
|
||||
}));
|
||||
|
||||
resp = resp.concat(trades);
|
||||
});
|
||||
return resp;
|
||||
},
|
||||
[MultiBotStoreGetters.allDailyStatsAllBots](state: FTMultiBotState, getters): DailyReturnValue {
|
||||
const resp: Record<string, DailyRecord> = {};
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
const x = getters[`${botId}/${BotStoreGetters.dailyStats}`]?.data?.forEach((d) => {
|
||||
if (!resp[d.date]) {
|
||||
resp[d.date] = { ...d };
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].abs_profit += d.abs_profit;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].fiat_value += d.fiat_value;
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
resp[d.date].trade_count += d.trade_count;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dailyReturn: DailyReturnValue = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
stake_currency: 'USDT',
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
fiat_display_currency: 'USD',
|
||||
data: Object.values(resp),
|
||||
};
|
||||
return dailyReturn;
|
||||
},
|
||||
};
|
||||
// Autocreate getters from botStores
|
||||
Object.keys(BotStoreGetters).forEach((e) => {
|
||||
getters[e] = (state, getters) => {
|
||||
return getters[`${state.selectedBot}/${e}`];
|
||||
};
|
||||
});
|
||||
|
||||
// Create selected getters
|
||||
createAllGetters.forEach((e: string) => {
|
||||
const getterName = `all${e.charAt(0).toUpperCase() + e.slice(1)}`;
|
||||
console.log('creating getter', e, getterName);
|
||||
getters[getterName] = (state, getters) => {
|
||||
const result = {};
|
||||
|
||||
getters.allAvailableBotsList.forEach((botId) => {
|
||||
result[botId] = getters[`${botId}/${e}`];
|
||||
});
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
const mutations = {
|
||||
selectBot(state: FTMultiBotState, botId: string) {
|
||||
if (botId in state.availableBots) {
|
||||
state.selectedBot = botId;
|
||||
} else {
|
||||
console.warn(`Botid ${botId} not available, but selected.`);
|
||||
}
|
||||
},
|
||||
setGlobalAutoRefresh(state, value: boolean) {
|
||||
state.globalAutoRefresh = value;
|
||||
},
|
||||
setRefreshing(state, refreshing: boolean) {
|
||||
state.refreshing = refreshing;
|
||||
},
|
||||
addBot(state: FTMultiBotState, bot: BotDescriptor) {
|
||||
state.availableBots[bot.botId] = bot;
|
||||
},
|
||||
removeBot(state: FTMultiBotState, botId: string) {
|
||||
if (botId in state.availableBots) {
|
||||
delete state.availableBots[botId];
|
||||
}
|
||||
},
|
||||
setRefreshInterval(state: FTMultiBotState, interval: number | null) {
|
||||
state.refreshInterval = interval;
|
||||
},
|
||||
setRefreshIntervalSlow(state: FTMultiBotState, interval: number | null) {
|
||||
state.refreshIntervalSlow = interval;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
// Actions automatically filled below
|
||||
addBot({ dispatch, getters, commit }, bot: BotDescriptor) {
|
||||
if (Object.keys(getters.allAvailableBots).includes(bot.botId)) {
|
||||
// throw 'Bot already present';
|
||||
// TODO: handle error!
|
||||
console.log('Bot already present');
|
||||
return;
|
||||
}
|
||||
console.log('add bot', bot);
|
||||
store.registerModule(['ftbot', bot.botId], createBotSubStore(bot.botId, bot.botName));
|
||||
dispatch(`${bot.botId}/botAdded`);
|
||||
commit('addBot', bot);
|
||||
},
|
||||
removeBot({ commit, getters, dispatch }, botId: string) {
|
||||
if (Object.keys(getters.allAvailableBots).includes(botId)) {
|
||||
dispatch(`${botId}/logout`);
|
||||
store.unregisterModule([`ftbot`, botId]);
|
||||
commit('removeBot', botId);
|
||||
} else {
|
||||
console.warn(`bot ${botId} not found! could not remove`);
|
||||
}
|
||||
},
|
||||
selectFirstBot({ commit, getters }) {
|
||||
if (getters.hasBots) {
|
||||
const selBotId = localStorage.getItem(AUTH_SELECTED_BOT);
|
||||
const firstBot = Object.keys(getters.allAvailableBots)[0];
|
||||
let selBot: string | undefined = firstBot;
|
||||
if (selBotId) {
|
||||
selBot = Object.keys(getters.allAvailableBots).find((x) => x === selBotId);
|
||||
}
|
||||
commit('selectBot', getters.allAvailableBots[selBot || firstBot].botId);
|
||||
}
|
||||
},
|
||||
selectBot({ commit }, botId: string) {
|
||||
localStorage.setItem(AUTH_SELECTED_BOT, botId);
|
||||
commit('selectBot', botId);
|
||||
},
|
||||
setGlobalAutoRefresh({ commit }, value: boolean) {
|
||||
commit('setGlobalAutoRefresh', value);
|
||||
},
|
||||
allRefreshFrequent({ dispatch, getters }, forceUpdate = false) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
if (
|
||||
getters[`${e}/${BotStoreGetters.refreshNow}`] &&
|
||||
(getters[MultiBotStoreGetters.globalAutoRefresh] || forceUpdate)
|
||||
) {
|
||||
// console.log('refreshing', e);
|
||||
dispatch(`${e}/${BotStoreActions.refreshFrequent}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
allRefreshSlow({ dispatch, getters }, forceUpdate = false) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
if (
|
||||
getters[`${e}/${BotStoreGetters.refreshNow}`] &&
|
||||
(getters[MultiBotStoreGetters.globalAutoRefresh] || forceUpdate)
|
||||
) {
|
||||
dispatch(`${e}/${BotStoreActions.refreshSlow}`, forceUpdate);
|
||||
}
|
||||
});
|
||||
},
|
||||
async allRefreshFull({ commit, dispatch, state }) {
|
||||
if (state.refreshing) {
|
||||
return;
|
||||
}
|
||||
commit('setRefreshing', true);
|
||||
try {
|
||||
// Ensure all bots status is correct.
|
||||
await dispatch('pingAll');
|
||||
const updates: Promise<AxiosInstance>[] = [];
|
||||
updates.push(dispatch('allRefreshFrequent', false));
|
||||
updates.push(dispatch('allRefreshSlow', true));
|
||||
// updates.push(dispatch('getDaily'));
|
||||
// updates.push(dispatch('getBalance'));
|
||||
|
||||
await Promise.all(updates);
|
||||
console.log('refreshing_end');
|
||||
} finally {
|
||||
commit('setRefreshing', false);
|
||||
}
|
||||
},
|
||||
|
||||
startRefresh({ state, dispatch, commit }) {
|
||||
console.log('Starting automatic refresh.');
|
||||
dispatch('allRefreshFull');
|
||||
|
||||
if (!state.refreshInterval) {
|
||||
// Set interval for refresh
|
||||
const refreshInterval = window.setInterval(() => {
|
||||
dispatch('allRefreshFrequent');
|
||||
}, 5000);
|
||||
commit('setRefreshInterval', refreshInterval);
|
||||
}
|
||||
if (!state.refreshIntervalSlow) {
|
||||
const refreshIntervalSlow = window.setInterval(() => {
|
||||
dispatch('allRefreshSlow', false);
|
||||
}, 60000);
|
||||
commit('setRefreshIntervalSlow', refreshIntervalSlow);
|
||||
}
|
||||
},
|
||||
stopRefresh({ state, commit }: { state: FTMultiBotState; commit: any }) {
|
||||
console.log('Stopping automatic refresh.');
|
||||
if (state.refreshInterval) {
|
||||
window.clearInterval(state.refreshInterval);
|
||||
commit('setRefreshInterval', null);
|
||||
}
|
||||
if (state.refreshIntervalSlow) {
|
||||
window.clearInterval(state.refreshIntervalSlow);
|
||||
commit('setRefreshIntervalSlow', null);
|
||||
}
|
||||
},
|
||||
|
||||
async pingAll({ getters, dispatch }) {
|
||||
await Promise.all(
|
||||
getters.allAvailableBotsList.map(async (e) => {
|
||||
await dispatch(`${e}/ping`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
allGetState({ getters, dispatch }) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
dispatch(`${e}/getState`);
|
||||
});
|
||||
},
|
||||
allGetDaily({ getters, dispatch }, payload: DailyPayload) {
|
||||
getters.allAvailableBotsList.forEach((e) => {
|
||||
dispatch(`${e}/getDaily`, payload);
|
||||
});
|
||||
},
|
||||
};
|
||||
// Autocreate Actions from botstores
|
||||
Object.keys(BotStoreActions).forEach((e) => {
|
||||
actions[e] = ({ state, dispatch, getters }, ...args) => {
|
||||
if (getters.hasBots) {
|
||||
return dispatch(`${state.selectedBot}/${e}`, ...args);
|
||||
}
|
||||
console.warn(`bot ${state.selectedBot} is not registered.`);
|
||||
return {};
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
namespaced: true,
|
||||
// modules: {
|
||||
// 'ftbot.0': createBotSubStore('ftbot.0'),
|
||||
// },
|
||||
state,
|
||||
mutations,
|
||||
|
||||
getters,
|
||||
actions,
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -17,6 +17,10 @@ import {
|
|||
} from '@/types';
|
||||
|
||||
export interface FtbotStateType {
|
||||
ping: string;
|
||||
isBotOnline: boolean;
|
||||
autoRefresh: boolean;
|
||||
refreshing: boolean;
|
||||
version: string;
|
||||
lastLogs: LogLine[];
|
||||
refreshRequired: boolean;
|
||||
|
@ -54,41 +58,46 @@ export interface FtbotStateType {
|
|||
backtestHistory: Record<string, StrategyBacktestResult>;
|
||||
}
|
||||
|
||||
const state: FtbotStateType = {
|
||||
version: '',
|
||||
lastLogs: [],
|
||||
refreshRequired: true,
|
||||
trades: [],
|
||||
openTrades: [],
|
||||
tradeCount: 0,
|
||||
performanceStats: [],
|
||||
whitelist: [],
|
||||
blacklist: [],
|
||||
profit: {},
|
||||
botState: undefined,
|
||||
balance: {},
|
||||
dailyStats: {},
|
||||
pairlistMethods: [],
|
||||
detailTradeId: undefined,
|
||||
selectedPair: '',
|
||||
candleData: {},
|
||||
history: {},
|
||||
strategyPlotConfig: undefined,
|
||||
customPlotConfig: {},
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
strategyList: [],
|
||||
strategy: {},
|
||||
pairlist: [],
|
||||
currentLocks: undefined,
|
||||
// backtesting
|
||||
backtestRunning: false,
|
||||
backtestProgress: 0.0,
|
||||
backtestStep: BacktestSteps.none,
|
||||
backtestTradeCount: 0,
|
||||
backtestResult: undefined,
|
||||
selectedBacktestResultKey: '',
|
||||
backtestHistory: {},
|
||||
const state = (): FtbotStateType => {
|
||||
return {
|
||||
ping: '',
|
||||
isBotOnline: false,
|
||||
autoRefresh: false,
|
||||
refreshing: false,
|
||||
version: '',
|
||||
lastLogs: [],
|
||||
refreshRequired: true,
|
||||
trades: [],
|
||||
openTrades: [],
|
||||
tradeCount: 0,
|
||||
performanceStats: [],
|
||||
whitelist: [],
|
||||
blacklist: [],
|
||||
profit: {},
|
||||
botState: undefined,
|
||||
balance: {},
|
||||
dailyStats: {},
|
||||
pairlistMethods: [],
|
||||
detailTradeId: undefined,
|
||||
selectedPair: '',
|
||||
candleData: {},
|
||||
history: {},
|
||||
strategyPlotConfig: undefined,
|
||||
customPlotConfig: {},
|
||||
plotConfigName: getPlotConfigName(),
|
||||
availablePlotConfigNames: getAllPlotConfigNames(),
|
||||
strategyList: [],
|
||||
strategy: {},
|
||||
pairlist: [],
|
||||
currentLocks: undefined,
|
||||
// backtesting
|
||||
backtestRunning: false,
|
||||
backtestProgress: 0.0,
|
||||
backtestStep: BacktestSteps.none,
|
||||
backtestTradeCount: 0,
|
||||
backtestResult: undefined,
|
||||
selectedBacktestResultKey: '',
|
||||
backtestHistory: {},
|
||||
};
|
||||
};
|
||||
|
||||
export default state;
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import { GridItemData } from 'vue-grid-layout';
|
||||
|
||||
export enum TradeLayout {
|
||||
botControls = 'g-botControls',
|
||||
multiPane = 'g-multiPane',
|
||||
openTrades = 'g-openTrades',
|
||||
tradeHistory = 'g-tradeHistory',
|
||||
tradeDetail = 'g-tradeDetail',
|
||||
logView = 'g-logView',
|
||||
chartView = 'g-chartView',
|
||||
}
|
||||
|
||||
export enum DashboardLayout {
|
||||
KPI = 'g-kpi',
|
||||
dailyChart = 'g-dailyChart',
|
||||
hourlyChart = 'g-hourlyChart',
|
||||
botComparison = 'g-botComparison',
|
||||
allOpenTrades = 'g-allOpenTrades',
|
||||
cumChartChart = 'g-cumChartChart',
|
||||
tradesLogChart = 'g-TradesLogChart',
|
||||
}
|
||||
|
@ -40,29 +38,27 @@ export enum LayoutMutations {
|
|||
}
|
||||
// Define default layouts
|
||||
const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
||||
{ i: TradeLayout.botControls, x: 0, y: 0, w: 3, h: 3 },
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 3, w: 3, h: 32 },
|
||||
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 3, h: 35 },
|
||||
{ i: TradeLayout.chartView, x: 3, y: 0, w: 9, h: 14 },
|
||||
{ i: TradeLayout.tradeDetail, x: 3, y: 19, w: 9, h: 6 },
|
||||
{ i: TradeLayout.openTrades, x: 3, y: 14, w: 9, h: 5 },
|
||||
{ i: TradeLayout.tradeHistory, x: 3, y: 25, w: 9, h: 10 },
|
||||
{ i: TradeLayout.logView, x: 0, y: 35, w: 12, h: 13 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
||||
{ i: DashboardLayout.KPI, x: 0, y: 0, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.dailyChart, x: 4, y: 0, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.hourlyChart, x: 4, y: 6, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 0, y: 6, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 8, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 8, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 8, y: 6, w: 4, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 12, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const DEFAULT_DASHBOARD_LAYOUT_SM: GridItemData[] = [
|
||||
{ i: DashboardLayout.KPI, x: 0, y: 0, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.dailyChart, x: 0, y: 6, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.hourlyChart, x: 0, y: 12, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 0, y: 18, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 24, w: 12, h: 4 },
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 12, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.allOpenTrades, x: 0, y: 6, w: 12, h: 8 },
|
||||
{ i: DashboardLayout.dailyChart, x: 0, y: 14, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.cumChartChart, x: 0, y: 20, w: 12, h: 6 },
|
||||
{ i: DashboardLayout.tradesLogChart, x: 0, y: 26, w: 12, h: 4 },
|
||||
];
|
||||
|
||||
const STORE_DASHBOARD_LAYOUT = 'ftDashboardLayout';
|
||||
|
|
|
@ -11,21 +11,25 @@ export enum OpenTradeVizOptions {
|
|||
export enum SettingsGetters {
|
||||
openTradesInTitle = 'openTradesInTitle',
|
||||
timezone = 'timezone',
|
||||
backgroundSync = 'backgroundSync',
|
||||
}
|
||||
|
||||
export enum SettingsActions {
|
||||
setOpenTradesInTitle = 'setOpenTradesInTitle',
|
||||
setTimeZone = 'setTimeZone',
|
||||
setBackgroundSync = 'setBackgroundSync',
|
||||
}
|
||||
|
||||
export enum SettingsMutations {
|
||||
setOpenTrades = 'setOpenTrades',
|
||||
setTimeZone = 'setTimeZone',
|
||||
setBackgroundSync = 'setBackgroundSync',
|
||||
}
|
||||
|
||||
export interface SettingsType {
|
||||
openTradesInTitle: string;
|
||||
timezone: string;
|
||||
backgroundSync: boolean;
|
||||
}
|
||||
|
||||
function getSettings() {
|
||||
|
@ -37,7 +41,7 @@ function getSettings() {
|
|||
}
|
||||
const storedSettings = getSettings();
|
||||
|
||||
function updateSetting(key: string, value: string) {
|
||||
function updateSetting(key: string, value: string | boolean) {
|
||||
const settings = getSettings() || {};
|
||||
settings[key] = value;
|
||||
localStorage.setItem(STORE_UI_SETTINGS, JSON.stringify(settings));
|
||||
|
@ -46,18 +50,22 @@ function updateSetting(key: string, value: string) {
|
|||
const state: SettingsType = {
|
||||
openTradesInTitle: storedSettings?.openTradesInTitle || OpenTradeVizOptions.showPill,
|
||||
timezone: storedSettings.timezone || 'UTC',
|
||||
backgroundSync: storedSettings.backgroundSync || true,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters: {
|
||||
[SettingsGetters.openTradesInTitle](state) {
|
||||
[SettingsGetters.openTradesInTitle](state): string {
|
||||
return state.openTradesInTitle;
|
||||
},
|
||||
[SettingsGetters.timezone](state) {
|
||||
[SettingsGetters.timezone](state): string {
|
||||
return state.timezone;
|
||||
},
|
||||
[SettingsGetters.backgroundSync](state): boolean {
|
||||
return state.backgroundSync;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
[SettingsMutations.setOpenTrades](state, value: string) {
|
||||
|
@ -68,6 +76,10 @@ export default {
|
|||
state.timezone = timezone;
|
||||
updateSetting('timezone', timezone);
|
||||
},
|
||||
[SettingsMutations.setBackgroundSync](state, backgroundSync: boolean) {
|
||||
state.backgroundSync = backgroundSync;
|
||||
updateSetting('backgroundSync', backgroundSync);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
[SettingsActions.setOpenTradesInTitle]({ commit }, locked: boolean) {
|
||||
|
@ -77,5 +89,8 @@ export default {
|
|||
setTimezone(timezone);
|
||||
commit(SettingsMutations.setTimeZone, timezone);
|
||||
},
|
||||
[SettingsActions.setBackgroundSync]({ commit }, timezone: string) {
|
||||
commit(SettingsMutations.setBackgroundSync, timezone);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,6 +4,13 @@
|
|||
background-color: #000000;
|
||||
}
|
||||
|
||||
.text-profit {
|
||||
color: $color-profit;
|
||||
}
|
||||
.text-loss {
|
||||
color: $color-loss;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
$bg-dark: #3c3c3c;
|
||||
$bg-darker: darken($bg-dark, 5%);
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
// variables created for the project and not overwrite of bootstrap
|
||||
$fontsize-small: 0.9rem;
|
||||
$fontsize-small: 0.8rem;
|
||||
|
||||
$color-profit: #12bb7b;
|
||||
$color-loss: #ef5350;
|
||||
|
|
|
@ -1,5 +1,33 @@
|
|||
export interface AuthPayload {
|
||||
botName: string;
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
}
|
||||
|
||||
export interface AuthStorage {
|
||||
botName: string;
|
||||
apiUrl: string;
|
||||
refreshToken: string;
|
||||
accessToken: string;
|
||||
autoRefresh: boolean;
|
||||
}
|
||||
|
||||
export interface AuthStorageMulti {
|
||||
[key: string]: AuthStorage;
|
||||
}
|
||||
|
||||
export interface BotDescriptor {
|
||||
botName: string;
|
||||
botId: string;
|
||||
botUrl: string;
|
||||
}
|
||||
|
||||
export interface BotDescriptors {
|
||||
[key: string]: BotDescriptor;
|
||||
}
|
||||
|
|
|
@ -18,4 +18,14 @@ export interface BalanceInterface {
|
|||
total: number;
|
||||
/** Balance in FIAT currency */
|
||||
value: number;
|
||||
/** Assumed starting capital */
|
||||
starting_capital: number;
|
||||
/** Change between starting capital and current value */
|
||||
starting_capital_ratio: number;
|
||||
starting_capital_pct: number;
|
||||
/** Assumed starting capital in FIAT currency */
|
||||
starting_capital_fiat: number;
|
||||
/** Change between starting capital and current value in fiat */
|
||||
starting_capital_fiat_ratio: number;
|
||||
starting_capital_fiat_pct: number;
|
||||
}
|
||||
|
|
|
@ -2,3 +2,7 @@ export interface CumProfitData {
|
|||
[date: string]: number;
|
||||
profit: number;
|
||||
}
|
||||
|
||||
export interface CumProfitDataPerDate {
|
||||
[key: number]: CumProfitData;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
export interface Trade {
|
||||
/**
|
||||
* corresponds to the UI (ftbot.1) - does NOT relate to the backend!
|
||||
*/
|
||||
botId: string;
|
||||
/**
|
||||
* Corresponds to the UI botID + tradeid. Does not relate to backend!
|
||||
*/
|
||||
botTradeId: string;
|
||||
/**
|
||||
* Given bot Name (in the UI). Does not relate to backend!
|
||||
*/
|
||||
botName: string;
|
||||
trade_id: number;
|
||||
pair: string;
|
||||
is_open: boolean;
|
||||
|
|
|
@ -13,53 +13,6 @@
|
|||
@layout-updated="layoutUpdated"
|
||||
@breakpoint-changed="breakpointChanged"
|
||||
>
|
||||
<GridItem
|
||||
:i="gridLayoutKPI.i"
|
||||
:x="gridLayoutKPI.x"
|
||||
:y="gridLayoutKPI.y"
|
||||
:w="gridLayoutKPI.w"
|
||||
:h="gridLayoutKPI.h"
|
||||
:min-w="3"
|
||||
:min-h="4"
|
||||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Bot KPI">
|
||||
<b-card-group deck>
|
||||
<b-card header="Open / Total trades">
|
||||
<b-card-text>
|
||||
<span class="text-primary">{{ openTrades.length }}</span> /
|
||||
<span class="text">{{ profit.trade_count }}</span>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<b-card header="Won / lost trades">
|
||||
<b-card-text>
|
||||
<span class="text-success">{{ profit.winning_trades }}</span> /
|
||||
<span class="text-danger">{{ profit.losing_trades }}</span>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<b-card header="Last trade">
|
||||
<b-card-text>{{ profit.latest_trade_date }}</b-card-text>
|
||||
</b-card>
|
||||
</b-card-group>
|
||||
<b-card-group deck class="mt-2">
|
||||
<b-card header="Best performing">
|
||||
<b-card-text>{{ profit.best_pair }}</b-card-text>
|
||||
</b-card>
|
||||
<b-card header="Total Balance">
|
||||
<b-card-text
|
||||
>{{ formatPrice(balance.total, botState.stake_currency_decimals || 8) }}
|
||||
{{ dailyStats.stake_currency }}</b-card-text
|
||||
>
|
||||
</b-card>
|
||||
<b-card v-if="profit.profit_closed_fiat" header="Total profit">
|
||||
<b-card-text
|
||||
>{{ formatPrice(profit.profit_closed_fiat, 2) }}
|
||||
{{ dailyStats.fiat_display_currency }}</b-card-text
|
||||
>
|
||||
</b-card>
|
||||
</b-card-group>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutDaily.i"
|
||||
:x="gridLayoutDaily.x"
|
||||
|
@ -70,22 +23,40 @@
|
|||
:min-h="4"
|
||||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Daily Profit">
|
||||
<DailyChart v-if="dailyStats.data" :daily-stats="dailyStats" :show-title="false" />
|
||||
<DraggableContainer :header="`Daily Profit ${botCount > 1 ? 'combined' : ''}`">
|
||||
<DailyChart
|
||||
v-if="allDailyStatsAllBots"
|
||||
:daily-stats="allDailyStatsAllBots"
|
||||
:show-title="false"
|
||||
/>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutHourly.i"
|
||||
:x="gridLayoutHourly.x"
|
||||
:y="gridLayoutHourly.y"
|
||||
:w="gridLayoutHourly.w"
|
||||
:h="gridLayoutHourly.h"
|
||||
:i="gridLayoutBotComparison.i"
|
||||
:x="gridLayoutBotComparison.x"
|
||||
:y="gridLayoutBotComparison.y"
|
||||
:w="gridLayoutBotComparison.w"
|
||||
:h="gridLayoutBotComparison.h"
|
||||
:min-w="3"
|
||||
:min-h="4"
|
||||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Hourly Profit">
|
||||
<HourlyChart :trades="closedTrades" :show-title="false" />
|
||||
<DraggableContainer header="Bot comparison">
|
||||
<bot-comparison-list />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutAllOpenTrades.i"
|
||||
:x="gridLayoutAllOpenTrades.x"
|
||||
:y="gridLayoutAllOpenTrades.y"
|
||||
:w="gridLayoutAllOpenTrades.w"
|
||||
:h="gridLayoutAllOpenTrades.h"
|
||||
:min-w="3"
|
||||
:min-h="4"
|
||||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Open Trades">
|
||||
<trade-list :active-trades="true" :trades="allOpenTradesAllBots" multi-bot-view />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
|
@ -99,7 +70,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Cumulative Profit">
|
||||
<CumProfitChart :trades="closedTrades" :show-title="false" />
|
||||
<CumProfitChart :trades="allTradesAllBots" :show-title="false" />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
|
@ -113,7 +84,7 @@
|
|||
drag-allow-from=".drag-header"
|
||||
>
|
||||
<DraggableContainer header="Trades Log">
|
||||
<TradesLogChart :trades="closedTrades" :show-title="false" />
|
||||
<TradesLogChart :trades="allTradesAllBots" :show-title="false" />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
</GridLayout>
|
||||
|
@ -127,9 +98,10 @@ import { namespace } from 'vuex-class';
|
|||
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
|
||||
|
||||
import DailyChart from '@/components/charts/DailyChart.vue';
|
||||
import HourlyChart from '@/components/charts/HourlyChart.vue';
|
||||
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
|
||||
import TradesLogChart from '@/components/charts/TradesLog.vue';
|
||||
import BotComparisonList from '@/components/ftbot/BotComarisonList.vue';
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||
|
||||
import {
|
||||
|
@ -138,15 +110,9 @@ import {
|
|||
LayoutActions,
|
||||
LayoutGetters,
|
||||
} from '@/store/modules/layout';
|
||||
import {
|
||||
Trade,
|
||||
DailyReturnValue,
|
||||
BalanceInterface,
|
||||
ProfitInterface,
|
||||
DailyPayload,
|
||||
BotState,
|
||||
} from '@/types';
|
||||
import { Trade, DailyReturnValue, DailyPayload, ClosedTrade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import { MultiBotStoreGetters } from '@/store/modules/botStoreWrapper';
|
||||
|
||||
const ftbot = namespace('ftbot');
|
||||
const layoutNs = namespace('layout');
|
||||
|
@ -156,34 +122,35 @@ const layoutNs = namespace('layout');
|
|||
GridLayout,
|
||||
GridItem,
|
||||
DailyChart,
|
||||
HourlyChart,
|
||||
CumProfitChart,
|
||||
TradesLogChart,
|
||||
BotComparisonList,
|
||||
TradeList,
|
||||
DraggableContainer,
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
@ftbot.Getter closedTrades!: Trade[];
|
||||
@ftbot.Getter [MultiBotStoreGetters.botCount]!: number;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.dailyStats]!: DailyReturnValue;
|
||||
@ftbot.Getter [MultiBotStoreGetters.allOpenTradesAllBots]!: Trade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.openTrades]!: Array<Trade>;
|
||||
@ftbot.Getter [MultiBotStoreGetters.allTradesAllBots]!: ClosedTrade[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.balance]!: BalanceInterface;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.botState]?: BotState;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.profit]!: ProfitInterface;
|
||||
@ftbot.Getter [MultiBotStoreGetters.allDailyStatsAllBots]!: Record<string, DailyReturnValue>;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.performanceStats]!: PerformanceEntry[];
|
||||
|
||||
@ftbot.Action getPerformance;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ftbot.Action getDaily!: (payload?: DailyPayload) => void;
|
||||
@ftbot.Action allGetDaily!: (payload?: DailyPayload) => void;
|
||||
|
||||
@ftbot.Action getTrades;
|
||||
|
||||
@ftbot.Action getOpenTrades;
|
||||
|
||||
@ftbot.Action getProfit;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getDashboardLayoutSm]!: GridItemData[];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getDashboardLayout]!: GridItemData[];
|
||||
|
@ -192,12 +159,6 @@ export default class Dashboard extends Vue {
|
|||
|
||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||
|
||||
@ftbot.Action getOpenTrades;
|
||||
|
||||
@ftbot.Action getBalance;
|
||||
|
||||
@ftbot.Action getProfit;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
localGridLayout: GridItemData[] = [];
|
||||
|
@ -232,16 +193,16 @@ export default class Dashboard extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
get gridLayoutKPI(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.KPI);
|
||||
}
|
||||
|
||||
get gridLayoutDaily(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.dailyChart);
|
||||
}
|
||||
|
||||
get gridLayoutHourly(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.hourlyChart);
|
||||
get gridLayoutBotComparison(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.botComparison);
|
||||
}
|
||||
|
||||
get gridLayoutAllOpenTrades(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, DashboardLayout.allOpenTrades);
|
||||
}
|
||||
|
||||
get gridLayoutCumChart(): GridItemData {
|
||||
|
@ -259,10 +220,9 @@ export default class Dashboard extends Vue {
|
|||
}
|
||||
|
||||
mounted() {
|
||||
this.getDaily({ timescale: 30 });
|
||||
this.allGetDaily({ timescale: 30 });
|
||||
this.getTrades();
|
||||
this.getOpenTrades();
|
||||
this.getBalance();
|
||||
this.getPerformance();
|
||||
this.getProfit();
|
||||
this.localGridLayout = [...this.getDashboardLayoutSm];
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<div class="container col-12 col-sm-6 col-lg-4">
|
||||
<bot-list />
|
||||
</div>
|
||||
<hr />
|
||||
<!-- <img alt="Freqtrade logo" src="../assets/freqtrade-logo.png" width="450px" class="my-5" /> -->
|
||||
<div alt="Freqtrade logo" class="logo-svg my-5 mx-auto" />
|
||||
<div>
|
||||
|
@ -19,7 +23,11 @@
|
|||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
import BotList from '@/components/BotList.vue';
|
||||
|
||||
@Component({
|
||||
components: { BotList },
|
||||
})
|
||||
export default class Home extends Vue {}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<b-button v-b-modal.modal-prevent-closing>Login</b-button>
|
||||
<b-modal id="modal-prevent-closing" ref="modal" title="Submit Your Name" @ok="handleOk">
|
||||
<Login id="loginForm" ref="loginForm" in-modal />
|
||||
<b-button v-b-modal.modal-prevent-closing>{{ loginText }}</b-button>
|
||||
<b-modal id="modal-prevent-closing" ref="modal" title="Login to your bot" @ok="handleOk">
|
||||
<Login id="loginForm" ref="loginForm" in-modal @loginResult="handleLoginResult" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
import Login from '@/components/Login.vue';
|
||||
|
||||
|
@ -18,12 +18,21 @@ import Login from '@/components/Login.vue';
|
|||
export default class LoginModal extends Vue {
|
||||
$refs!: {
|
||||
loginForm: HTMLFormElement;
|
||||
modal: HTMLElement;
|
||||
};
|
||||
|
||||
@Prop({ required: false, default: 'Login', type: String }) loginText!: string;
|
||||
|
||||
resetLogin() {
|
||||
// this.$refs.loginForm.resetLogin();
|
||||
}
|
||||
|
||||
handleLoginResult(result: boolean) {
|
||||
if (result) {
|
||||
(this.$refs.modal as any).hide();
|
||||
}
|
||||
}
|
||||
|
||||
handleOk(evt) {
|
||||
evt.preventDefault();
|
||||
this.$refs.loginForm.handleSubmit();
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
>
|
||||
<b-form-select v-model="timezoneLoc" :options="timezoneOptions"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group description="Keep background sync running while other bots are selected.">
|
||||
<b-checkbox v-model="backgroundSyncLocal">Background sync</b-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</b-card>
|
||||
</div>
|
||||
|
@ -46,10 +49,14 @@ export default class Template extends Vue {
|
|||
|
||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||
|
||||
@uiSettingsNs.Getter [SettingsGetters.backgroundSync]: boolean;
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setOpenTradesInTitle];
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setTimeZone];
|
||||
|
||||
@uiSettingsNs.Action [SettingsActions.setBackgroundSync];
|
||||
|
||||
openTradesOptions = [
|
||||
{ value: OpenTradeVizOptions.showPill, text: 'Show pill in icon' },
|
||||
{ value: OpenTradeVizOptions.asTitle, text: 'Show in title' },
|
||||
|
@ -82,6 +89,14 @@ export default class Template extends Vue {
|
|||
set layoutLockedLocal(value: boolean) {
|
||||
this.setLayoutLocked(value);
|
||||
}
|
||||
|
||||
get backgroundSyncLocal(): boolean {
|
||||
return this.backgroundSync;
|
||||
}
|
||||
|
||||
set backgroundSyncLocal(value: boolean) {
|
||||
this.setBackgroundSync(value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -9,19 +9,6 @@
|
|||
:is-draggable="!getLayoutLocked"
|
||||
@layout-updated="layoutUpdatedEvent"
|
||||
>
|
||||
<GridItem
|
||||
:i="gridLayoutBotControls.i"
|
||||
:x="gridLayoutBotControls.x"
|
||||
:y="gridLayoutBotControls.y"
|
||||
:w="gridLayoutBotControls.w"
|
||||
:h="gridLayoutBotControls.h"
|
||||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Bot Controls">
|
||||
<ReloadControl class="mt-2" />
|
||||
<BotControls />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutMultiPane.i"
|
||||
:x="gridLayoutMultiPane.x"
|
||||
|
@ -31,11 +18,14 @@
|
|||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Multi Pane">
|
||||
<b-tabs content-class="mt-3" class="mt-3">
|
||||
<b-tabs content-class="mt-3" class="mt-1">
|
||||
<b-tab title="Pairs combined" active>
|
||||
<PairSummary :pairlist="whitelist" :current-locks="currentLocks" :trades="openTrades" />
|
||||
</b-tab>
|
||||
<b-tab title="Status">
|
||||
<b-tab title="General">
|
||||
<div class="d-flex justify-content-center">
|
||||
<BotControls class="mt-1 mb-2" />
|
||||
</div>
|
||||
<BotStatus />
|
||||
</b-tab>
|
||||
<b-tab title="Performance">
|
||||
|
@ -106,18 +96,6 @@
|
|||
<TradeDetail :trade="tradeDetail"> </TradeDetail>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutLogView.i"
|
||||
:x="gridLayoutLogView.x"
|
||||
:y="gridLayoutLogView.y"
|
||||
:w="gridLayoutLogView.w"
|
||||
:h="gridLayoutLogView.h"
|
||||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Logs">
|
||||
<LogViewer />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
:i="gridLayoutChartView.i"
|
||||
:x="gridLayoutChartView.x"
|
||||
|
@ -145,20 +123,18 @@ import { Component, Vue } from 'vue-property-decorator';
|
|||
import { namespace } from 'vuex-class';
|
||||
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
|
||||
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
import Performance from '@/components/ftbot/Performance.vue';
|
||||
import Balance from '@/components/ftbot/Balance.vue';
|
||||
import BotControls from '@/components/ftbot/BotControls.vue';
|
||||
import BotStatus from '@/components/ftbot/BotStatus.vue';
|
||||
import Balance from '@/components/ftbot/Balance.vue';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
import DailyStats from '@/components/ftbot/DailyStats.vue';
|
||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||
import FTBotAPIPairList from '@/components/ftbot/FTBotAPIPairList.vue';
|
||||
import PairLockList from '@/components/ftbot/PairLockList.vue';
|
||||
import PairSummary from '@/components/ftbot/PairSummary.vue';
|
||||
import Performance from '@/components/ftbot/Performance.vue';
|
||||
import TradeDetail from '@/components/ftbot/TradeDetail.vue';
|
||||
import ReloadControl from '@/components/ftbot/ReloadControl.vue';
|
||||
import LogViewer from '@/components/ftbot/LogViewer.vue';
|
||||
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
|
||||
import CandleChartContainer from '@/components/charts/CandleChartContainer.vue';
|
||||
import TradeList from '@/components/ftbot/TradeList.vue';
|
||||
|
||||
import { Lock, Trade } from '@/types';
|
||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
|
@ -169,22 +145,20 @@ const layoutNs = namespace('layout');
|
|||
|
||||
@Component({
|
||||
components: {
|
||||
GridLayout,
|
||||
GridItem,
|
||||
DraggableContainer,
|
||||
TradeList,
|
||||
Performance,
|
||||
Balance,
|
||||
BotControls,
|
||||
BotStatus,
|
||||
Balance,
|
||||
CandleChartContainer,
|
||||
DailyStats,
|
||||
DraggableContainer,
|
||||
FTBotAPIPairList,
|
||||
GridItem,
|
||||
GridLayout,
|
||||
PairLockList,
|
||||
PairSummary,
|
||||
Performance,
|
||||
TradeDetail,
|
||||
ReloadControl,
|
||||
LogViewer,
|
||||
CandleChartContainer,
|
||||
TradeList,
|
||||
},
|
||||
})
|
||||
export default class Trading extends Vue {
|
||||
|
@ -214,10 +188,6 @@ export default class Trading extends Vue {
|
|||
return this.getTradingLayout;
|
||||
}
|
||||
|
||||
get gridLayoutBotControls(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.botControls);
|
||||
}
|
||||
|
||||
get gridLayoutMultiPane(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.multiPane);
|
||||
}
|
||||
|
@ -234,10 +204,6 @@ export default class Trading extends Vue {
|
|||
return findGridLayout(this.gridLayout, TradeLayout.tradeDetail);
|
||||
}
|
||||
|
||||
get gridLayoutLogView(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.logView);
|
||||
}
|
||||
|
||||
get gridLayoutChartView(): GridItemData {
|
||||
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user