From 2c6ae5ce76760a6a6634db645ccb66786638a1a0 Mon Sep 17 00:00:00 2001 From: Kevin Tam Date: Wed, 17 Jan 2024 17:34:44 +0800 Subject: [PATCH 1/5] chore: fix ts warning on TreeTableNode prop --- client/src/components/TreeTableNode.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/TreeTableNode.vue b/client/src/components/TreeTableNode.vue index db718df6..3b2c3fb7 100644 --- a/client/src/components/TreeTableNode.vue +++ b/client/src/components/TreeTableNode.vue @@ -29,7 +29,7 @@ name: 'TreeTableNode', props: { node: Object as () => TreeTableDTO, - depth: Number + depth: { type: Number, required: true }, }, data(): {toggled: Record} { const ret: {toggled: Record} = { From c005188242d7eaea602ed50a42ca5b4a509a5adf Mon Sep 17 00:00:00 2001 From: Kevin Tam Date: Wed, 17 Jan 2024 17:35:10 +0800 Subject: [PATCH 2/5] chore: wip for PnlExplain on subaccounts --- client/src/boot/notify.ts | 2 +- client/src/lib/PLExplain.ts | 32 +++++-- client/src/lib/utils.ts | 9 ++ client/src/pages/PnlExplain.vue | 125 +++++++++++++++----------- client/src/pages/PnlExplainDetail.vue | 10 +++ client/src/store/index.ts | 2 +- 6 files changed, 117 insertions(+), 63 deletions(-) diff --git a/client/src/boot/notify.ts b/client/src/boot/notify.ts index 9ef5cc51..2f084826 100644 --- a/client/src/boot/notify.ts +++ b/client/src/boot/notify.ts @@ -7,7 +7,7 @@ interface MyNotify { warning(msg: string): void } -const qnotify: MyNotify = { +export const qnotify: MyNotify = { success(msg) { Notify.create({ type: 'positive', diff --git a/client/src/lib/PLExplain.ts b/client/src/lib/PLExplain.ts index 326c5bb1..287a0a94 100644 --- a/client/src/lib/PLExplain.ts +++ b/client/src/lib/PLExplain.ts @@ -2,12 +2,13 @@ import {LocalDate} from '@js-joda/core'; import {AccountCommandDTO, PostingEx} from 'src/lib/assetdb/models'; import { - isSubAccountOf, + getAccountLevel, + isSubAccountOf, parentAccountIdOf, positionSetFx, positionUnderAccount, postingsToPositionSet } from 'src/lib/utils'; -import { keys, groupBy, sum, range } from 'lodash'; +import {keys, groupBy, sum, range, uniq} from 'lodash'; import {SingleFXConverter} from 'src/lib/fx'; export interface PLExplainDTO { @@ -17,13 +18,19 @@ export interface PLExplainDTO { newActivityPnl: number, newActivityByAccount: {accountId: string, explain: number}[] totalEquity: number totalIncome: number, totalYieldIncome: number - totalExpense: number, totalDeltaExplain: number + totalExpense: number, expenseByAccount: {accountId: string, value: number}[] + totalDeltaExplain: number delta: any[] tenor?: string } export function pnlExplain(startDate: LocalDate, toDate: LocalDate, allPostings: PostingEx[], allCmds: AccountCommandDTO[], baseCcy: string, fxConverter: SingleFXConverter): PLExplainDTO { + // for leveled breakdown + const levels = 2 + const allAccountIds = uniq(allCmds.map(p => p.accountId)) + const allAccountLevels = uniq( allAccountIds.map(id => getAccountLevel(id, levels)) ) + const postings = allPostings.filter(p => isSubAccountOf(p.account, 'Assets')); // FIXME: +Liabilities const fromDate = startDate.minusDays(1) // So we do inclusivity of the startDate const toStartPostings = postings.filter(p => LocalDate.parse(p.date).isBefore(fromDate.plusDays(1))); @@ -67,7 +74,6 @@ export function pnlExplain(startDate: LocalDate, toDate: LocalDate, allPostings: }).filter(x => x.explain !== 0.0) // mapValues(activityPostingsByAccount, (ps: PostingEx[]) => positionSetFx(postingsToPositionSet(ps), baseCcy, LocalDate.parse(ps[0].date), fxConverter)) const newActivityPnl = sum(newActivityByAccount.map(x => x.explain)) - const equityPositionChange = positionUnderAccount(duringPostings, 'Equity') const yieldPostings = duringPostings.filter( p => { @@ -79,11 +85,20 @@ export function pnlExplain(startDate: LocalDate, toDate: LocalDate, allPostings: }); const totalYieldIncome = sum(yieldIncomes) - + const equityPositionChange = positionUnderAccount(duringPostings, 'Equity') const totalEquity = -positionSetFx(equityPositionChange, baseCcy, toDate, fxConverter) const totalIncome = -positionSetFx(positionUnderAccount(duringPostings, 'Income'), baseCcy, toDate, fxConverter) - totalYieldIncome - const totalExpense = positionSetFx(positionUnderAccount(duringPostings, 'Expenses'), baseCcy, toDate, fxConverter) + const expensePositionChange = positionUnderAccount(duringPostings, 'Expenses') + const totalExpense = positionSetFx(expensePositionChange, baseCcy, toDate, fxConverter) + + // Do an l2 breakdown detail of expenses + const expenseAccountIdBreakdown = allAccountLevels.filter(a => parentAccountIdOf(a) === 'Expenses') + const expenseByAccount = expenseAccountIdBreakdown.map(accountId => { + const ps = positionUnderAccount(duringPostings, accountId) + const value = positionSetFx(ps, baseCcy, toDate, fxConverter) + return {accountId, value} + }) const explained = totalDeltaExplain + newActivityPnl + totalEquity + totalIncome + totalYieldIncome - totalExpense const unexplained = actual - explained; @@ -103,7 +118,8 @@ export function pnlExplain(startDate: LocalDate, toDate: LocalDate, allPostings: newActivityPnl, newActivityByAccount, totalEquity, totalIncome, totalYieldIncome, - totalExpense, totalDeltaExplain, + totalExpense, expenseByAccount, + totalDeltaExplain, delta}; } @@ -143,6 +159,7 @@ function totalPnlExplain(exps: PLExplainDTO[]):PLExplainDTO { totalYieldIncome: sum(exps.map(e => e.totalYieldIncome)), totalExpense: sum(exps.map(e => e.totalExpense)), totalDeltaExplain: sum(exps.map(e => e.totalDeltaExplain)), + expenseByAccount: [], delta: [], tenor: 'total' } @@ -162,6 +179,7 @@ function dividePnlExplain(p: PLExplainDTO, n: number):PLExplainDTO { totalExpense: p.totalExpense/n, totalDeltaExplain: p.totalDeltaExplain/n, delta: [], // not supported, + expenseByAccount: [], // TODO tenor: 'avg', // that's why we do a division } } diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts index fb6a5c3a..3ee1181a 100644 --- a/client/src/lib/utils.ts +++ b/client/src/lib/utils.ts @@ -23,6 +23,15 @@ export function parentAccountIdOf(accountId: string):string { } } +export function getAccountLevel(accountName: string, n: number): string { + const accountParts = accountName.split(':'); + if (accountParts.length <= n) { + return accountName; + } else { + return accountParts.slice(0, n).join(':'); + } +} + export function isSubAccountOf(accountId: string, parentId: string): boolean { return (accountId === parentId) || (accountId.startsWith(parentId + ':')) } diff --git a/client/src/pages/PnlExplain.vue b/client/src/pages/PnlExplain.vue index 3e720545..4e4eb836 100644 --- a/client/src/pages/PnlExplain.vue +++ b/client/src/pages/PnlExplain.vue @@ -7,7 +7,8 @@ - +
{{ explains[explains.length-1].expenseByAccount }}
+
@@ -69,9 +70,20 @@ - + + + + + + + + + @@ -99,61 +111,66 @@ - - export default { - name: 'PnlExplain', - components: { - HelpTip, - }, - computed: { - ...mapGetters([ - 'baseCcy', - 'allPostingsEx', - 'fxConverter', - ]), - }, - mounted() { - this.refresh() - }, - methods: { - async refresh() { - const notify = this.$notify; - try { - this.explains = await apiPnlExplainMonthly(this.$store); - } catch (error) { - console.error(error); - notify.error(error); - } - }, - onColumnClick(explain) { - if (explain.fromDate && explain.toDate) { - this.$router.push({name: 'pnldetail', params: {fromDate: explain.fromDate, toDate: explain.toDate}}); - } - }, - percChange(explainData) { - const denom = explainData.toNetworth ? explainData.toNetworth - explainData.actual : 0.0; - return (denom === 0.0) ? 0.0 : explainData.actual / denom; - }, - amount: (value) => value.toFixed(2), - perc: (value) => (100*value).toFixed(1) + '%', - }, - data() { - // eslint-disable-next-line - const self = this; - return { - explains: [], - matAnalytics, - }; - } - } -
{{ explainData.totalIncome.toFixed(2) }}
Expenses Expenses {{ explainData.totalExpense.toFixed(2) }}
{{ rec.accountId}}{{ explainData.expenseByAccount[recIdx]?.value?.toFixed(2) }}
+ + New Activity P&L + {{ explainData.newActivityPnl.toFixed(2) }}
Equity {{ explainData.totalEquity.toFixed(2) }}