Merge branch 'master' into vue-grid-merge

This commit is contained in:
Matthias 2020-09-03 06:58:06 +02:00
commit 6637135db8
16 changed files with 231 additions and 31 deletions

View File

@ -53,7 +53,7 @@
<script lang="ts">
import { Component, Vue, Emit, Prop } from 'vue-property-decorator';
import { Mutation } from 'vuex-class';
import { Action } from 'vuex-class';
import userService from '@/shared/userService';
import { setBaseUrl } from '@/shared/apiService';
@ -63,7 +63,7 @@ const defaultURL = 'http://localhost:8080';
@Component({})
export default class Login extends Vue {
@Mutation setLoggedIn;
@Action setLoggedIn;
@Prop({ default: false }) inModal!: boolean;

View File

@ -17,6 +17,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { AlertActions } from '@/store/modules/alerts';
const alerts = namespace('alerts');
@ -24,10 +25,10 @@ const alerts = namespace('alerts');
export default class BotAlerts extends Vue {
@alerts.State activeMessages;
@alerts.Mutation removeAlert;
@alerts.Action [AlertActions.removeAlert];
closeAlert() {
this.removeAlert();
this[AlertActions.removeAlert]();
}
}
</script>

View File

@ -3,9 +3,10 @@
<p>
Running Freqtrade <strong>{{ version }}</strong>
</p>
<p v-if="profit.profit_all_coin">
Avg Profit {{ profit.profit_all_coin.toFixed(2) }}% in {{ profit.trade_count }} Trades, with
an average duration of {{ profit.avg_duration }}. Best pair: {{ profit.best_pair }}.
<p>
Avg Profit {{ formatPercent(profit.profit_all_ratio_mean) }} (&sum;
{{ formatPercent(profit.profit_all_ratio_sum) }}) in {{ profit.trade_count }} Trades, with an
average duration of {{ profit.avg_duration }}. Best pair: {{ profit.best_pair }}.
</p>
<p v-if="profit.first_trade_timestamp">
First trade opened:
@ -38,6 +39,8 @@ import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { BotState } from '@/types';
import { formatPercent } from '@/shared/formatters';
const ftbot = namespace('ftbot');
@Component({})
@ -48,6 +51,8 @@ export default class BotStatus extends Vue {
@ftbot.State botState!: BotState;
formatPercent = formatPercent;
formatTimestamp(timestamp) {
return new Date(timestamp).toUTCString();
}

View File

@ -0,0 +1,39 @@
<template>
<div class="h-100 d-inline-block">
<div :class="isProfitable ? 'triangle-up' : 'triangle-down'"></div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import { Trade } from '@/types';
@Component({})
export default class ProfitSymbol extends Vue {
@Prop({ required: true }) trade!: Trade;
get isProfitable() {
console.log(this.trade);
const res = (this.trade.close_profit ?? 1) < 0 || (this.trade.current_profit ?? 1) < 0;
console.log(res);
return res;
}
}
</script>
<style scoped>
.triangle-up {
width: 0;
height: 0;
border-style: solid;
border-width: 0 0.5rem 0.9rem 0.5rem;
border-color: transparent transparent #00db58 transparent;
}
.triangle-down {
width: 0;
height: 0;
border-style: solid;
border-width: 0.9rem 0.5rem 0 0.5rem;
border-color: #ff0000 transparent transparent transparent;
}
</style>

View File

@ -2,6 +2,7 @@
<div class="h-100 d-flex overflow-auto">
<div>
<b-table
ref="tradesTable"
class="table-sm"
:items="trades"
:fields="tableFields"
@ -9,9 +10,11 @@
:empty-text="emptyText"
:per-page="perPage"
:current-page="currentPage"
primary-key="trade_id"
selectable
select-mode="single"
@row-contextmenu="handleContextMenuEvent"
@row-clicked="onRowClicked"
@row-selected="onRowSelected"
>
<template v-slot:cell(actions)="row">
@ -21,7 +24,7 @@
</b-button>
</template>
<template v-slot:cell(pair)="row">
<span class="mr-1" v-html="profitSymbol(row.item)"></span>
<ProfitSymbol :trade="row.item" />
<span>
{{ row.item.pair }}
</span>
@ -45,11 +48,18 @@ import { namespace } from 'vuex-class';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { formatPercent } from '@/shared/formatters';
import { Trade } from '@/types';
import ProfitSymbol from './ProfitSymbol.vue';
const ftbot = namespace('ftbot');
@Component({})
@Component({
components: { ProfitSymbol },
})
export default class TradeList extends Vue {
$refs!: {
tradesTable: HTMLFormElement;
};
@Prop({ required: true })
trades!: Array<Trade>;
@ -62,9 +72,9 @@ export default class TradeList extends Vue {
@Prop({ default: 'No Trades to show.' })
emptyText!: string;
@ftbot.State detailTradeId?: string;
@ftbot.State detailTradeId?: number;
@ftbot.Mutation setDetailTrade;
@ftbot.Action setDetailTrade;
@ftbot.Action forcesell!: (tradeid: string) => Promise<string>;
@ -72,6 +82,8 @@ export default class TradeList extends Vue {
currentPage = 1;
selectedItemIndex? = undefined;
get rows(): number {
return this.trades.length;
}
@ -98,11 +110,6 @@ export default class TradeList extends Vue {
...(this.activeTrades ? [{ key: 'actions' }] : []),
];
profitSymbol(item) {
// Red arrow / green circle
return item.close_profit < 0 || item.current_profit < 0 ? `&#x1F534;` : `&#x1F7E2;`;
}
forcesellHandler(item) {
this.$bvModal
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
@ -136,14 +143,34 @@ export default class TradeList extends Vue {
});
}
onRowSelected(items) {
onRowClicked(item, index) {
// Only allow single selection mode!
if (items.length > 0) {
this.setDetailTrade(items[0]);
if (
item &&
item.trade_id !== this.detailTradeId &&
!this.$refs.tradesTable.isRowSelected(index)
) {
this.setDetailTrade(item);
} else {
console.log('unsetting item');
this.setDetailTrade(null);
}
}
onRowSelected(items) {
// console.log('onRowSelected1');
// console.log(items);
if (this.detailTradeId) {
// console.log('onRowSelected2');
const itemIndex = this.trades.findIndex((v) => v.trade_id === this.detailTradeId);
if (itemIndex >= 0) {
this.$refs.tradesTable.selectRow(itemIndex);
} else {
console.log(`Unsetting item for tradeid ${this.selectedItemIndex}`);
this.selectedItemIndex = undefined;
}
}
}
}
</script>

View File

@ -44,7 +44,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import LoginModal from '@/views/LoginModal.vue';
import { State, Mutation, namespace } from 'vuex-class';
import { State, Action, namespace } from 'vuex-class';
import userService from '@/shared/userService';
import BootswatchThemeSelect from '@/components/BootswatchThemeSelect.vue';
@ -61,7 +61,7 @@ export default class NavBar extends Vue {
@State isBotOnline!: boolean;
@Mutation setLoggedIn;
@Action setLoggedIn;
@ftbot.Action ping;

View File

@ -43,6 +43,9 @@ export default new Vuex.Store({
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) {

View File

@ -1,20 +1,33 @@
export enum AlertActions {
addAlert = 'addAlert',
removeAlert = 'removeAlert',
}
export enum AlertMutations {
addAlert = 'addAlert',
removeAlert = 'removeAlert',
}
export default {
namespaced: true,
state: {
activeMessages: [],
},
mutations: {
addAlert(state, message) {
[AlertMutations.addAlert](state, message) {
console.log(`adding message '${message.message}' to message queue`);
state.activeMessages.push(message);
},
removeAlert(state) {
[AlertMutations.removeAlert](state) {
state.activeMessages.shift();
},
},
actions: {
addAlert({ commit }, message) {
commit('addAlert', message);
[AlertActions.addAlert]({ commit }, message) {
commit(AlertMutations.addAlert, message);
},
[AlertActions.removeAlert]({ commit }) {
commit(AlertMutations.removeAlert);
},
},
};

View File

@ -1,5 +1,5 @@
import { api } from '@/shared/apiService';
import { BotState, BlacklistPayload, ForcebuyPayload, Logs, DailyPayload } from '@/types';
import { BotState, BlacklistPayload, ForcebuyPayload, Logs, DailyPayload, Trade } from '@/types';
export enum UserStoreGetters {
openTrades = 'openTrades',
@ -76,7 +76,7 @@ export default {
updateLogs(state, logs: Logs) {
state.lastLogs = logs.logs;
},
setDetailTrade(state, trade) {
setDetailTrade(state, trade: Trade) {
state.detailTradeId = trade ? trade.trade_id : null;
},
},
@ -90,6 +90,9 @@ export default {
})
.catch(console.error);
},
setDetailTrade({ commit }, trade: Trade) {
commit('setDetailTrade', trade);
},
getTrades({ commit }) {
return api
.get('/trades')

View File

@ -10,6 +10,7 @@ export enum TradeLayout {
}
export enum DashboardLayout {
KPI = 'g-kpi',
dailyChart = 'g-dailyChart',
hourlyChart = 'g-hourlyChart',
cumChartChart = 'g-cumChartChart',
@ -26,8 +27,9 @@ const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
];
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
{ i: DashboardLayout.dailyChart, x: 0, y: 0, w: 4, h: 6 },
{ i: DashboardLayout.hourlyChart, x: 4, y: 0, w: 4, h: 6 },
{ 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 },
];

View File

@ -12,4 +12,6 @@ export interface DailyRecord {
export interface DailyReturnValue {
data: Array<DailyRecord>;
fiat_display_currency: string;
stake_currency: string;
}

View File

@ -2,4 +2,5 @@ export * from './auth';
export * from './blacklist';
export * from './chart';
export * from './daily';
export * from './profit';
export * from './types';

25
src/types/profit.ts Normal file
View File

@ -0,0 +1,25 @@
export interface ProfitInterface {
profit_closed_coin: number;
profit_closed_percent_mean: number;
profit_closed_ratio_mean: number;
profit_closed_percent_sum: number;
profit_closed_ratio_sum: number;
profit_closed_fiat: number;
profit_all_coin: number;
profit_all_percent_mean: number;
profit_all_ratio_mean: number;
profit_all_percent_sum: number;
profit_all_ratio_sum: number;
profit_all_fiat: number;
trade_count: number;
closed_trade_count: number;
first_trade_date: string;
first_trade_timestamp: number;
latest_trade_date: string;
latest_trade_timestamp: number;
avg_duration: string;
best_pair: string;
best_rate: number;
winning_trades: number;
losing_trades: number;
}

View File

@ -90,6 +90,7 @@ export interface Trade {
fee_close_currency?: string;
current_rate?: number;
current_profit?: number;
sell_reason?: string;
min_rate?: number;
max_rate?: number;

View File

@ -6,6 +6,56 @@
:vertical-compact="false"
@layout-updated="layoutUpdatedEvent"
>
<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">
<div>
<b-card-group deck>
<b-card header="Open / Total trades">
<b-card-text>
<span class="text-primary">{{ openTrades.length }}</span> /
<span class="text-secondary">{{ 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>
</div>
<div class="mt-3">
<b-card-group deck>
<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) }} {{ 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) }}
{{ dailyStats.fiat_display_currency }}</b-card-text
>
</b-card>
</b-card-group>
</div>
</DraggableContainer>
</GridItem>
<GridItem
:i="gridLayoutDaily.i"
:x="gridLayoutDaily.x"
@ -52,6 +102,8 @@
</template>
<script lang="ts">
import { formatPrice } from '@/shared/formatters';
import { Component, Vue } from 'vue-property-decorator';
import { namespace } from 'vuex-class';
import { GridLayout, GridItem, GridItemData } from 'vue-grid-layout';
@ -61,8 +113,8 @@ import HourlyChart from '@/components/charts/HourlyChart.vue';
import CumProfitChart from '@/components/charts/CumProfitChart.vue';
import DraggableContainer from '@/components/layout/DraggableContainer.vue';
import { Trade, DailyReturnValue } from '@/types';
import { DashboardLayout, findGridLayout } from '@/store/modules/layout';
import { Trade, DailyReturnValue, BalanceInterface, ProfitInterface } from '@/types';
const ftbot = namespace('ftbot');
const layoutNs = namespace('layout');
@ -82,6 +134,16 @@ export default class Dashboard extends Vue {
@ftbot.State dailyStats!: DailyReturnValue;
@ftbot.Getter openTrades!: Array<Trade>;
@ftbot.State balance!: BalanceInterface;
@ftbot.State profit!: ProfitInterface;
@ftbot.State performanceStats!: Array<PerformanceEntry>;
@ftbot.Action getPerformance;
@ftbot.Action getDaily;
@ftbot.Action getTrades;
@ -90,10 +152,22 @@ export default class Dashboard extends Vue {
@layoutNs.Mutation setDashboardLayout;
@ftbot.Action getOpenTrades;
@ftbot.Action getBalance;
@ftbot.Action getProfit;
formatPrice = formatPrice;
get gridLayout() {
return this.getDashboardLayout;
}
get gridLayoutKPI(): GridItemData {
return findGridLayout(this.gridLayout, DashboardLayout.KPI);
}
get gridLayoutDaily(): GridItemData {
return findGridLayout(this.gridLayout, DashboardLayout.dailyChart);
}
@ -109,6 +183,10 @@ export default class Dashboard extends Vue {
mounted() {
this.getDaily();
this.getTrades();
this.getOpenTrades();
this.getBalance();
this.getPerformance();
this.getProfit();
}
layoutUpdatedEvent(newLayout) {

View File

@ -150,7 +150,7 @@ const layoutNs = namespace('layout');
},
})
export default class Trading extends Vue {
@ftbot.State detailTradeId!: string;
@ftbot.State detailTradeId!: number;
@ftbot.Getter openTrades!: Trade[];