Skip to content

Commit

Permalink
feat(gui): Fund Statement enhance SimpleRR and InternalRR calculation (
Browse files Browse the repository at this point in the history
…#857)

* fix(ui): crypto hosts's protocol part

* feat(gui): Fund Statement enhance SimpleRR and InternalRR calculation
  • Loading branch information
zccz14 authored Oct 19, 2024
1 parent 4f06066 commit 95173f7
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 5 deletions.
10 changes: 9 additions & 1 deletion ui/web/src/modules/Fund/FundStatements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ registerPage('FundStatements', () => {
columnHelper.accessor('detail.after_tax_profit', {
header: () => '收益',
}),
columnHelper.accessor('detail.holding_days', {
header: () => '持有天数',
cell: (ctx) => `${Math.ceil(ctx.getValue())}`,
}),
columnHelper.accessor('detail.after_tax_profit_rate', {
header: () => '收益率',
header: () => '简单收益率',
cell: (ctx) => `${(ctx.getValue() * 100).toFixed(2)}%`,
}),
columnHelper.accessor('detail.after_tax_IRR', {
header: () => '内部收益率',
cell: (ctx) => `${(ctx.getValue() * 100).toFixed(2)}%`,
}),
columnHelper.accessor('meta.share', {
Expand Down
13 changes: 13 additions & 0 deletions ui/web/src/modules/Fund/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ export interface IFundState {
total_profit: number;
};
investors: Record<string, InvestorMeta>; // 投资人数据
investor_cashflow: Record<string, InvestorCashFlowItem[]>;
investor_derived: Record<string, InvestorInfoDerived>;
events: IFundEvent[];
}

export interface InvestorCashFlowItem {
updated_at: number;
deposit: number;
}

export interface InvestorMeta {
/** 姓名 */
name: string;
Expand All @@ -67,12 +73,18 @@ export interface InvestorMeta {
deposit: number;
/** 税率 */
tax_rate: number;
/** 创建时间 */
created_at: number;
}

/**
* 投资人信息的计算衍生数据
*/
export interface InvestorInfoDerived {
/** 持有时间 */
holding_days: number;
/** 资产在时间上的积分 */
timed_assets: number;
/** 税前资产 */
pre_tax_assets: number;
/** 应税额 */
Expand All @@ -85,6 +97,7 @@ export interface InvestorInfoDerived {
after_tax_profit: number;
/** 税后收益率 */
after_tax_profit_rate: number;
after_tax_IRR: number;
/** 税后份额 */
after_tax_share: number;

Expand Down
71 changes: 68 additions & 3 deletions ui/web/src/modules/Fund/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IFundEvent, IFundState } from './model';
import { IFundEvent, IFundState, InvestorCashFlowItem } from './model';

export const reduceState = (state: IFundState, event: IFundEvent): IFundState => {
const nextState = structuredClone(state);
Expand All @@ -24,6 +24,7 @@ export const reduceState = (state: IFundState, event: IFundEvent): IFundState =>
const deposit = event.order.deposit;
const investor = (nextState.investors[event.order.name] ??= {
name: event.order.name,
created_at: nextState.updated_at,
deposit: 0,
share: 0,
tax_threshold: 0,
Expand All @@ -33,6 +34,8 @@ export const reduceState = (state: IFundState, event: IFundEvent): IFundState =>
investor.tax_threshold += deposit;
investor.share += deposit / state.summary_derived.unit_price;
nextState.total_assets += deposit;
const cashflow = (nextState.investor_cashflow[event.order.name] ??= []);
cashflow.push({ updated_at: nextState.updated_at, deposit });
}
// 更新投资人信息
if (event.investor) {
Expand Down Expand Up @@ -66,23 +69,49 @@ export const reduceState = (state: IFundState, event: IFundEvent): IFundState =>

// 投资人衍生数据
Object.values(nextState.investors).forEach((v) => {
const investor = state.investor_derived[v.name];

const share_ratio =
nextState.summary_derived.total_share !== 0 ? v.share / nextState.summary_derived.total_share : 0;
const pre_tax_assets = v.share * nextState.summary_derived.unit_price;
const taxable = pre_tax_assets - v.tax_threshold;
const tax = Math.max(0, taxable * v.tax_rate);
const after_tax_assets = pre_tax_assets - tax;
const after_tax_profit = after_tax_assets - v.deposit;
// Assert: 税后收益和净入金不可能同时为负数
const after_tax_profit_rate = after_tax_profit / (v.deposit >= 0 ? v.deposit : after_tax_assets);
const timed_assets = investor
? investor.timed_assets +
((after_tax_assets + investor.after_tax_assets) * (nextState.updated_at - state.updated_at)) /
2 /
(365 * 86400_000)
: 0;
const holding_days = (nextState.updated_at - v.created_at) / 86400_000;
const avg_assets = (timed_assets / holding_days) * 365;
const after_tax_profit_rate = after_tax_profit / avg_assets;
const after_tax_share = after_tax_assets / nextState.summary_derived.unit_price;
const cashflow = nextState.investor_cashflow[v.name];
const after_tax_IRR =
nextState.updated_at - v.created_at > 0
? Math.pow(
1 +
XIRR(
cashflow.concat({
updated_at: nextState.updated_at,
deposit: -after_tax_assets,
}),
),
holding_days / 365,
) - 1
: 0;
nextState.investor_derived[v.name] = {
holding_days,
share_ratio,
taxable,
tax,
pre_tax_assets,
after_tax_assets,
after_tax_profit,
timed_assets,
after_tax_IRR,
after_tax_profit_rate,
after_tax_share,
};
Expand Down Expand Up @@ -121,9 +150,45 @@ export const getInitFundState = (): IFundState => ({
},
investors: {},
investor_derived: {},
investor_cashflow: {},
events: [],
});

export const fromFundEvents = (events: IFundEvent[]): IFundState => {
return events.reduce(reduceState, getInitFundState());
};

const XIRR = function xirr(cashflow: InvestorCashFlowItem[], guess = 0.05) {
// console.info('XIRR calculation started', cashflow);
const maxIterations = 100;
const tolerance = 1e-5;

let x0 = guess;
let x1;

for (let i = 0; i < maxIterations; i++) {
// console.info(`Iteration ${i}: ${x0}`);
let fValue = 0;
let fDerivative = 0;

for (let j = 0; j < cashflow.length; j++) {
const t = (cashflow[j].updated_at - cashflow[0].updated_at) / (1000 * 60 * 60 * 24 * 365); // Convert milliseconds to years
const expTerm = Math.exp(-x0 * t);
fValue -= cashflow[j].deposit * expTerm;
fDerivative += cashflow[j].deposit * expTerm * t;
}

fValue = fValue / cashflow.length;
fDerivative = fDerivative / cashflow.length;

x1 = x0 - fValue / fDerivative;

if (Math.abs(x1 - x0) < tolerance) {
return x1;
}

x0 = x1;
}

throw new Error('XIRR calculation did not converge');
};
2 changes: 1 addition & 1 deletion ui/web/src/modules/Workbench/HostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ registerPage('HostList', () => {
if (cryptoHosts$.value === undefined) return;
const label = (await showForm<string>({ type: 'string', title: 'Label' })) || '';
const keyPair = createKeyPair();
const url = new URL(`https://hosts.ntnl.io`);
const url = new URL(`wss://hosts.ntnl.io`);
url.searchParams.set('public_key', keyPair.public_key);
const signature = signMessage('', keyPair.private_key);
url.searchParams.set('signature', signature);
Expand Down

0 comments on commit 95173f7

Please sign in to comment.