Skip to content

Commit

Permalink
Merge pull request #149 from glorat/wip
Browse files Browse the repository at this point in the history
feat: pnl can expand expenses
  • Loading branch information
glorat authored Jan 18, 2024
2 parents edd5d1a + 656fef8 commit da15839
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 66 deletions.
2 changes: 1 addition & 1 deletion client/src/boot/notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface MyNotify {
warning(msg: string): void
}

const qnotify: MyNotify = {
export const qnotify: MyNotify = {
success(msg) {
Notify.create({
type: 'positive',
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/TreeTableNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
name: 'TreeTableNode',
props: {
node: Object as () => TreeTableDTO,
depth: Number
depth: { type: Number, required: true },
},
data(): {toggled: Record<string, boolean>} {
const ret: {toggled: Record<string, boolean>} = {
Expand Down
33 changes: 26 additions & 7 deletions client/src/lib/PLExplain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)));
Expand Down Expand Up @@ -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 => {
Expand All @@ -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;
Expand All @@ -103,7 +118,8 @@ export function pnlExplain(startDate: LocalDate, toDate: LocalDate, allPostings:
newActivityPnl, newActivityByAccount,
totalEquity,
totalIncome, totalYieldIncome,
totalExpense, totalDeltaExplain,
totalExpense, expenseByAccount,
totalDeltaExplain,
delta};
}

Expand Down Expand Up @@ -143,6 +159,8 @@ 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: exps[0].expenseByAccount.map(e => e.accountId)
.map((accountId,idx) => ({accountId, value: sum(exps.map(e => e.expenseByAccount[idx].value))})),
delta: [],
tenor: 'total'
}
Expand All @@ -162,6 +180,7 @@ function dividePnlExplain(p: PLExplainDTO, n: number):PLExplainDTO {
totalExpense: p.totalExpense/n,
totalDeltaExplain: p.totalDeltaExplain/n,
delta: [], // not supported,
expenseByAccount: p.expenseByAccount.map(x => ({accountId: x.accountId, value: x.value/n})),
tenor: 'avg', // that's why we do a division
}
}
9 changes: 9 additions & 0 deletions client/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ':'))
}
Expand Down
110 changes: 54 additions & 56 deletions client/src/pages/PnlExplain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- "totalEquity" -> totalEquity, "totalIncome" -> totalIncome, "totalExpense" -> totalExpense, "totalDeltaExplain" -> totalDeltaExplain-->
<!-- // , "delta" -> deltaExplain-->
<!-- )-->
<table class="sortable">
<table class="sortable">
<tbody>
<tr>
<td></td>
Expand Down Expand Up @@ -69,9 +69,16 @@
<td class="num" v-for="explainData in explains">{{ explainData.totalIncome.toFixed(2) }}</td>
</tr>
<tr>
<td>Expenses</td>
<td><q-btn round flat size="xs" :icon="expansions['expenses'] ? matRemoveCircleOutline : matAddCircleOutline" @click="expansions['expenses'] = !expansions['expenses']"></q-btn>
Expenses</td>
<td class="num" v-for="explainData in explains">{{ explainData.totalExpense.toFixed(2) }}</td>
</tr>
<template v-if="expansions['expenses']">
<tr v-for="(rec, recIdx) in explains[explains.length-1].expenseByAccount">
<td> - {{ rec.accountId}}</td>
<td class="num" v-for="explainData in explains">{{ explainData.expenseByAccount[recIdx]?.value?.toFixed(2) }}</td>
</tr>
</template>
<tr>
<td>Equity</td>
<td class="num" v-for="explainData in explains">{{ explainData.totalEquity.toFixed(2) }}</td>
Expand Down Expand Up @@ -99,62 +106,53 @@
</my-page>
</template>

<script>
import HelpTip from '../components/HelpTip';
import { mapGetters } from 'vuex';
import { apiPnlExplainMonthly } from '../lib/apiFacade';
import { matAnalytics } from '@quasar/extras/material-icons'
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,
};
}
}
<script setup lang="ts">
import {computed, onMounted, reactive, ref} from 'vue';
import HelpTip from '../components/HelpTip.vue';
import {apiPnlExplainMonthly} from '../lib/apiFacade';
import {matAddCircleOutline, matAnalytics, matRemoveCircleOutline} from '@quasar/extras/material-icons';
import {PLExplainDTO} from 'src/lib/PLExplain';
import {qnotify} from 'boot/notify';
import {useStore} from 'src/store';
import {router} from 'src/router';
const store = useStore();
const explains = ref<PLExplainDTO[]>([]);
const baseCcy = computed(() => store.getters.baseCcy);
const allPostingsEx = computed(() => store.getters.allPostingsEx);
const fxConverter = computed(() => store.getters.fxConverter);
const expansions = reactive<Record<string, boolean>>({})
const refresh = async () => {
const notify = qnotify;
try {
explains.value = await apiPnlExplainMonthly(store);
} catch (error: any) {
console.error(error);
notify.error(error);
}
};
const onColumnClick = (explain: PLExplainDTO) => {
if (explain.fromDate && explain.toDate) {
router.push({ name: 'pnldetail', params: { fromDate: explain.fromDate, toDate: explain.toDate } });
}
};
const percChange = (explainData: PLExplainDTO) => {
const denom = explainData.toNetworth ? explainData.toNetworth - explainData.actual : 0.0;
return denom === 0.0 ? 0.0 : explainData.actual / denom;
};
const amount = (value: number) => value.toFixed(2);
const perc = (value: number) => (100 * value).toFixed(1) + '%';
onMounted(refresh);
</script>




<style scoped>
.subtotal {
border-top-color: black;
Expand Down
18 changes: 18 additions & 0 deletions client/src/pages/PnlExplainDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@
<!-- <td class="subtitle">Equity</td>-->
<!-- <td class="num">{{ explainData.totalEquity.toFixed(2) }}</td>-->
<!-- </tr>-->
<template v-if="explainData.expenseByAccount.length">
<tr v-if="explainData.expenseByAccount.length">
<td colspan="4" class="subtitle">Expenses</td>
<td>P&L</td>
</tr>
<tr v-for="exp in explainData.expenseByAccount">
<td colspan="4">{{ exp.accountId }}</td>
<td class="num">{{ exp.value.toFixed(2) }}</td>
</tr>
<tr>
<td class="subtotal" colspan="4">Total</td>
<td class="subtotal num">{{ explainData.totalExpense.toFixed(2) }}</td>
</tr>
<tr><td></td><td></td></tr>
</template>

<tr v-if="explainData.newActivityByAccount.length">
<td colspan="4" class="subtitle">New Activity</td>
<td>P&L</td>
Expand Down Expand Up @@ -219,7 +235,9 @@ export default defineComponent({
},
data() {
const explains: PLExplainDTO[] = [];
const expansions: Record<string, boolean> = {};
return {
expansions,
explains,
};
},
Expand Down
2 changes: 1 addition & 1 deletion client/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export default store(function (/* { ssrContext } */) {
return Store
})

export function useStore() {
export function useStore(): VuexStore<MyState> {
return vuexUseStore(storeKey)
}

Expand Down
Loading

0 comments on commit da15839

Please sign in to comment.