From f8630a0f8064b75e4ead72500df4f7bb1b50c58c Mon Sep 17 00:00:00 2001 From: Ronan-Yann Lorin Date: Thu, 31 Oct 2024 19:09:42 +0100 Subject: [PATCH] Display opening dates details row for trades summary --- src/app/components/Number/Number.tsx | 12 +- .../Portfolio/Trade/TradesMonthlyTable.tsx | 208 +++++++++--------- .../Portfolio/Trade/TradesSummary.tsx | 4 +- src/app/utils.ts | 6 +- src/models/trade.model.ts | 10 +- src/routers/trades.types.ts | 4 +- src/routers/trades.utils.ts | 79 ++++--- 7 files changed, 178 insertions(+), 145 deletions(-) diff --git a/src/app/components/Number/Number.tsx b/src/app/components/Number/Number.tsx index c07b9b8..ab577e1 100644 --- a/src/app/components/Number/Number.tsx +++ b/src/app/components/Number/Number.tsx @@ -1,5 +1,6 @@ import { Text, TextProps } from "@chakra-ui/react"; import React, { FunctionComponent } from "react"; +import { formatNumber } from "../../utils"; type NumberProps = { /** Value to render */ @@ -15,19 +16,14 @@ type NumberProps = { const Number: FunctionComponent = ({ value, decimals = 0, + isPercent = false, color, - isPercent, ...rest }): React.ReactNode => { - const rounded = value - ? (isPercent ? value * 100 : value).toLocaleString(undefined, { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals, - }) + (isPercent ? "%" : "") - : "-"; + const rounded = formatNumber(value, decimals, isPercent); let style: { color: string }; if (color) { - style = { color }; + if (color !== "-") style = { color }; } else if (value > 0) { style = { color: "green.500" }; } else if (value < 0) { diff --git a/src/app/components/Portfolio/Trade/TradesMonthlyTable.tsx b/src/app/components/Portfolio/Trade/TradesMonthlyTable.tsx index 8c35088..5a2efb0 100644 --- a/src/app/components/Portfolio/Trade/TradesMonthlyTable.tsx +++ b/src/app/components/Portfolio/Trade/TradesMonthlyTable.tsx @@ -1,115 +1,117 @@ -import { Link, Table, TableCaption, TableContainer, Tbody, Td, Tfoot, Thead, Tr } from "@chakra-ui/react"; +import { HStack, Link, Text, VStack } from "@chakra-ui/react"; import React, { FunctionComponent } from "react"; import { Link as RouterLink } from "react-router-dom"; -import { TradeMonthlySynthesys } from "../../../../routers/trades.types"; -import { formatNumber } from "../../../utils"; +import { TradeMonthlyRow, TradeMonthlySynthesys, TradeMonthlySynthesysEntry } from "../../../../routers/trades.types"; import Number from "../../Number/Number"; type TradesMonthlyTableProps = { - title?: string; content: TradeMonthlySynthesys; }; -const TradesMonthlyTable: FunctionComponent = ({ - title, - content, - ..._rest -}): React.ReactNode => { +type minorRowProps = { + major: string; + index: string; + content: TradeMonthlySynthesysEntry; +}; + +const MinorRow: FunctionComponent = ({ major, index, content, ..._rest }): React.ReactNode => { + console.log("MinorRow", index, content); + return ( + <> + + + + + {index} + + + + + + + + + + + + ); +}; + +type majorRowProps = { + index: string; + content: TradeMonthlyRow; +}; + +const MajorRow: FunctionComponent = ({ index, content, ..._rest }): React.ReactNode => { + console.log("MajorRow", index, content); + return ( + <> + + + + {index} + + + + + + + + + + + + {Object.keys(content) + .sort((a: string, b: string) => b.localeCompare(a)) + .filter((index) => index !== "-") + .map((subIndex) => ( + + ))} + + ); +}; + +const TradesMonthlyTable: FunctionComponent = ({ content, ..._rest }): React.ReactNode => { + console.log("TradesMonthlyTable", content); return ( - - - - {title} ({Object.keys(content).length}) - - - - - - - - - - - - - - - {Object.keys(content) - .sort((a: string, b: string) => b.localeCompare(a)) - .map((key) => ( - - - - - - - - - - - ))} - - - - - - - - - - - - - -
Month#SuccessDurationMinAverageMaxPnL
- - {key} - - {content[key].count}{formatNumber((content[key].success / content[key].count) * 100)}%{formatNumber(content[key].duration / content[key].count)} - - - - - - - -
Total - {Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0)} - - {formatNumber( - (Object.values(content).reduce((p: number, v) => (p += v.success || 0), 0) / - Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0)) * - 100, - )} - % - - {formatNumber( - Object.values(content).reduce((p: number, v) => (p += v.duration || 0), 0) / - Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0), - )} - - (p ? Math.min(p, v.min) : v.min), undefined)} - /> - - (p += v.total || 0), 0) / - Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0) - } - fontWeight="bold" - /> - - (p ? Math.max(p, v.max) : v.max), undefined)} - /> - - (p += v.total || 0), 0)} - fontWeight="bold" - /> -
-
+ <> + + + + Close + + + Open + + + # + + + Success + + + Duration + + + Min + + + Average + + + Max + + + PnL + + + {Object.keys(content) + .sort((a: string, b: string) => b.localeCompare(a)) + .map((index) => ( + + ))} + + ); }; diff --git a/src/app/components/Portfolio/Trade/TradesSummary.tsx b/src/app/components/Portfolio/Trade/TradesSummary.tsx index f7411e8..34c4c36 100644 --- a/src/app/components/Portfolio/Trade/TradesSummary.tsx +++ b/src/app/components/Portfolio/Trade/TradesSummary.tsx @@ -15,7 +15,7 @@ const TradeSummary: FunctionComponent = ({ ..._rest }): React { label: "PnL", data: labels.reduce((p, v) => { - p.push(theSynthesys.byMonth[v].total); // eslint-disable-line @typescript-eslint/no-unsafe-argument + p.push(theSynthesys.byMonth[v]["-"].total); // eslint-disable-line @typescript-eslint/no-unsafe-argument return p; }, [] as number[]), }, @@ -44,7 +44,7 @@ const TradeSummary: FunctionComponent = ({ ..._rest }): React Closed trades by month - + ); }; diff --git a/src/app/utils.ts b/src/app/utils.ts index 248ad1a..84ca1ac 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -3,11 +3,11 @@ export const obfuscate = (text: string): string => { return len < 4 ? "****" : text.slice(0, 2) + "*".repeat(len - 4) + text.slice(len - 2, len); }; -export const formatNumber = (value: number | undefined, decimals = 0): string => { +export const formatNumber = (value: number | undefined, decimals = 0, isPercent = false): string => { return value - ? value.toLocaleString(undefined, { + ? (isPercent ? value * 100 : value).toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, - }) + }) + (isPercent ? "%" : "") : "-"; }; diff --git a/src/models/trade.model.ts b/src/models/trade.model.ts index 647d243..471afb3 100644 --- a/src/models/trade.model.ts +++ b/src/models/trade.model.ts @@ -1,4 +1,11 @@ -import { CreationOptional, ForeignKey, InferAttributes, InferCreationAttributes, NonAttribute } from "sequelize"; +import { + CreationOptional, + ForeignKey, + HasManyGetAssociationsMixin, + InferAttributes, + InferCreationAttributes, + NonAttribute, +} from "sequelize"; import { BelongsTo, Column, DataType, HasMany, Model, Table } from "sequelize-typescript"; import { Contract } from "./contract.model"; import { expirationToDate } from "./date_utils"; @@ -69,6 +76,7 @@ export class Trade extends Model< @HasMany(() => Statement, { foreignKey: "trade_unit_id" }) declare statements?: Statement[]; + declare getStatements: HasManyGetAssociationsMixin; /** Total duration if trade is closed or current duration is trade is open */ get duration(): NonAttribute { diff --git a/src/routers/trades.types.ts b/src/routers/trades.types.ts index 35384e5..7aaabe9 100644 --- a/src/routers/trades.types.ts +++ b/src/routers/trades.types.ts @@ -55,7 +55,7 @@ export type TradeMonthlySynthesysEntry = { max: number; total: number; }; -export type TradeMonthlySynthesys = Record<"string", TradeMonthlySynthesysEntry>; - +export type TradeMonthlyRow = Record<"string", TradeMonthlySynthesysEntry>; +export type TradeMonthlySynthesys = Record<"string", TradeMonthlyRow>; export type TradeSynthesys = { open: TradeEntry[]; byMonth: TradeMonthlySynthesys }; export type OpenTradesWithPositions = { trades: TradeEntry[]; positions: (PositionEntry | OptionPositionEntry)[] }; diff --git a/src/routers/trades.utils.ts b/src/routers/trades.utils.ts index 147d823..1eee212 100644 --- a/src/routers/trades.utils.ts +++ b/src/routers/trades.utils.ts @@ -187,14 +187,6 @@ export const tradeModelToTradeEntry = async ( }, ): Promise => { // Init TradeEntry - console.log( - "tradeModelToTradeEntry", - thisTrade.id, - thisTrade.risk, - thisTrade.PnL, - thisTrade.expectedDuration, - thisTrade.expiryPnl, - ); let apy: number | undefined = undefined; switch (thisTrade.status) { case TradeStatus.open: @@ -604,32 +596,67 @@ export const makeSynthesys = async (trades: Trade[]): Promise => return trades.reduce( async (p, item) => p.then(async (theSynthesys) => { + if (item.PnL && !item.pnlInBase) { + // Update missing data, should run only once + item.statements = await item.getStatements(); + await updateTradeDetails(item); + await item.save(); + } if (item.closingDate) { const idx = formatDate(item.closingDate); + const idy = formatDate(item.openingDate); if (theSynthesys.byMonth[idx] === undefined) { - theSynthesys.byMonth[idx] = { count: 0, success: 0, duration: 0, min: undefined, max: undefined, total: 0 }; + theSynthesys.byMonth[idx] = { + "-": { + count: 0, + success: 0, + duration: 0, + min: undefined, + max: undefined, + total: 0, + }, + }; } - theSynthesys.byMonth[idx].count += 1; - theSynthesys.byMonth[idx].duration += item.duration; + if (theSynthesys.byMonth[idx][idy] === undefined) { + theSynthesys.byMonth[idx][idy] = { + count: 0, + success: 0, + duration: 0, + min: undefined, + max: undefined, + total: 0, + }; + } + theSynthesys.byMonth[idx][idy].count += 1; + theSynthesys.byMonth[idx][idy].duration += item.duration; if (item.pnlInBase) { - if (item.pnlInBase > 0) theSynthesys.byMonth[idx].success += 1; - theSynthesys.byMonth[idx].total += item.pnlInBase; - theSynthesys.byMonth[idx].min = theSynthesys.byMonth[idx].min - ? Math.min(theSynthesys.byMonth[idx].min as number, item.pnlInBase) + if (item.pnlInBase > 0) theSynthesys.byMonth[idx][idy].success += 1; + theSynthesys.byMonth[idx][idy].total += item.pnlInBase; + theSynthesys.byMonth[idx][idy].min = theSynthesys.byMonth[idx][idy].min + ? Math.min(theSynthesys.byMonth[idx][idy].min as number, item.pnlInBase) : item.pnlInBase; - theSynthesys.byMonth[idx].max = theSynthesys.byMonth[idx].max - ? Math.max(theSynthesys.byMonth[idx].max as number, item.pnlInBase) + theSynthesys.byMonth[idx][idy].max = theSynthesys.byMonth[idx][idy].max + ? Math.max(theSynthesys.byMonth[idx][idy].max as number, item.pnlInBase) : item.pnlInBase; } else if (item.PnL) { - // TODO: multiply by baseRate - if (item.PnL > 0) theSynthesys.byMonth[idx].success += 1; - theSynthesys.byMonth[idx].total += item.PnL; - theSynthesys.byMonth[idx].min = theSynthesys.byMonth[idx].min - ? Math.min(theSynthesys.byMonth[idx].min as number, item.PnL) - : item.PnL; - theSynthesys.byMonth[idx].max = theSynthesys.byMonth[idx].max - ? Math.max(theSynthesys.byMonth[idx].max as number, item.PnL) - : item.PnL; + logger.log( + LogLevel.Error, + MODULE + ".makeSynthesys", + undefined, + `pnlInBase is missing for trade #${item.id}, incorrect data returned`, + ); + } + theSynthesys.byMonth[idx]["-"].count += 1; + theSynthesys.byMonth[idx]["-"].duration += item.duration; + if (item.pnlInBase) { + if (item.pnlInBase > 0) theSynthesys.byMonth[idx]["-"].success += 1; + theSynthesys.byMonth[idx]["-"].total += item.pnlInBase; + theSynthesys.byMonth[idx]["-"].min = theSynthesys.byMonth[idx]["-"].min + ? Math.min(theSynthesys.byMonth[idx]["-"].min as number, item.pnlInBase) + : item.pnlInBase; + theSynthesys.byMonth[idx]["-"].max = theSynthesys.byMonth[idx]["-"].max + ? Math.max(theSynthesys.byMonth[idx]["-"].max as number, item.pnlInBase) + : item.pnlInBase; } return theSynthesys; } else {