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<NumberProps> = ({ 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<TradesMonthlyTableProps> = ({ - title, - content, - ..._rest -}): React.ReactNode => { +type minorRowProps = { + major: string; + index: string; + content: TradeMonthlySynthesysEntry; +}; + +const MinorRow: FunctionComponent<minorRowProps> = ({ major, index, content, ..._rest }): React.ReactNode => { + console.log("MinorRow", index, content); + return ( + <> + <HStack> + <Text width="100px"></Text> + <Text width="100px"> + <Link to={`../../month/${major.substring(0, 4)}/${major.substring(5)}`} as={RouterLink}> + {index} + </Link> + </Text> + <Number value={content.count} color="-" width="100px" /> + <Number value={content.success / content.count} isPercent color="-" width="100px" /> + <Number value={content.duration / content.count} color="-" width="100px" /> + <Number value={content.min} width="100px" /> + <Number value={content.total / content.count} width="100px" /> + <Number value={content.max} width="100px" /> + <Number value={content.total} width="100px" /> + </HStack> + </> + ); +}; + +type majorRowProps = { + index: string; + content: TradeMonthlyRow; +}; + +const MajorRow: FunctionComponent<majorRowProps> = ({ index, content, ..._rest }): React.ReactNode => { + console.log("MajorRow", index, content); + return ( + <> + <HStack> + <Text width="100px"> + <Link to={`../../month/${index.substring(0, 4)}/${index.substring(5)}`} as={RouterLink}> + {index} + </Link> + </Text> + <Text width="100px"></Text> + <Number value={content["-"].count} color="-" width="100px" /> + <Number value={content["-"].success / content["-"].count} isPercent color="-" width="100px" /> + <Number value={content["-"].duration / content["-"].count} color="-" width="100px" /> + <Number value={content["-"].min} width="100px" /> + <Number value={content["-"].total / content["-"].count} width="100px" /> + <Number value={content["-"].max} width="100px" /> + <Number value={content["-"].total} width="100px" /> + </HStack> + {Object.keys(content) + .sort((a: string, b: string) => b.localeCompare(a)) + .filter((index) => index !== "-") + .map((subIndex) => ( + <MinorRow key={subIndex} major={index} index={subIndex} content={content[subIndex]} /> + ))} + </> + ); +}; + +const TradesMonthlyTable: FunctionComponent<TradesMonthlyTableProps> = ({ content, ..._rest }): React.ReactNode => { + console.log("TradesMonthlyTable", content); return ( - <TableContainer> - <Table variant="simple" size="sm"> - <TableCaption> - {title} ({Object.keys(content).length}) - </TableCaption> - <Thead> - <Tr> - <Td>Month</Td> - <Td>#</Td> - <Td>Success</Td> - <Td>Duration</Td> - <Td>Min</Td> - <Td>Average</Td> - <Td>Max</Td> - <Td>PnL</Td> - </Tr> - </Thead> - <Tbody> - {Object.keys(content) - .sort((a: string, b: string) => b.localeCompare(a)) - .map((key) => ( - <Tr key={key}> - <Td> - <Link to={`../../month/${key.substring(0, 4)}/${key.substring(5)}`} as={RouterLink}> - {key} - </Link> - </Td> - <Td isNumeric>{content[key].count}</Td> - <Td isNumeric>{formatNumber((content[key].success / content[key].count) * 100)}%</Td> - <Td isNumeric>{formatNumber(content[key].duration / content[key].count)}</Td> - <Td> - <Number value={content[key].min} /> - </Td> - <Td> - <Number value={content[key].total / content[key].count} /> - </Td> - <Td> - <Number value={content[key].max} /> - </Td> - <Td> - <Number value={content[key].total} /> - </Td> - </Tr> - ))} - </Tbody> - <Tfoot> - <Tr> - <Td fontWeight="bold">Total</Td> - <Td isNumeric fontWeight="bold"> - {Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0)} - </Td> - <Td isNumeric fontWeight="bold"> - {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, - )} - % - </Td> - <Td isNumeric fontWeight="bold"> - {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), - )} - </Td> - <Td> - <Number - value={Object.values(content).reduce((p: number, v) => (p ? Math.min(p, v.min) : v.min), undefined)} - /> - </Td> - <Td> - <Number - value={ - Object.values(content).reduce((p: number, v) => (p += v.total || 0), 0) / - Object.values(content).reduce((p: number, v) => (p += v.count || 0), 0) - } - fontWeight="bold" - /> - </Td> - <Td> - <Number - value={Object.values(content).reduce((p: number, v) => (p ? Math.max(p, v.max) : v.max), undefined)} - /> - </Td> - <Td> - <Number - value={Object.values(content).reduce((p: number, v) => (p += v.total || 0), 0)} - fontWeight="bold" - /> - </Td> - </Tr> - </Tfoot> - </Table> - </TableContainer> + <> + <VStack> + <HStack> + <Text width="100px" align="center"> + Close + </Text> + <Text width="100px" align="center"> + Open + </Text> + <Text width="100px" align="center"> + # + </Text> + <Text width="100px" align="center"> + Success + </Text> + <Text width="100px" align="center"> + Duration + </Text> + <Text width="100px" align="center"> + Min + </Text> + <Text width="100px" align="center"> + Average + </Text> + <Text width="100px" align="center"> + Max + </Text> + <Text width="100px" align="center"> + PnL + </Text> + </HStack> + {Object.keys(content) + .sort((a: string, b: string) => b.localeCompare(a)) + .map((index) => ( + <MajorRow key={index} index={index} content={content[index]} /> + ))} + </VStack> + </> ); }; 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<TradeSummaryProps> = ({ ..._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<TradeSummaryProps> = ({ ..._rest }): React </Box> <Text>Closed trades by month</Text> <BarChart title="Realized Performance" labels={labels} datasets={datasets} /> - <TradesMonthlyTable content={theSynthesys.byMonth} title="Closed trades" /> + <TradesMonthlyTable content={theSynthesys.byMonth} /> </> ); }; 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<Statement>; /** Total duration if trade is closed or current duration is trade is open */ get duration(): NonAttribute<number> { 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<TradeEntry> => { // 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<TradeSynthesys> => 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 {