mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-26 21:15:15 +00:00
commit
07bae3ae6b
|
@ -13,8 +13,12 @@
|
|||
"cy:open-ct": "cypress open-ct",
|
||||
"cy:run-ct": "cypress run-ct"
|
||||
},
|
||||
"resolutions": {
|
||||
"vue-demi": "0.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cypress/vue": "^2.2.3",
|
||||
"@vue/composition-api": "^1.4.3",
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
|
@ -27,6 +31,7 @@
|
|||
"humanize-duration": "^3.27.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.5",
|
||||
"vue-demi": "0.12.1",
|
||||
"vue-echarts": "^6.0.0",
|
||||
"vue-grid-layout": "^2.3.12",
|
||||
"vue-material-design-icons": "^5.0.0",
|
||||
|
@ -34,7 +39,8 @@
|
|||
"vue-router": "^3.5.3",
|
||||
"vue-select": "^3.16.0",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-class": "^0.3.2"
|
||||
"vuex-class": "^0.3.2",
|
||||
"vuex-composition-helpers": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-dev-server": "^1.8.0",
|
||||
|
@ -49,7 +55,6 @@
|
|||
"@vue/cli-plugin-unit-jest": "~4.5.15",
|
||||
"@vue/cli-plugin-vuex": "~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-prettier": "^6.0.0",
|
||||
"@vue/eslint-config-typescript": "^5.1.0",
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
<div id="app" class="d-flex flex-column vh-100">
|
||||
<NavBar />
|
||||
<Body class="flex-fill overflow-auto" />
|
||||
<NavFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import NavBar from '@/components/layout/NavBar.vue';
|
||||
import NavFooter from '@/components/layout/NavFooter.vue';
|
||||
import Body from '@/components/layout/Body.vue';
|
||||
import { namespace } from 'vuex-class';
|
||||
import { SettingsGetters } from './store/modules/settings';
|
||||
|
@ -17,7 +19,7 @@ import StoreModules from './store/storeSubModules';
|
|||
const uiSettingsNs = namespace(StoreModules.uiSettings);
|
||||
|
||||
@Component({
|
||||
components: { NavBar, Body },
|
||||
components: { NavBar, Body, NavFooter },
|
||||
})
|
||||
export default class App extends Vue {
|
||||
@uiSettingsNs.Getter [SettingsGetters.timezone]: string;
|
||||
|
|
|
@ -81,7 +81,7 @@ export default class CumProfitChart extends Vue {
|
|||
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 });
|
||||
if (!this.botList.includes(trade.botId)) {
|
||||
this.botList.push(trade.botId);
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
||||
import { Getter } from 'vuex-class';
|
||||
import { useGetters } from 'vuex-composition-helpers';
|
||||
import { ref, defineComponent, computed, ComputedRef } from '@vue/composition-api';
|
||||
import ECharts from 'vue-echarts';
|
||||
import { EChartsOption } from 'echarts';
|
||||
// import { EChartsOption } from 'echarts';
|
||||
|
||||
import { use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
@ -38,46 +38,46 @@ use([
|
|||
const CHART_ABS_PROFIT = 'Absolute profit';
|
||||
const CHART_TRADE_COUNT = 'Trade Count';
|
||||
|
||||
@Component({
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'v-chart': ECharts,
|
||||
},
|
||||
})
|
||||
export default class DailyChart extends Vue {
|
||||
@Prop({ required: true }) dailyStats!: DailyReturnValue;
|
||||
props: {
|
||||
dailyStats: {
|
||||
type: Object as () => DailyReturnValue,
|
||||
required: true,
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
@Prop({ default: true, type: Boolean }) showTitle!: boolean;
|
||||
|
||||
@Getter getChartTheme!: string;
|
||||
|
||||
get absoluteMin() {
|
||||
return Number(
|
||||
this.dailyStats.data.reduce(
|
||||
setup(props) {
|
||||
const { getChartTheme } = useGetters(['getChartTheme']);
|
||||
const absoluteMin: ComputedRef<number> = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(min, p) => (p.abs_profit < min ? p.abs_profit : min),
|
||||
this.dailyStats.data[0]?.abs_profit,
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
get absoluteMax() {
|
||||
return Number(
|
||||
this.dailyStats.data.reduce(
|
||||
const absoluteMax: ComputedRef<number> = computed(() =>
|
||||
props.dailyStats.data.reduce(
|
||||
(max, p) => (p.abs_profit > max ? p.abs_profit : max),
|
||||
this.dailyStats.data[0]?.abs_profit,
|
||||
props.dailyStats.data[0]?.abs_profit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
get dailyChartOptions(): EChartsOption {
|
||||
return {
|
||||
// : Ref<EChartsOption>
|
||||
const dailyChartOptions = ref({
|
||||
title: {
|
||||
text: 'Daily profit',
|
||||
show: this.showTitle,
|
||||
show: props.showTitle,
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
dataset: {
|
||||
dimensions: ['date', 'abs_profit', 'trade_count'],
|
||||
source: this.dailyStats.data,
|
||||
source: props.dailyStats.data,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
|
@ -92,10 +92,12 @@ export default class DailyChart extends Vue {
|
|||
data: [CHART_ABS_PROFIT, CHART_TRADE_COUNT],
|
||||
right: '5%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
inverse: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
inverse: true,
|
||||
},
|
||||
],
|
||||
visualMap: [
|
||||
{
|
||||
dimension: 1,
|
||||
|
@ -104,12 +106,12 @@ export default class DailyChart extends Vue {
|
|||
pieces: [
|
||||
{
|
||||
max: 0.0,
|
||||
min: this.absoluteMin,
|
||||
min: absoluteMin.value,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
min: 0.0,
|
||||
max: this.absoluteMax,
|
||||
max: absoluteMax.value,
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
|
@ -149,9 +151,14 @@ export default class DailyChart extends Vue {
|
|||
yAxisIndex: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
dailyChartOptions,
|
||||
getChartTheme,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
@ -63,6 +67,8 @@ export default class PairSummary extends Vue {
|
|||
|
||||
@ftbot.Getter [BotStoreGetters.selectedPair]!: string;
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
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>
|
||||
<ValuePair description="TradeId">{{ trade.trade_id }}</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 v-if="trade.buy_tag" description="Buy tag">{{ trade.buy_tag }}</ValuePair>
|
||||
<ValuePair description="Open Rate">{{ formatPrice(trade.open_rate) }}</ValuePair>
|
||||
|
@ -24,10 +27,10 @@
|
|||
v-if="trade.profit_ratio && trade.profit_abs"
|
||||
:description="`${trade.is_open ? 'Current Profit' : 'Close Profit'}`"
|
||||
>
|
||||
{{ formatPercent(trade.profit_ratio) }} | {{ trade.profit_abs }}
|
||||
<trade-profit class="ml-2" :trade="trade" />
|
||||
</ValuePair>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="mt-2 mt-lg-0 col-lg-7">
|
||||
<h5 class="detail-header">Stoploss</h5>
|
||||
<ValuePair description="Stoploss">
|
||||
{{ formatPercent(trade.stop_loss_pct / 100) }} |
|
||||
|
@ -57,21 +60,26 @@
|
|||
|
||||
<script lang="ts">
|
||||
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 TradeProfit from '@/components/ftbot/TradeProfit.vue';
|
||||
import { Trade } from '@/types';
|
||||
|
||||
@Component({
|
||||
components: { ValuePair },
|
||||
components: { ValuePair, TradeProfit },
|
||||
})
|
||||
export default class TradeDetail extends Vue {
|
||||
@Prop({ type: Object, required: true }) trade!: Trade;
|
||||
|
||||
@Prop({ type: String, required: true }) stakeCurrency!: string;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
formatPercent = formatPercent;
|
||||
|
||||
formatPrice = formatPrice;
|
||||
|
||||
formatPriceCurrency = formatPriceCurrency;
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
|
|
@ -24,45 +24,12 @@
|
|||
<ActionIcon :size="16" title="Actions" />
|
||||
</b-button>
|
||||
<b-popover :target="`btn-actions_${row.index}`" triggers="focus" placement="left">
|
||||
<div class="d-flex flex-column">
|
||||
<b-button
|
||||
v-if="botApiVersion <= 1.1"
|
||||
class="btn-xs text-left"
|
||||
size="sm"
|
||||
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>
|
||||
<trade-actions
|
||||
:trade="row.item"
|
||||
:bot-api-version="botApiVersion"
|
||||
@deleteTrade="removeTradeHandler"
|
||||
@forceSell="forcesellHandler"
|
||||
/>
|
||||
</b-popover>
|
||||
</template>
|
||||
<template #cell(pair)="row">
|
||||
|
@ -118,11 +85,12 @@ import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
|||
import { BotStoreGetters } from '@/store/modules/ftbot';
|
||||
import StoreModules from '@/store/storeSubModules';
|
||||
import TradeProfit from './TradeProfit.vue';
|
||||
import TradeActions from './TradeActions.vue';
|
||||
|
||||
const ftbot = namespace(StoreModules.ftbot);
|
||||
|
||||
@Component({
|
||||
components: { DeleteIcon, ForceSellIcon, ActionIcon, DateTimeTZ, TradeProfit },
|
||||
components: { DeleteIcon, ForceSellIcon, ActionIcon, DateTimeTZ, TradeProfit, TradeActions },
|
||||
})
|
||||
export default class TradeList extends Vue {
|
||||
$refs!: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { timestampms, timestampmsWithTimezone } from '@/shared/formatters';
|
||||
import { timestampms, timestampmsWithTimezone, timestampToDateString } from '@/shared/formatters';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@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 }) dateOnly!: boolean;
|
||||
|
||||
timestampms = timestampms;
|
||||
|
||||
get formattedDate(): string {
|
||||
if (this.dateOnly) {
|
||||
return timestampToDateString(this.date);
|
||||
}
|
||||
if (this.showTimezone) {
|
||||
return timestampmsWithTimezone(this.date);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="h-100 d-inline-block">
|
||||
<div class="d-inline-block">
|
||||
<div :class="isProfitable ? 'triangle-up' : 'triangle-down'"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -19,7 +19,7 @@ export default Vue.extend({
|
|||
},
|
||||
classLabel: {
|
||||
type: String,
|
||||
default: 'col-4 font-weight-bold',
|
||||
default: 'col-4 font-weight-bold mb-0',
|
||||
},
|
||||
classValue: {
|
||||
type: String,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<main class="container-fluid container-main">
|
||||
<main>
|
||||
<BotAlerts />
|
||||
<router-view />
|
||||
</main>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<router-link v-if="!canRunBacktest" class="nav-link navbar-nav" to="/dashboard"
|
||||
>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 v-if="canRunBacktest" class="nav-link navbar-nav" to="/backtest"
|
||||
>Backtest</router-link
|
||||
|
@ -75,7 +75,6 @@
|
|||
<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' }}
|
||||
|
@ -86,13 +85,6 @@
|
|||
</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"
|
||||
|
|
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 './plugins/bootstrap-vue';
|
||||
import './plugins/composition_api';
|
||||
import App from './App.vue';
|
||||
import store from './store';
|
||||
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',
|
||||
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',
|
||||
name: 'Freqtrade Settings',
|
||||
|
|
|
@ -20,6 +20,17 @@ export function formatPrice(value: number, decimals = 8): string {
|
|||
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 {
|
||||
return parse(datestring, format, 0);
|
||||
}
|
||||
|
|
|
@ -316,7 +316,11 @@ export default function createBotStore(store) {
|
|||
async pingAll({ getters, dispatch }) {
|
||||
await Promise.all(
|
||||
getters.allAvailableBotsList.map(async (e) => {
|
||||
await dispatch(`${e}/ping`);
|
||||
try {
|
||||
await dispatch(`${e}/ping`);
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -76,6 +76,7 @@ export enum BotStoreGetters {
|
|||
selectedBacktestResult = 'selectedBacktestResult',
|
||||
canRunBacktest = 'canRunBacktest',
|
||||
stakeCurrencyDecimals = 'stakeCurrencyDecimals',
|
||||
stakeCurrency = 'stakeCurrency',
|
||||
strategyPlotConfig = 'strategyPlotConfig',
|
||||
version = 'version',
|
||||
botApiVersion = 'botApiVersion',
|
||||
|
@ -251,6 +252,9 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
[BotStoreGetters.stakeCurrencyDecimals](state: FtbotStateType): number {
|
||||
return state.botState?.stake_currency_decimals || 3;
|
||||
},
|
||||
[BotStoreGetters.stakeCurrency](state: FtbotStateType): string {
|
||||
return state.botState?.stake_currency || '';
|
||||
},
|
||||
[BotStoreGetters.strategyPlotConfig](state: FtbotStateType): PlotConfig | undefined {
|
||||
return state.strategyPlotConfig;
|
||||
},
|
||||
|
@ -458,6 +462,7 @@ export function createBotSubStore(botId: string, botName: string) {
|
|||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
//
|
||||
commit('setIsBotOnline', false);
|
||||
return Promise.reject();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ export enum DashboardLayout {
|
|||
export enum LayoutGetters {
|
||||
getDashboardLayoutSm = 'getDashboardLayoutSm',
|
||||
getDashboardLayout = 'getDashboardLayout',
|
||||
getTradingLayoutSm = 'getTradingLayoutSm',
|
||||
getTradingLayout = 'getTradingLayout',
|
||||
getLayoutLocked = 'getLayoutLocked',
|
||||
}
|
||||
|
@ -45,6 +46,15 @@ const DEFAULT_TRADING_LAYOUT: GridItemData[] = [
|
|||
{ 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[] = [
|
||||
{ i: DashboardLayout.botComparison, x: 0, y: 0, w: 8, h: 6 } /* Bot Comparison */,
|
||||
{ i: DashboardLayout.dailyChart, x: 8, y: 0, w: 4, h: 6 },
|
||||
|
@ -110,6 +120,9 @@ export default {
|
|||
[LayoutGetters.getDashboardLayout](state) {
|
||||
return state.dashboardLayout;
|
||||
},
|
||||
[LayoutGetters.getTradingLayoutSm]() {
|
||||
return [...DEFAULT_TRADING_LAYOUT_SM];
|
||||
},
|
||||
[LayoutGetters.getTradingLayout](state) {
|
||||
return state.tradingLayout;
|
||||
},
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
:is-draggable="!isLayoutLocked"
|
||||
:responsive="true"
|
||||
:prevent-collision="true"
|
||||
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||
@layout-updated="layoutUpdated"
|
||||
@breakpoint-changed="breakpointChanged"
|
||||
>
|
||||
|
@ -171,7 +172,7 @@ export default class Dashboard extends Vue {
|
|||
}
|
||||
|
||||
get isResizableLayout() {
|
||||
return ['', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||
return ['', 'sm', 'md', 'lg', 'xl'].includes(this.currentBreakpoint);
|
||||
}
|
||||
|
||||
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"
|
||||
:vertical-compact="false"
|
||||
:margin="[5, 5]"
|
||||
:is-resizable="!getLayoutLocked"
|
||||
:is-draggable="!getLayoutLocked"
|
||||
:responsive-layouts="responsiveGridLayouts"
|
||||
:is-resizable="!isLayoutLocked"
|
||||
:is-draggable="!isLayoutLocked"
|
||||
:responsive="true"
|
||||
:cols="{ lg: 12, md: 12, sm: 12, xs: 4, xxs: 2 }"
|
||||
@layout-updated="layoutUpdatedEvent"
|
||||
@breakpoint-changed="breakpointChanged"
|
||||
>
|
||||
<GridItem
|
||||
v-if="gridLayoutMultiPane.h != 0"
|
||||
:i="gridLayoutMultiPane.i"
|
||||
:x="gridLayoutMultiPane.x"
|
||||
:y="gridLayoutMultiPane.y"
|
||||
|
@ -48,6 +53,7 @@
|
|||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
v-if="gridLayoutOpenTrades.h != 0"
|
||||
:i="gridLayoutOpenTrades.i"
|
||||
:x="gridLayoutOpenTrades.x"
|
||||
:y="gridLayoutOpenTrades.y"
|
||||
|
@ -66,6 +72,7 @@
|
|||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
v-if="gridLayoutTradeHistory.h != 0"
|
||||
:i="gridLayoutTradeHistory.i"
|
||||
:x="gridLayoutTradeHistory.x"
|
||||
:y="gridLayoutTradeHistory.y"
|
||||
|
@ -78,12 +85,13 @@
|
|||
class="trade-history"
|
||||
:trades="closedTrades"
|
||||
title="Trade history"
|
||||
:show-filter="true"
|
||||
empty-text="No closed trades so far."
|
||||
/>
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
v-if="detailTradeId"
|
||||
v-if="detailTradeId && gridLayoutTradeDetail.h != 0"
|
||||
:i="gridLayoutTradeDetail.i"
|
||||
:x="gridLayoutTradeDetail.x"
|
||||
:y="gridLayoutTradeDetail.y"
|
||||
|
@ -93,10 +101,11 @@
|
|||
drag-allow-from=".card-header"
|
||||
>
|
||||
<DraggableContainer header="Trade Detail">
|
||||
<TradeDetail :trade="tradeDetail"> </TradeDetail>
|
||||
<TradeDetail :trade="tradeDetail" :stake-currency="stakeCurrency" />
|
||||
</DraggableContainer>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
v-if="gridLayoutTradeDetail.h != 0"
|
||||
:i="gridLayoutChartView.i"
|
||||
:x="gridLayoutChartView.x"
|
||||
:y="gridLayoutChartView.y"
|
||||
|
@ -179,14 +188,37 @@ export default class Trading extends Vue {
|
|||
|
||||
@ftbot.Getter [BotStoreGetters.whitelist]!: string[];
|
||||
|
||||
@ftbot.Getter [BotStoreGetters.stakeCurrency]!: string;
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getTradingLayout]!: GridItemData[];
|
||||
|
||||
@layoutNs.Getter [LayoutGetters.getTradingLayoutSm]!: GridItemData[];
|
||||
|
||||
@layoutNs.Action [LayoutActions.setTradingLayout];
|
||||
|
||||
@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[] {
|
||||
return this.getTradingLayout;
|
||||
if (this.isResizableLayout) {
|
||||
return this.getTradingLayout;
|
||||
}
|
||||
return this.localGridLayout;
|
||||
}
|
||||
|
||||
set gridLayout(newLayout) {
|
||||
// Dummy setter to make gridLayout happy. Updates happen through layoutUpdated.
|
||||
}
|
||||
|
||||
get gridLayoutMultiPane(): GridItemData {
|
||||
|
@ -209,8 +241,25 @@ export default class Trading extends Vue {
|
|||
return findGridLayout(this.gridLayout, TradeLayout.chartView);
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.localGridLayout = [...this.getTradingLayoutSm];
|
||||
}
|
||||
|
||||
layoutUpdatedEvent(newLayout) {
|
||||
this.setTradingLayout(newLayout);
|
||||
if (this.isResizableLayout) {
|
||||
this.setTradingLayout(newLayout);
|
||||
}
|
||||
}
|
||||
|
||||
get responsiveGridLayouts() {
|
||||
return {
|
||||
sm: this[LayoutGetters.getTradingLayoutSm],
|
||||
};
|
||||
}
|
||||
|
||||
breakpointChanged(newBreakpoint) {
|
||||
console.log('breakpoint:', newBreakpoint);
|
||||
this.currentBreakpoint = newBreakpoint;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { formatPercent } from '@/shared/formatters';
|
||||
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||
|
||||
describe('formatters.ts', () => {
|
||||
it('Format percent correctly', () => {
|
||||
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"
|
||||
integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==
|
||||
|
||||
vue-demi@^0.11.2:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.11.2.tgz#faa06da53887c493a695b997f4fcb4784a667990"
|
||||
integrity sha512-J+X8Au6BhQdcej6LY4O986634hZLu55L0ewU2j8my7WIKlu8cK0dqmdUxqVHHMd/cMrKKZ9SywB/id6aLhwCtA==
|
||||
vue-demi@0.12.1, vue-demi@^0.11.2:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.1.tgz#f7e18efbecffd11ab069d1472d7a06e319b4174c"
|
||||
integrity sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==
|
||||
|
||||
vue-echarts@^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"
|
||||
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:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||
|
|
Loading…
Reference in New Issue
Block a user