mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-23 11:35:14 +00:00
commit
07bae3ae6b
|
@ -13,8 +13,12 @@
|
||||||
"cy:open-ct": "cypress open-ct",
|
"cy:open-ct": "cypress open-ct",
|
||||||
"cy:run-ct": "cypress run-ct"
|
"cy:run-ct": "cypress run-ct"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"vue-demi": "0.12.1"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cypress/vue": "^2.2.3",
|
"@cypress/vue": "^2.2.3",
|
||||||
|
"@vue/composition-api": "^1.4.3",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-vue": "^2.21.2",
|
"bootstrap-vue": "^2.21.2",
|
||||||
|
@ -27,6 +31,7 @@
|
||||||
"humanize-duration": "^3.27.1",
|
"humanize-duration": "^3.27.1",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-class-component": "^7.2.5",
|
"vue-class-component": "^7.2.5",
|
||||||
|
"vue-demi": "0.12.1",
|
||||||
"vue-echarts": "^6.0.0",
|
"vue-echarts": "^6.0.0",
|
||||||
"vue-grid-layout": "^2.3.12",
|
"vue-grid-layout": "^2.3.12",
|
||||||
"vue-material-design-icons": "^5.0.0",
|
"vue-material-design-icons": "^5.0.0",
|
||||||
|
@ -34,7 +39,8 @@
|
||||||
"vue-router": "^3.5.3",
|
"vue-router": "^3.5.3",
|
||||||
"vue-select": "^3.16.0",
|
"vue-select": "^3.16.0",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^3.6.2",
|
||||||
"vuex-class": "^0.3.2"
|
"vuex-class": "^0.3.2",
|
||||||
|
"vuex-composition-helpers": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/webpack-dev-server": "^1.8.0",
|
"@cypress/webpack-dev-server": "^1.8.0",
|
||||||
|
@ -49,7 +55,6 @@
|
||||||
"@vue/cli-plugin-unit-jest": "~4.5.15",
|
"@vue/cli-plugin-unit-jest": "~4.5.15",
|
||||||
"@vue/cli-plugin-vuex": "~4.5.15",
|
"@vue/cli-plugin-vuex": "~4.5.15",
|
||||||
"@vue/cli-service": "~4.5.15",
|
"@vue/cli-service": "~4.5.15",
|
||||||
"@vue/composition-api": "^1.4.3",
|
|
||||||
"@vue/eslint-config-airbnb": "^5.1.0",
|
"@vue/eslint-config-airbnb": "^5.1.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@vue/eslint-config-prettier": "^6.0.0",
|
||||||
"@vue/eslint-config-typescript": "^5.1.0",
|
"@vue/eslint-config-typescript": "^5.1.0",
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
<div id="app" class="d-flex flex-column vh-100">
|
<div id="app" class="d-flex flex-column vh-100">
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<Body class="flex-fill overflow-auto" />
|
<Body class="flex-fill overflow-auto" />
|
||||||
|
<NavFooter />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import NavBar from '@/components/layout/NavBar.vue';
|
import NavBar from '@/components/layout/NavBar.vue';
|
||||||
|
import NavFooter from '@/components/layout/NavFooter.vue';
|
||||||
import Body from '@/components/layout/Body.vue';
|
import Body from '@/components/layout/Body.vue';
|
||||||
import { namespace } from 'vuex-class';
|
import { namespace } from 'vuex-class';
|
||||||
import { SettingsGetters } from './store/modules/settings';
|
import { SettingsGetters } from './store/modules/settings';
|
||||||
|
@ -17,7 +19,7 @@ import StoreModules from './store/storeSubModules';
|
||||||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { NavBar, Body },
|
components: { NavBar, Body, NavFooter },
|
||||||
})
|
})
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||||
|
|
|
@ -81,7 +81,7 @@ export default class CumProfitChart extends Vue {
|
||||||
resD[trade.close_timestamp][trade.botId] = profit;
|
resD[trade.close_timestamp][trade.botId] = profit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(trade.close_date, profit);
|
// console.log(trade.close_date, profit);
|
||||||
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
res.push({ date: trade.close_timestamp, profit, [trade.botId]: profit });
|
||||||
if (!this.botList.includes(trade.botId)) {
|
if (!this.botList.includes(trade.botId)) {
|
||||||
this.botList.push(trade.botId);
|
this.botList.push(trade.botId);
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
import { useGetters } from 'vuex-composition-helpers';
|
||||||
import { Getter } from 'vuex-class';
|
import { ref, defineComponent, computed, ComputedRef } from '@vue/composition-api';
|
||||||
import ECharts from 'vue-echarts';
|
import ECharts from 'vue-echarts';
|
||||||
import { EChartsOption } from 'echarts';
|
// import { EChartsOption } from 'echarts';
|
||||||
|
|
||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
@ -38,46 +38,46 @@ use([
|
||||||
const CHART_ABS_PROFIT = 'Absolute profit';
|
const CHART_ABS_PROFIT = 'Absolute profit';
|
||||||
const CHART_TRADE_COUNT = 'Trade Count';
|
const CHART_TRADE_COUNT = 'Trade Count';
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
'v-chart': ECharts,
|
'v-chart': ECharts,
|
||||||
},
|
},
|
||||||
})
|
props: {
|
||||||
export default class DailyChart extends Vue {
|
dailyStats: {
|
||||||
@Prop({ required: true }) dailyStats!: DailyReturnValue;
|
type: Object as () => DailyReturnValue,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
setup(props) {
|
||||||
|
const { getChartTheme } = useGetters(['getChartTheme']);
|
||||||
@Getter getChartTheme!: string;
|
const absoluteMin: ComputedRef<number> = computed(() =>
|
||||||
|
props.dailyStats.data.reduce(
|
||||||
get absoluteMin() {
|
|
||||||
return Number(
|
|
||||||
this.dailyStats.data.reduce(
|
|
||||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||||
this.dailyStats.data[0]?.abs_profit,
|
props.dailyStats.data[0]?.abs_profit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
get absoluteMax() {
|
const absoluteMax: ComputedRef<number> = computed(() =>
|
||||||
return Number(
|
props.dailyStats.data.reduce(
|
||||||
this.dailyStats.data.reduce(
|
|
||||||
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
||||||
this.dailyStats.data[0]?.abs_profit,
|
props.dailyStats.data[0]?.abs_profit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
// : Ref<EChartsOption>
|
||||||
|
const dailyChartOptions = ref({
|
||||||
get dailyChartOptions(): EChartsOption {
|
|
||||||
return {
|
|
||||||
title: {
|
title: {
|
||||||
text: 'Daily profit',
|
text: 'Daily profit',
|
||||||
show: this.showTitle,
|
show: props.showTitle,
|
||||||
},
|
},
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
dataset: {
|
dataset: {
|
||||||
dimensions: ['date', 'abs_profit', 'trade_count'],
|
dimensions: ['date', 'abs_profit', 'trade_count'],
|
||||||
source: this.dailyStats.data,
|
source: props.dailyStats.data,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
|
@ -92,10 +92,12 @@ export default class DailyChart extends Vue {
|
||||||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||||
right: '5%',
|
right: '5%',
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: [
|
||||||
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
inverse: true,
|
inverse: true,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
visualMap: [
|
visualMap: [
|
||||||
{
|
{
|
||||||
dimension: 1,
|
dimension: 1,
|
||||||
|
@ -104,12 +106,12 @@ export default class DailyChart extends Vue {
|
||||||
pieces: [
|
pieces: [
|
||||||
{
|
{
|
||||||
max: 0.0,
|
max: 0.0,
|
||||||
min: this.absoluteMin,
|
min: absoluteMin.value,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
min: 0.0,
|
min: 0.0,
|
||||||
max: this.absoluteMax,
|
max: absoluteMax.value,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -149,9 +151,14 @@ export default class DailyChart extends Vue {
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dailyChartOptions,
|
||||||
|
getChartTheme,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
174
src/components/ftbot/CustomTradeList.vue
Normal file
174
src/components/ftbot/CustomTradeList.vue
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-100 overflow-auto p-1">
|
||||||
|
<b-list-group id="tradeList">
|
||||||
|
<b-list-group-item
|
||||||
|
v-for="trade in filteredTrades"
|
||||||
|
:key="trade.trade_id"
|
||||||
|
class="border border-secondary rounded my-05 px-1"
|
||||||
|
@click="tradeClick(trade)"
|
||||||
|
>
|
||||||
|
<CustomTradeListEntry :trade="trade" :stake-currency-decimals="stakeCurrencyDecimals" />
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
|
||||||
|
<span v-if="trades.length == 0" class="mt-5">{{ emptyText }}</span>
|
||||||
|
|
||||||
|
<div class="w-100 d-flex justify-content-between mt-1">
|
||||||
|
<b-pagination
|
||||||
|
v-if="!activeTrades"
|
||||||
|
v-model="currentPage"
|
||||||
|
:total-rows="rows"
|
||||||
|
:per-page="perPage"
|
||||||
|
aria-controls="tradeList"
|
||||||
|
></b-pagination>
|
||||||
|
<b-input
|
||||||
|
v-if="showFilter"
|
||||||
|
v-model="filterText"
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter"
|
||||||
|
size="sm"
|
||||||
|
style="width: unset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||||
|
import { namespace } from 'vuex-class';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||||
|
import { MultiDeletePayload, MultiForcesellPayload, Trade } from '@/types';
|
||||||
|
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||||
|
import ForceSellIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
||||||
|
import ActionIcon from 'vue-material-design-icons/GestureTap.vue';
|
||||||
|
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
|
import StoreModules from '@/store/storeSubModules';
|
||||||
|
import CustomTradeListEntry from '@/components/ftbot/CustomTradeListEntry.vue';
|
||||||
|
import TradeProfit from './TradeProfit.vue';
|
||||||
|
|
||||||
|
const ftbot = namespace(StoreModules.ftbot);
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
DeleteIcon,
|
||||||
|
ForceSellIcon,
|
||||||
|
ActionIcon,
|
||||||
|
DateTimeTZ,
|
||||||
|
TradeProfit,
|
||||||
|
CustomTradeListEntry,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class CustomTradeList extends Vue {
|
||||||
|
$refs!: {
|
||||||
|
tradesTable: HTMLFormElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
formatPercent = formatPercent;
|
||||||
|
|
||||||
|
formatPrice = formatPrice;
|
||||||
|
|
||||||
|
@Prop({ required: true }) trades!: Array<Trade>;
|
||||||
|
|
||||||
|
@Prop({ default: 'Trades' }) title!: string;
|
||||||
|
|
||||||
|
@Prop({ required: false, default: '' }) stakeCurrency!: string;
|
||||||
|
|
||||||
|
@Prop({ default: false }) activeTrades!: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: false }) showFilter!: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: false, type: Boolean }) multiBotView!: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: 'No Trades to show.' }) emptyText!: string;
|
||||||
|
|
||||||
|
@Prop({ default: 3, type: Number }) stakeCurrencyDecimals!: number;
|
||||||
|
|
||||||
|
@ftbot.Action setDetailTrade;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ftbot.Action forceSellMulti!: (payload: MultiForcesellPayload) => Promise<string>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@ftbot.Action deleteTradeMulti!: (payload: MultiDeletePayload) => Promise<string>;
|
||||||
|
|
||||||
|
currentPage = 1;
|
||||||
|
|
||||||
|
selectedItemIndex? = undefined;
|
||||||
|
|
||||||
|
filterText = '';
|
||||||
|
|
||||||
|
get rows(): number {
|
||||||
|
return this.trades.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
perPage = this.activeTrades ? 200 : 25;
|
||||||
|
|
||||||
|
get filteredTrades() {
|
||||||
|
return this.trades.slice(
|
||||||
|
(this.currentPage - 1) * this.perPage,
|
||||||
|
this.currentPage * this.perPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatPriceWithDecimals(price) {
|
||||||
|
return formatPrice(price, this.stakeCurrencyDecimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
forcesellHandler(item: Trade, ordertype: string | undefined = undefined) {
|
||||||
|
this.$bvModal
|
||||||
|
.msgBoxConfirm(`Really forcesell trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||||
|
.then((value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
const payload: MultiForcesellPayload = {
|
||||||
|
tradeid: String(item.trade_id),
|
||||||
|
botId: item.botId,
|
||||||
|
};
|
||||||
|
if (ordertype) {
|
||||||
|
payload.ordertype = ordertype;
|
||||||
|
}
|
||||||
|
this.forceSellMulti(payload)
|
||||||
|
.then((xxx) => console.log(xxx))
|
||||||
|
.catch((error) => console.log(error.response));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleContextMenuEvent(item, index, event) {
|
||||||
|
// stop browser context menu from appearing
|
||||||
|
if (!this.activeTrades) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
// log the selected item to the console
|
||||||
|
console.log(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTradeHandler(item) {
|
||||||
|
console.log(item);
|
||||||
|
this.$bvModal
|
||||||
|
.msgBoxConfirm(`Really delete trade ${item.trade_id} (Pair ${item.pair})?`)
|
||||||
|
.then((value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
const payload: MultiDeletePayload = {
|
||||||
|
tradeid: String(item.trade_id),
|
||||||
|
botId: item.botId,
|
||||||
|
};
|
||||||
|
this.deleteTradeMulti(payload).catch((error) => console.log(error.response));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeClick(trade) {
|
||||||
|
this.setDetailTrade(trade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.my-05 {
|
||||||
|
margin-top: 0.125rem;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
</style>
|
66
src/components/ftbot/CustomTradeListEntry.vue
Normal file
66
src/components/ftbot/CustomTradeListEntry.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div
|
||||||
|
class="px-1 d-flex flex-row flex-fill text-left justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span class="mr-1 font-weight-bold">{{ trade.pair }}</span>
|
||||||
|
<small class="text-secondary">(#{{ trade.trade_id }})</small>
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
<DateTimeTZ :date="trade.open_timestamp" :date-only="true" title="open Date" />
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<trade-profit class="col-5" :trade="trade" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from '@vue/composition-api';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { formatPercent, formatPrice } from '@/shared/formatters';
|
||||||
|
import { Trade } from '@/types';
|
||||||
|
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
|
import TradeProfit from './TradeProfit.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
DateTimeTZ,
|
||||||
|
TradeProfit,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
trade: {
|
||||||
|
type: Object as () => Trade,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
stakeCurrencyDecimals: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showDetails: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
formatPrice,
|
||||||
|
formatPercent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-body {
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
.table-sm {
|
||||||
|
font-size: $fontsize-small;
|
||||||
|
}
|
||||||
|
.btn-xs {
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,7 +17,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" />
|
<TradeProfit v-if="comb.trade && !backtestMode" :trade="comb.trade" />
|
||||||
<ProfitPill v-if="backtestMode && comb.tradeCount > 0" :profit-ratio="comb.profit" />
|
<ProfitPill
|
||||||
|
v-if="backtestMode && comb.tradeCount > 0"
|
||||||
|
:profit-ratio="comb.profit"
|
||||||
|
:stake-currency="stakeCurrency"
|
||||||
|
/>
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
</template>
|
</template>
|
||||||
|
@ -63,6 +67,8 @@ export default class PairSummary extends Vue {
|
||||||
|
|
||||||
@ftbot.Getter [BotStoreGetters.selectedPair]!: string;
|
@ftbot.Getter [BotStoreGetters.selectedPair]!: string;
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||||
|
|
||||||
timestampms = timestampms;
|
timestampms = timestampms;
|
||||||
|
|
||||||
formatPercent = formatPercent;
|
formatPercent = formatPercent;
|
||||||
|
|
69
src/components/ftbot/TradeActions.vue
Normal file
69
src/components/ftbot/TradeActions.vue
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<b-button
|
||||||
|
v-if="botApiVersion <= 1.1"
|
||||||
|
class="btn-xs text-left"
|
||||||
|
size="sm"
|
||||||
|
title="Forcesell"
|
||||||
|
@click="$emit('forceSell', trade)"
|
||||||
|
>
|
||||||
|
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
v-if="botApiVersion > 1.1"
|
||||||
|
class="btn-xs text-left"
|
||||||
|
size="sm"
|
||||||
|
title="Forcesell limit"
|
||||||
|
@click="$emit('forceSell', trade, 'limit')"
|
||||||
|
>
|
||||||
|
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell limit
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
v-if="botApiVersion > 1.1"
|
||||||
|
class="btn-xs text-left mt-1"
|
||||||
|
size="sm"
|
||||||
|
title="Forcesell market"
|
||||||
|
@click="$emit('forceSell', trade, 'market')"
|
||||||
|
>
|
||||||
|
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell market
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
class="btn-xs text-left mt-1"
|
||||||
|
size="sm"
|
||||||
|
title="Delete trade"
|
||||||
|
@click="$emit('deleteTrade', trade)"
|
||||||
|
>
|
||||||
|
<DeleteIcon :size="16" title="Delete trade" class="mr-1" />
|
||||||
|
Delete
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Trade } from '@/types';
|
||||||
|
import { defineComponent } from '@vue/composition-api';
|
||||||
|
import DeleteIcon from 'vue-material-design-icons/Delete.vue';
|
||||||
|
import ForceSellIcon from 'vue-material-design-icons/CloseBoxMultiple.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'TradeActions',
|
||||||
|
components: { DeleteIcon, ForceSellIcon },
|
||||||
|
props: {
|
||||||
|
botApiVersion: {
|
||||||
|
type: Number,
|
||||||
|
default: 1.0,
|
||||||
|
},
|
||||||
|
trade: {
|
||||||
|
type: Object as () => Trade,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['forceSell', 'deleteTrade'],
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
|
@ -5,6 +5,9 @@
|
||||||
<h5 class="detail-header">General</h5>
|
<h5 class="detail-header">General</h5>
|
||||||
<ValuePair description="TradeId">{{ trade.trade_id }}</ValuePair>
|
<ValuePair description="TradeId">{{ trade.trade_id }}</ValuePair>
|
||||||
<ValuePair description="Pair">{{ trade.pair }}</ValuePair>
|
<ValuePair description="Pair">{{ trade.pair }}</ValuePair>
|
||||||
|
<ValuePair description="Stake">{{
|
||||||
|
formatPriceCurrency(trade.stake_amount, stakeCurrency)
|
||||||
|
}}</ValuePair>
|
||||||
<ValuePair description="Open date">{{ timestampms(trade.open_timestamp) }}</ValuePair>
|
<ValuePair description="Open date">{{ timestampms(trade.open_timestamp) }}</ValuePair>
|
||||||
<ValuePair v-if="trade.buy_tag" description="Buy tag">{{ trade.buy_tag }}</ValuePair>
|
<ValuePair v-if="trade.buy_tag" description="Buy tag">{{ trade.buy_tag }}</ValuePair>
|
||||||
<ValuePair description="Open Rate">{{ formatPrice(trade.open_rate) }}</ValuePair>
|
<ValuePair description="Open Rate">{{ formatPrice(trade.open_rate) }}</ValuePair>
|
||||||
|
@ -24,10 +27,10 @@
|
||||||
v-if="trade.profit_ratio && trade.profit_abs"
|
v-if="trade.profit_ratio && trade.profit_abs"
|
||||||
:description="`${trade.is_open ? 'Current Profit' : 'Close Profit'}`"
|
:description="`${trade.is_open ? 'Current Profit' : 'Close Profit'}`"
|
||||||
>
|
>
|
||||||
{{ formatPercent(trade.profit_ratio) }} | {{ trade.profit_abs }}
|
<trade-profit class="ml-2" :trade="trade" />
|
||||||
</ValuePair>
|
</ValuePair>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-7">
|
<div class="mt-2 mt-lg-0 col-lg-7">
|
||||||
<h5 class="detail-header">Stoploss</h5>
|
<h5 class="detail-header">Stoploss</h5>
|
||||||
<ValuePair description="Stoploss">
|
<ValuePair description="Stoploss">
|
||||||
{{ formatPercent(trade.stop_loss_pct / 100) }} |
|
{{ formatPercent(trade.stop_loss_pct / 100) }} |
|
||||||
|
@ -57,21 +60,26 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||||
import { formatPercent, formatPrice, timestampms } from '@/shared/formatters';
|
import { formatPercent, formatPriceCurrency, formatPrice, timestampms } from '@/shared/formatters';
|
||||||
import ValuePair from '@/components/general/ValuePair.vue';
|
import ValuePair from '@/components/general/ValuePair.vue';
|
||||||
|
import TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||||
import { Trade } from '@/types';
|
import { Trade } from '@/types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { ValuePair },
|
components: { ValuePair, TradeProfit },
|
||||||
})
|
})
|
||||||
export default class TradeDetail extends Vue {
|
export default class TradeDetail extends Vue {
|
||||||
@Prop({ type: Object, required: true }) trade!: Trade;
|
@Prop({ type: Object, required: true }) trade!: Trade;
|
||||||
|
|
||||||
|
@Prop({ type: String, required: true }) stakeCurrency!: string;
|
||||||
|
|
||||||
timestampms = timestampms;
|
timestampms = timestampms;
|
||||||
|
|
||||||
formatPercent = formatPercent;
|
formatPercent = formatPercent;
|
||||||
|
|
||||||
formatPrice = formatPrice;
|
formatPrice = formatPrice;
|
||||||
|
|
||||||
|
formatPriceCurrency = formatPriceCurrency;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -24,45 +24,12 @@
|
||||||
<ActionIcon :size="16" title="Actions" />
|
<ActionIcon :size="16" title="Actions" />
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-popover :target="`btn-actions_${row.index}`" triggers="focus" placement="left">
|
<b-popover :target="`btn-actions_${row.index}`" triggers="focus" placement="left">
|
||||||
<div class="d-flex flex-column">
|
<trade-actions
|
||||||
<b-button
|
:trade="row.item"
|
||||||
v-if="botApiVersion <= 1.1"
|
:bot-api-version="botApiVersion"
|
||||||
class="btn-xs text-left"
|
@deleteTrade="removeTradeHandler"
|
||||||
size="sm"
|
@forceSell="forcesellHandler"
|
||||||
title="Forcesell"
|
/>
|
||||||
@click="forcesellHandler(row.item)"
|
|
||||||
>
|
|
||||||
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell
|
|
||||||
</b-button>
|
|
||||||
<b-button
|
|
||||||
v-if="botApiVersion > 1.1"
|
|
||||||
class="btn-xs text-left"
|
|
||||||
size="sm"
|
|
||||||
title="Forcesell limit"
|
|
||||||
@click="forcesellHandler(row.item, 'limit')"
|
|
||||||
>
|
|
||||||
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell limit
|
|
||||||
</b-button>
|
|
||||||
<b-button
|
|
||||||
v-if="botApiVersion > 1.1"
|
|
||||||
class="btn-xs text-left mt-1"
|
|
||||||
size="sm"
|
|
||||||
title="Forcesell market"
|
|
||||||
@click="forcesellHandler(row.item, 'market')"
|
|
||||||
>
|
|
||||||
<ForceSellIcon :size="16" title="Forcesell" class="mr-1" />Forcesell market
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button
|
|
||||||
class="btn-xs text-left mt-1"
|
|
||||||
size="sm"
|
|
||||||
title="Delete trade"
|
|
||||||
@click="removeTradeHandler(row.item)"
|
|
||||||
>
|
|
||||||
<DeleteIcon :size="16" title="Delete trade" class="mr-1" />
|
|
||||||
Delete
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</b-popover>
|
</b-popover>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(pair)="row">
|
<template #cell(pair)="row">
|
||||||
|
@ -118,11 +85,12 @@ import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||||
import StoreModules from '@/store/storeSubModules';
|
import StoreModules from '@/store/storeSubModules';
|
||||||
import TradeProfit from './TradeProfit.vue';
|
import TradeProfit from './TradeProfit.vue';
|
||||||
|
import TradeActions from './TradeActions.vue';
|
||||||
|
|
||||||
const ftbot = namespace(StoreModules.ftbot);
|
const ftbot = namespace(StoreModules.ftbot);
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { DeleteIcon, ForceSellIcon, ActionIcon, DateTimeTZ, TradeProfit },
|
components: { DeleteIcon, ForceSellIcon, ActionIcon, DateTimeTZ, TradeProfit, TradeActions },
|
||||||
})
|
})
|
||||||
export default class TradeList extends Vue {
|
export default class TradeList extends Vue {
|
||||||
$refs!: {
|
$refs!: {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { timestampms, timestampmsWithTimezone } from '@/shared/formatters';
|
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
@Component({})
|
@Component({})
|
||||||
|
@ -12,9 +12,14 @@ export default class DateTimeTZ extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, type: Boolean, default: false }) showTimezone!: boolean;
|
@Prop({ required: false, type: Boolean, default: false }) showTimezone!: boolean;
|
||||||
|
|
||||||
|
@Prop({ required: false, type: Boolean, default: false }) dateOnly!: boolean;
|
||||||
|
|
||||||
timestampms = timestampms;
|
timestampms = timestampms;
|
||||||
|
|
||||||
get formattedDate(): string {
|
get formattedDate(): string {
|
||||||
|
if (this.dateOnly) {
|
||||||
|
return timestampToDateString(this.date);
|
||||||
|
}
|
||||||
if (this.showTimezone) {
|
if (this.showTimezone) {
|
||||||
return timestampmsWithTimezone(this.date);
|
return timestampmsWithTimezone(this.date);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-100 d-inline-block">
|
<div class="d-inline-block">
|
||||||
<div :class="isProfitable ? 'triangle-up' : 'triangle-down'"></div>
|
<div :class="isProfitable ? 'triangle-up' : 'triangle-down'"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
classLabel: {
|
classLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'col-4 font-weight-bold',
|
default: 'col-4 font-weight-bold mb-0',
|
||||||
},
|
},
|
||||||
classValue: {
|
classValue: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<main class="container-fluid container-main">
|
<main>
|
||||||
<BotAlerts />
|
<BotAlerts />
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
||||||
>Dashboard</router-link
|
>Dashboard</router-link
|
||||||
>
|
>
|
||||||
<router-link class="nav-link navbar-nav" to="/graph">Graph</router-link>
|
<router-link class="nav-link navbar-nav" to="/graph">Chart</router-link>
|
||||||
<router-link class="nav-link navbar-nav" to="/logs">Logs</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"
|
<router-link v-if="canRunBacktest" class="nav-link navbar-nav" to="/backtest"
|
||||||
>Backtest</router-link
|
>Backtest</router-link
|
||||||
|
@ -75,7 +75,6 @@
|
||||||
<div class="d-block d-sm-none">
|
<div class="d-block d-sm-none">
|
||||||
<!-- Visible only on XS -->
|
<!-- Visible only on XS -->
|
||||||
<li class="nav-item text-secondary ml-2 d-sm-none d-flex justify-content-between">
|
<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">
|
<div class="d-flex">
|
||||||
<b-nav-text class="verticalCenter small mr-2">
|
<b-nav-text class="verticalCenter small mr-2">
|
||||||
{{ botName || 'No bot selected' }}
|
{{ botName || 'No bot selected' }}
|
||||||
|
@ -86,13 +85,6 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<router-link class="nav-link navbar-nav" to="/settings">Settings</router-link>
|
<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
|
<router-link
|
||||||
v-if="botCount === 1"
|
v-if="botCount === 1"
|
||||||
class="nav-link navbar-nav"
|
class="nav-link navbar-nav"
|
||||||
|
|
57
src/components/layout/NavFooter.vue
Normal file
57
src/components/layout/NavFooter.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<footer class="d-md-none">
|
||||||
|
<!-- Only visible on xs (phone) viewport! -->
|
||||||
|
<hr class="my-0" />
|
||||||
|
<div class="d-flex flex-align-center justify-content-center">
|
||||||
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/open_trades">
|
||||||
|
<OpenTradesIcon />
|
||||||
|
Trades
|
||||||
|
</router-link>
|
||||||
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/trade_history">
|
||||||
|
<ClosedTradesIcon />
|
||||||
|
History
|
||||||
|
</router-link>
|
||||||
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/pairlist">
|
||||||
|
<PairListIcon />
|
||||||
|
Pairlist
|
||||||
|
</router-link>
|
||||||
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/balance">
|
||||||
|
<BalanceIcon />
|
||||||
|
Balance
|
||||||
|
</router-link>
|
||||||
|
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard">
|
||||||
|
<DashboardIcon />
|
||||||
|
Dashboard
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import { namespace } from 'vuex-class';
|
||||||
|
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||||
|
import OpenTradesIcon from 'vue-material-design-icons/FolderOpen.vue';
|
||||||
|
import ClosedTradesIcon from 'vue-material-design-icons/FolderLock.vue';
|
||||||
|
import BalanceIcon from 'vue-material-design-icons/Bank.vue';
|
||||||
|
import PairListIcon from 'vue-material-design-icons/ViewList.vue';
|
||||||
|
import DashboardIcon from 'vue-material-design-icons/ViewDashboardOutline.vue';
|
||||||
|
|
||||||
|
const ftbot = namespace('ftbot');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { OpenTradesIcon, ClosedTradesIcon, BalanceIcon, PairListIcon, DashboardIcon },
|
||||||
|
})
|
||||||
|
export default class NavFooter extends Vue {
|
||||||
|
@ftbot.Getter [BotStoreGetters.canRunBacktest]!: boolean;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
[data-theme='dark'] {
|
||||||
|
.router-link-active,
|
||||||
|
.nav-link:active {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,6 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import './plugins/bootstrap-vue';
|
import './plugins/bootstrap-vue';
|
||||||
|
import './plugins/composition_api';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
4
src/plugins/composition_api.ts
Normal file
4
src/plugins/composition_api.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueCompositionAPI from '@vue/composition-api';
|
||||||
|
|
||||||
|
Vue.use(VueCompositionAPI);
|
|
@ -42,6 +42,26 @@ const routes: Array<RouteConfig> = [
|
||||||
name: 'Freqtrade Dashboard',
|
name: 'Freqtrade Dashboard',
|
||||||
component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'),
|
component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/balance',
|
||||||
|
name: 'Freqtrade Balance',
|
||||||
|
component: () => import(/* webpackChunkName: "balance" */ '@/components/ftbot/Balance.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/open_trades',
|
||||||
|
component: () => import(/* webpackChunkName: "trades" */ '@/views/TradesList.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/trade_history',
|
||||||
|
component: () => import(/* webpackChunkName: "trades" */ '@/views/TradesList.vue'),
|
||||||
|
props: { history: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pairlist',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "pairlist" */ '@/components/ftbot/FTBotAPIPairList.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Freqtrade Settings',
|
name: 'Freqtrade Settings',
|
||||||
|
|
|
@ -20,6 +20,17 @@ export function formatPrice(value: number, decimals = 8): string {
|
||||||
return !isUndefined(value) ? parseFloat(value.toFixed(decimals)).toString() : '';
|
return !isUndefined(value) ? parseFloat(value.toFixed(decimals)).toString() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats price in the format "<price> <StakeCurrency>" using "deciaml" decimals
|
||||||
|
* @param price Price to format
|
||||||
|
* @param currency currency to use
|
||||||
|
* @param decimals Decimals
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function formatPriceCurrency(price, currency: string, decimals = 3) {
|
||||||
|
return `${formatPrice(price, decimals)} ${currency}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function dateFromString(datestring: string, format: string): Date {
|
export function dateFromString(datestring: string, format: string): Date {
|
||||||
return parse(datestring, format, 0);
|
return parse(datestring, format, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -316,7 +316,11 @@ export default function createBotStore(store) {
|
||||||
async pingAll({ getters, dispatch }) {
|
async pingAll({ getters, dispatch }) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
getters.allAvailableBotsList.map(async (e) => {
|
getters.allAvailableBotsList.map(async (e) => {
|
||||||
|
try {
|
||||||
await dispatch(`${e}/ping`);
|
await dispatch(`${e}/ping`);
|
||||||
|
} catch {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -76,6 +76,7 @@ export enum BotStoreGetters {
|
||||||
selectedBacktestResult = 'selectedBacktestResult',
|
selectedBacktestResult = 'selectedBacktestResult',
|
||||||
canRunBacktest = 'canRunBacktest',
|
canRunBacktest = 'canRunBacktest',
|
||||||
stakeCurrencyDecimals = 'stakeCurrencyDecimals',
|
stakeCurrencyDecimals = 'stakeCurrencyDecimals',
|
||||||
|
stakeCurrency = 'stakeCurrency',
|
||||||
strategyPlotConfig = 'strategyPlotConfig',
|
strategyPlotConfig = 'strategyPlotConfig',
|
||||||
version = 'version',
|
version = 'version',
|
||||||
botApiVersion = 'botApiVersion',
|
botApiVersion = 'botApiVersion',
|
||||||
|
@ -251,6 +252,9 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
[BotStoreGetters.stakeCurrencyDecimals](state: FtbotStateType): number {
|
[BotStoreGetters.stakeCurrencyDecimals](state: FtbotStateType): number {
|
||||||
return state.botState?.stake_currency_decimals || 3;
|
return state.botState?.stake_currency_decimals || 3;
|
||||||
},
|
},
|
||||||
|
[BotStoreGetters.stakeCurrency](state: FtbotStateType): string {
|
||||||
|
return state.botState?.stake_currency || '';
|
||||||
|
},
|
||||||
[BotStoreGetters.strategyPlotConfig](state: FtbotStateType): PlotConfig | undefined {
|
[BotStoreGetters.strategyPlotConfig](state: FtbotStateType): PlotConfig | undefined {
|
||||||
return state.strategyPlotConfig;
|
return state.strategyPlotConfig;
|
||||||
},
|
},
|
||||||
|
@ -458,6 +462,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
|
commit('setIsBotOnline', false);
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,6 +19,7 @@ export enum DashboardLayout {
|
||||||
export enum LayoutGetters {
|
export enum LayoutGetters {
|
||||||
getDashboardLayoutSm = 'getDashboardLayoutSm',
|
getDashboardLayoutSm = 'getDashboardLayoutSm',
|
||||||
getDashboardLayout = 'getDashboardLayout',
|
getDashboardLayout = 'getDashboardLayout',
|
||||||
|
getTradingLayoutSm = 'getTradingLayoutSm',
|
||||||
getTradingLayout = 'getTradingLayout',
|
getTradingLayout = 'getTradingLayout',
|
||||||
getLayoutLocked = 'getLayoutLocked',
|
getLayoutLocked = 'getLayoutLocked',
|
||||||
}
|
}
|
||||||
|
@ -45,6 +46,15 @@ const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
||||||
{ i: TradeLayout.tradeHistory, x: 3, y: 25, w: 9, h: 10 },
|
{ i: TradeLayout.tradeHistory, x: 3, y: 25, w: 9, h: 10 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Currently only multiPane is visible
|
||||||
|
const DEFAULT_TRADING_LAYOUT_SM: GridItemData[] = [
|
||||||
|
{ i: TradeLayout.multiPane, x: 0, y: 0, w: 12, h: 10 },
|
||||||
|
{ i: TradeLayout.chartView, x: 0, y: 10, w: 12, h: 0 },
|
||||||
|
{ i: TradeLayout.tradeDetail, x: 0, y: 19, w: 12, h: 0 },
|
||||||
|
{ i: TradeLayout.openTrades, x: 0, y: 8, w: 12, h: 0 },
|
||||||
|
{ i: TradeLayout.tradeHistory, x: 0, y: 25, w: 12, h: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
const DEFAULT_DASHBOARD_LAYOUT: GridItemData[] = [
|
||||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 8, h: 6 } /* Bot Comparison */,
|
{ 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.dailyChart, x: 8, y: 0, w: 4, h: 6 },
|
||||||
|
@ -110,6 +120,9 @@ export default {
|
||||||
[LayoutGetters.getDashboardLayout](state) {
|
[LayoutGetters.getDashboardLayout](state) {
|
||||||
return state.dashboardLayout;
|
return state.dashboardLayout;
|
||||||
},
|
},
|
||||||
|
[LayoutGetters.getTradingLayoutSm]() {
|
||||||
|
return [...DEFAULT_TRADING_LAYOUT_SM];
|
||||||
|
},
|
||||||
[LayoutGetters.getTradingLayout](state) {
|
[LayoutGetters.getTradingLayout](state) {
|
||||||
return state.tradingLayout;
|
return state.tradingLayout;
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
:is-draggable="!isLayoutLocked"
|
:is-draggable="!isLayoutLocked"
|
||||||
:responsive="true"
|
:responsive="true"
|
||||||
:prevent-collision="true"
|
:prevent-collision="true"
|
||||||
|
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||||
@layout-updated="layoutUpdated"
|
@layout-updated="layoutUpdated"
|
||||||
@breakpoint-changed="breakpointChanged"
|
@breakpoint-changed="breakpointChanged"
|
||||||
>
|
>
|
||||||
|
@ -171,7 +172,7 @@ export default class Dashboard extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get isResizableLayout() {
|
get isResizableLayout() {
|
||||||
return ['', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
return ['', 'sm', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
get gridLayout() {
|
get gridLayout() {
|
||||||
|
|
73
src/views/TradesList.vue
Normal file
73
src/views/TradesList.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- <TradeList
|
||||||
|
class="open-trades"
|
||||||
|
:trades="openTrades"
|
||||||
|
title="Open trades"
|
||||||
|
:active-trades="true"
|
||||||
|
empty-text="Currently no open trades."
|
||||||
|
/> -->
|
||||||
|
<CustomTradeList
|
||||||
|
v-if="!history && !detailTradeId"
|
||||||
|
:trades="openTrades"
|
||||||
|
title="Open trades"
|
||||||
|
:active-trades="true"
|
||||||
|
:stake-currency-decimals="stakeCurrencyDecimals"
|
||||||
|
empty-text="No open Trades."
|
||||||
|
/>
|
||||||
|
<CustomTradeList
|
||||||
|
v-if="history && !detailTradeId"
|
||||||
|
:trades="closedTrades"
|
||||||
|
title="Trade history"
|
||||||
|
:stake-currency-decimals="stakeCurrencyDecimals"
|
||||||
|
empty-text="No closed trades so far."
|
||||||
|
/>
|
||||||
|
<div v-if="detailTradeId" class="d-flex flex-column">
|
||||||
|
<b-button size="sm" class="align-self-start mt-1 ml-1" @click="setDetailTrade(null)"
|
||||||
|
><BackIcon /> Back</b-button
|
||||||
|
>
|
||||||
|
<TradeDetail :trade="tradeDetail" :stake-currency="stakeCurrency" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import { namespace } from 'vuex-class';
|
||||||
|
import CustomTradeList from '@/components/ftbot/CustomTradeList.vue';
|
||||||
|
import TradeDetail from '@/components/ftbot/TradeDetail.vue';
|
||||||
|
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
|
||||||
|
|
||||||
|
import { Trade } from '@/types';
|
||||||
|
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||||
|
import StoreModules from '@/store/storeSubModules';
|
||||||
|
|
||||||
|
const ftbot = namespace(StoreModules.ftbot);
|
||||||
|
// TODO: TradeDetail could be extracted into a sub-route to allow direct access
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
CustomTradeList,
|
||||||
|
TradeDetail,
|
||||||
|
BackIcon,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class TradesList extends Vue {
|
||||||
|
@Prop({ default: false }) history!: boolean;
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.openTrades]!: Trade[];
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.closedTrades]!: Trade[];
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.stakeCurrencyDecimals]!: number;
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.detailTradeId]?: number;
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.tradeDetail]!: Trade;
|
||||||
|
|
||||||
|
@ftbot.Action setDetailTrade;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -5,11 +5,16 @@
|
||||||
:layout="gridLayout"
|
:layout="gridLayout"
|
||||||
:vertical-compact="false"
|
:vertical-compact="false"
|
||||||
:margin="[5, 5]"
|
:margin="[5, 5]"
|
||||||
:is-resizable="!getLayoutLocked"
|
:responsive-layouts="responsiveGridLayouts"
|
||||||
:is-draggable="!getLayoutLocked"
|
:is-resizable="!isLayoutLocked"
|
||||||
|
:is-draggable="!isLayoutLocked"
|
||||||
|
:responsive="true"
|
||||||
|
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||||
@layout-updated="layoutUpdatedEvent"
|
@layout-updated="layoutUpdatedEvent"
|
||||||
|
@breakpoint-changed="breakpointChanged"
|
||||||
>
|
>
|
||||||
<GridItem
|
<GridItem
|
||||||
|
v-if="gridLayoutMultiPane.h != 0"
|
||||||
:i="gridLayoutMultiPane.i"
|
:i="gridLayoutMultiPane.i"
|
||||||
:x="gridLayoutMultiPane.x"
|
:x="gridLayoutMultiPane.x"
|
||||||
:y="gridLayoutMultiPane.y"
|
:y="gridLayoutMultiPane.y"
|
||||||
|
@ -48,6 +53,7 @@
|
||||||
</DraggableContainer>
|
</DraggableContainer>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem
|
<GridItem
|
||||||
|
v-if="gridLayoutOpenTrades.h != 0"
|
||||||
:i="gridLayoutOpenTrades.i"
|
:i="gridLayoutOpenTrades.i"
|
||||||
:x="gridLayoutOpenTrades.x"
|
:x="gridLayoutOpenTrades.x"
|
||||||
:y="gridLayoutOpenTrades.y"
|
:y="gridLayoutOpenTrades.y"
|
||||||
|
@ -66,6 +72,7 @@
|
||||||
</DraggableContainer>
|
</DraggableContainer>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem
|
<GridItem
|
||||||
|
v-if="gridLayoutTradeHistory.h != 0"
|
||||||
:i="gridLayoutTradeHistory.i"
|
:i="gridLayoutTradeHistory.i"
|
||||||
:x="gridLayoutTradeHistory.x"
|
:x="gridLayoutTradeHistory.x"
|
||||||
:y="gridLayoutTradeHistory.y"
|
:y="gridLayoutTradeHistory.y"
|
||||||
|
@ -78,12 +85,13 @@
|
||||||
class="trade-history"
|
class="trade-history"
|
||||||
:trades="closedTrades"
|
:trades="closedTrades"
|
||||||
title="Trade history"
|
title="Trade history"
|
||||||
|
:show-filter="true"
|
||||||
empty-text="No closed trades so far."
|
empty-text="No closed trades so far."
|
||||||
/>
|
/>
|
||||||
</DraggableContainer>
|
</DraggableContainer>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem
|
<GridItem
|
||||||
v-if="detailTradeId"
|
v-if="detailTradeId && gridLayoutTradeDetail.h != 0"
|
||||||
:i="gridLayoutTradeDetail.i"
|
:i="gridLayoutTradeDetail.i"
|
||||||
:x="gridLayoutTradeDetail.x"
|
:x="gridLayoutTradeDetail.x"
|
||||||
:y="gridLayoutTradeDetail.y"
|
:y="gridLayoutTradeDetail.y"
|
||||||
|
@ -93,10 +101,11 @@
|
||||||
drag-allow-from=".card-header"
|
drag-allow-from=".card-header"
|
||||||
>
|
>
|
||||||
<DraggableContainer header="Trade Detail">
|
<DraggableContainer header="Trade Detail">
|
||||||
<TradeDetail :trade="tradeDetail"> </TradeDetail>
|
<TradeDetail :trade="tradeDetail" :stake-currency="stakeCurrency" />
|
||||||
</DraggableContainer>
|
</DraggableContainer>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem
|
<GridItem
|
||||||
|
v-if="gridLayoutTradeDetail.h != 0"
|
||||||
:i="gridLayoutChartView.i"
|
:i="gridLayoutChartView.i"
|
||||||
:x="gridLayoutChartView.x"
|
:x="gridLayoutChartView.x"
|
||||||
:y="gridLayoutChartView.y"
|
:y="gridLayoutChartView.y"
|
||||||
|
@ -179,15 +188,38 @@ export default class Trading extends Vue {
|
||||||
|
|
||||||
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
||||||
|
|
||||||
|
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||||
|
|
||||||
@layoutNs.Getter [LayoutGetters.getTradingLayout]!: GridItemData[];
|
@layoutNs.Getter [LayoutGetters.getTradingLayout]!: GridItemData[];
|
||||||
|
|
||||||
|
@layoutNs.Getter [LayoutGetters.getTradingLayoutSm]!: GridItemData[];
|
||||||
|
|
||||||
@layoutNs.Action [LayoutActions.setTradingLayout];
|
@layoutNs.Action [LayoutActions.setTradingLayout];
|
||||||
|
|
||||||
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
@layoutNs.Getter [LayoutGetters.getLayoutLocked]: boolean;
|
||||||
|
|
||||||
|
currentBreakpoint = '';
|
||||||
|
|
||||||
|
localGridLayout: GridItemData[] = [];
|
||||||
|
|
||||||
|
get isLayoutLocked() {
|
||||||
|
return this.getLayoutLocked || !this.isResizableLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isResizableLayout() {
|
||||||
|
return ['', 'sm', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||||
|
}
|
||||||
|
|
||||||
get gridLayout(): GridItemData[] {
|
get gridLayout(): GridItemData[] {
|
||||||
|
if (this.isResizableLayout) {
|
||||||
return this.getTradingLayout;
|
return this.getTradingLayout;
|
||||||
}
|
}
|
||||||
|
return this.localGridLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
set gridLayout(newLayout) {
|
||||||
|
// Dummy setter to make gridLayout happy. Updates happen through layoutUpdated.
|
||||||
|
}
|
||||||
|
|
||||||
get gridLayoutMultiPane(): GridItemData {
|
get gridLayoutMultiPane(): GridItemData {
|
||||||
return findGridLayout(this.gridLayout, TradeLayout.multiPane);
|
return findGridLayout(this.gridLayout, TradeLayout.multiPane);
|
||||||
|
@ -209,9 +241,26 @@ export default class Trading extends Vue {
|
||||||
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.localGridLayout = [...this.getTradingLayoutSm];
|
||||||
|
}
|
||||||
|
|
||||||
layoutUpdatedEvent(newLayout) {
|
layoutUpdatedEvent(newLayout) {
|
||||||
|
if (this.isResizableLayout) {
|
||||||
this.setTradingLayout(newLayout);
|
this.setTradingLayout(newLayout);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get responsiveGridLayouts() {
|
||||||
|
return {
|
||||||
|
sm: this[LayoutGetters.getTradingLayoutSm],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpointChanged(newBreakpoint) {
|
||||||
|
console.log('breakpoint:', newBreakpoint);
|
||||||
|
this.currentBreakpoint = newBreakpoint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { formatPercent } from '@/shared/formatters';
|
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||||
|
|
||||||
describe('formatters.ts', () => {
|
describe('formatters.ts', () => {
|
||||||
it('Format percent correctly', () => {
|
it('Format percent correctly', () => {
|
||||||
expect(formatPercent(0.5)).toEqual('50.000%');
|
expect(formatPercent(0.5)).toEqual('50.000%');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('format price currency as expected', () => {
|
||||||
|
expect(formatPriceCurrency(5123.551123, 'USDT', 3)).toEqual('5123.551 USDT');
|
||||||
|
expect(formatPriceCurrency(5123.551123, 'USDT')).toEqual('5123.551 USDT');
|
||||||
|
expect(formatPriceCurrency(5123.551123, 'USDT', 5)).toEqual('5123.55112 USDT');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -11370,10 +11370,10 @@ vue-class-component@^7.2.5:
|
||||||
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4"
|
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4"
|
||||||
integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==
|
integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==
|
||||||
|
|
||||||
vue-demi@^0.11.2:
|
vue-demi@0.12.1, vue-demi@^0.11.2:
|
||||||
version "0.11.2"
|
version "0.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.11.2.tgz#faa06da53887c493a695b997f4fcb4784a667990"
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.1.tgz#f7e18efbecffd11ab069d1472d7a06e319b4174c"
|
||||||
integrity sha512-J+X8Au6BhQdcej6LY4O986634hZLu55L0ewU2j8my7WIKlu8cK0dqmdUxqVHHMd/cMrKKZ9SywB/id6aLhwCtA==
|
integrity sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==
|
||||||
|
|
||||||
vue-echarts@^6.0.0:
|
vue-echarts@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
|
@ -11506,6 +11506,11 @@ vuex-class@^0.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/vuex-class/-/vuex-class-0.3.2.tgz#c7e96a076c1682137d4d23a8dcfdc63f220e17a8"
|
resolved "https://registry.yarnpkg.com/vuex-class/-/vuex-class-0.3.2.tgz#c7e96a076c1682137d4d23a8dcfdc63f220e17a8"
|
||||||
integrity sha512-m0w7/FMsNcwJgunJeM+wcNaHzK2KX1K1rw2WUQf7Q16ndXHo7pflRyOV/E8795JO/7fstyjH3EgqBI4h4n4qXQ==
|
integrity sha512-m0w7/FMsNcwJgunJeM+wcNaHzK2KX1K1rw2WUQf7Q16ndXHo7pflRyOV/E8795JO/7fstyjH3EgqBI4h4n4qXQ==
|
||||||
|
|
||||||
|
vuex-composition-helpers@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vuex-composition-helpers/-/vuex-composition-helpers-1.1.0.tgz#a18d00192fbb0205630202aade1ec6d5f05d4c28"
|
||||||
|
integrity sha512-36f3MWRCW6QqtP3NLyLbtTPv8qWwbac7gAK9fM4ZtDWTCWuAeBoZEiM+bmPQweAQoMM7GRSXmw/90Egiqg0DCA==
|
||||||
|
|
||||||
vuex@^3.6.2:
|
vuex@^3.6.2:
|
||||||
version "3.6.2"
|
version "3.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user