diff --git a/package.json b/package.json index 906ee545..8e0a93ab 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.vue b/src/App.vue index 1d371ae2..bd2f9241 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,12 +2,14 @@
+
diff --git a/src/components/ftbot/CustomTradeListEntry.vue b/src/components/ftbot/CustomTradeListEntry.vue new file mode 100644 index 00000000..fa7a6218 --- /dev/null +++ b/src/components/ftbot/CustomTradeListEntry.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/components/ftbot/PairSummary.vue b/src/components/ftbot/PairSummary.vue index 51a167fe..d370b206 100644 --- a/src/components/ftbot/PairSummary.vue +++ b/src/components/ftbot/PairSummary.vue @@ -17,7 +17,11 @@ - + @@ -63,6 +67,8 @@ export default class PairSummary extends Vue { @ftbot.Getter [BotStoreGetters.selectedPair]!: string; + @ftbot.Getter [BotStoreGetters.stakeCurrency]!: string; + timestampms = timestampms; formatPercent = formatPercent; diff --git a/src/components/ftbot/TradeActions.vue b/src/components/ftbot/TradeActions.vue new file mode 100644 index 00000000..8cdfb66d --- /dev/null +++ b/src/components/ftbot/TradeActions.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/ftbot/TradeDetail.vue b/src/components/ftbot/TradeDetail.vue index 1abba239..9aefda91 100644 --- a/src/components/ftbot/TradeDetail.vue +++ b/src/components/ftbot/TradeDetail.vue @@ -5,6 +5,9 @@
General
{{ trade.trade_id }} {{ trade.pair }} + {{ + formatPriceCurrency(trade.stake_amount, stakeCurrency) + }} {{ timestampms(trade.open_timestamp) }} {{ trade.buy_tag }} {{ formatPrice(trade.open_rate) }} @@ -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 }} + -
+
Stoploss
{{ formatPercent(trade.stop_loss_pct / 100) }} | @@ -57,21 +60,26 @@ diff --git a/src/main.ts b/src/main.ts index 3186db92..3cc97cf7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'; diff --git a/src/plugins/composition_api.ts b/src/plugins/composition_api.ts new file mode 100644 index 00000000..2a11e75d --- /dev/null +++ b/src/plugins/composition_api.ts @@ -0,0 +1,4 @@ +import Vue from 'vue'; +import VueCompositionAPI from '@vue/composition-api'; + +Vue.use(VueCompositionAPI); diff --git a/src/router/index.ts b/src/router/index.ts index fbc87006..ce4cad45 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -42,6 +42,26 @@ const routes: Array = [ 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', diff --git a/src/shared/formatters.ts b/src/shared/formatters.ts index 1b6c4aea..e140b7b4 100644 --- a/src/shared/formatters.ts +++ b/src/shared/formatters.ts @@ -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 " " 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); } diff --git a/src/store/modules/botStoreWrapper.ts b/src/store/modules/botStoreWrapper.ts index 57ead006..3ed81a2a 100644 --- a/src/store/modules/botStoreWrapper.ts +++ b/src/store/modules/botStoreWrapper.ts @@ -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 + } }), ); }, diff --git a/src/store/modules/ftbot/index.ts b/src/store/modules/ftbot/index.ts index 131089d3..50fa28ad 100644 --- a/src/store/modules/ftbot/index.ts +++ b/src/store/modules/ftbot/index.ts @@ -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(); } }, diff --git a/src/store/modules/layout.ts b/src/store/modules/layout.ts index ecc3f36c..3f8a7307 100644 --- a/src/store/modules/layout.ts +++ b/src/store/modules/layout.ts @@ -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; }, diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 9acc4c9d..39fd68f3 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -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() { diff --git a/src/views/TradesList.vue b/src/views/TradesList.vue new file mode 100644 index 00000000..afc64cdb --- /dev/null +++ b/src/views/TradesList.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/src/views/Trading.vue b/src/views/Trading.vue index c14adb68..0ed009fc 100644 --- a/src/views/Trading.vue +++ b/src/views/Trading.vue @@ -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" > - + diff --git a/tests/unit/formatters.spec.ts b/tests/unit/formatters.spec.ts index 97192581..ceb6e70c 100644 --- a/tests/unit/formatters.spec.ts +++ b/tests/unit/formatters.spec.ts @@ -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'); + }); }); diff --git a/yarn.lock b/yarn.lock index 813a73ef..c2e235ae 100644 --- a/yarn.lock +++ b/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"