Skip to content

Commit

Permalink
205 compare jaws against indices on portfolioview (#236)
Browse files Browse the repository at this point in the history
* NavBar: make links use a tag

* Add endpoint to get portfolio historyu

* Fix handleResult so it doesn't nest errors 3 levels

* portfolioHistory: Make timestamps to ISO strings

* Add params to getPortfolioHistory endpoint

* Add very naive calculatePNL

* WIP fix test

* PNL: Add support for sales split into TP/SL

* Portfolio: Add cash box; refactor

* Add calculateNAV util

* NavBar: use AccountStore instead of AccountContext

* Replace NAV calculation with correct one

* WIP on trigger-calculate-nav

* calculateNAV: Improve tests, add info; update variable names

* Add getDateString helper to get YYYY-MM-DD string from any date

* Add trigger-calculate-nav endpoint

* add endpoint to get daily stats

* Rename api/nav -> api/daily-stats, cleanup

* BackendService: Add getDailyStats func

* Rename trigger nav -> daily-stats

* calculatePNL.test: remove empty arrow function

fixes build error
  • Loading branch information
mold authored Feb 16, 2023
1 parent 665b0bc commit 98188b3
Show file tree
Hide file tree
Showing 20 changed files with 833 additions and 71 deletions.
28 changes: 10 additions & 18 deletions src/components/organisms/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { signInWithGoogle } from "@jaws/auth/firestoreAuth";
import { getToday } from "@jaws/lib/helpers";
import { AccountContext } from "@jaws/store/account/accountContext";
import { useAccountStore } from "@jaws/store/account/accountContext";
import { User } from "@jaws/store/account/accountStore";
import { Theme } from "@jaws/styles/themes";
import { setCookies } from "cookies-next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useContext, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import styled, { css } from "styled-components";
import { useStore } from "zustand";
import Button from "../atoms/buttons/Button";

const NavBarContainer = styled.div`
Expand Down Expand Up @@ -44,15 +43,17 @@ const LinksContainer = styled.div`
gap: 15px;
`;

const NavBarItem = styled.div`
const NavBarItem = styled.a`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
text-decoration: none;
:hover {
color: ${({ theme }) => theme.palette.actionHover.text};
transform: scale(1.05);
text-decoration: none;
}
${({ active }: { active: boolean }) =>
active &&
Expand All @@ -74,10 +75,7 @@ const Navbar = () => {
setPathName(router.pathname);
}, [router]);

const store = useContext(AccountContext);
if (!store) throw new Error("Missing AccountContext.Provider in the tree");
const [isLoggedIn, setIsLoggedIn, setUser, logoutUser] = useStore(
store,
const [isLoggedIn, setIsLoggedIn, setUser, logoutUser] = useAccountStore(
(state) => [
state.isLoggedIn,
state.setIsLoggedIn,
Expand All @@ -103,21 +101,15 @@ const Navbar = () => {
<LogoContainer />
</Link>
<LinksContainer>
<Link href={`/daily-runs/${today}`}>
<NavBarItem
active={
pathName === "/daily-runs" ||
pathName === "/daily-runs/[date]" ||
pathName === "/daily-runs/[date]/[time]"
}
>
<Link href={`/daily-runs/${today}`} passHref>
<NavBarItem active={pathName.startsWith("/daily-runs")}>
Todays run
</NavBarItem>
</Link>
<Link href="/orders">
<Link href="/orders" passHref>
<NavBarItem active={pathName === "/orders"}>All orders</NavBarItem>
</Link>
<Link href="/portfolio">
<Link href="/portfolio" passHref>
<NavBarItem active={pathName === "/portfolio"}>Portfolio</NavBarItem>
</Link>
</LinksContainer>
Expand Down
2 changes: 1 addition & 1 deletion src/components/organisms/WidgetGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from "styled-components";

const GridContainer = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 10px;
`;

Expand Down
34 changes: 34 additions & 0 deletions src/db/dailyStatsEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { db } from "@jaws/services/firestoreService";
import { DailyStats } from "./dailyStatsMeta";
export async function getDailyStats({
startDate,
endDate,
accountId,
}: {
/** Format: YYYY-MM-DD (inclusive) */
startDate: string;
/** Format: YYYY-MM-DD (inclusive) */
endDate: string;
accountId: string;
}) {
return (
await db
.collection("daily-stats")
.where("accountId", "==", accountId)
.where("date", ">=", startDate)
.where("date", "<=", endDate)
.orderBy("date", "asc")
.get()
).docs.map((doc) => doc.data()) as DailyStats[];
}

export async function upsertDailyStats(stats: DailyStats) {
return db
.collection("daily-stats")
.doc(generateDocId(stats.accountId, stats.date))
.set(stats);
}

function generateDocId(accountId: string, date: string) {
return `${date.replace(/-/g, "")}-${accountId}`;
}
11 changes: 11 additions & 0 deletions src/db/dailyStatsMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface DailyStats {
accountId: string;
nav: number;
shares: number;
date: string;
/**
* TODO: Remove when not needed anymore :)
* @deprecated don't rely on this for anything
*/
debugInfo?: Record<string, any>;
}
15 changes: 15 additions & 0 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ function addZero(value: number) {
return value < 10 ? `0${value}` : value;
}

export const getDateString = ({
date,
withDashes,
}: {
date: Date;
withDashes: boolean;
}) => {
const day = date.getDate();
const month = date.getMonth() + 1;

return [date.getFullYear(), addZero(month), addZero(day)].join(
withDashes ? "-" : "",
);
};

export const getToday = () => {
const now = new Date();
const nowDate = now.getDate();
Expand Down
71 changes: 35 additions & 36 deletions src/lib/hooks/useGetTableData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ExtendedTradesDataType, TRADE_STATUS } from "@jaws/db/tradesMeta";
import {
getAccountAssets,
getAccountCashBalance,
getAccountEquity,
getJawsPortfolio,
getMovingAverages,
} from "@jaws/services/backendService";
Expand Down Expand Up @@ -35,15 +36,17 @@ export const useGetTableData = () => {
investedValue: number;
marketValue: number;
totalPortfolioValue: number;
cashBalance: number;
}>({} as any);

useEffect(() => {
Promise.all([
getAccountAssets(),
getAccountCashBalance(),
getJawsPortfolio(),
getAccountEquity(),
])
.then(async ([assetsResult, balance, trades]) => {
.then(async ([assetsResult, cashBalance, trades, equity]) => {
const assets = assetsResult.assets;

const movingAverages = await getMovingAverages(
Expand All @@ -62,12 +65,16 @@ export const useGetTableData = () => {
}[];

const tableData = convertToTableData({
assets,
balance,
equity,
data: sortedData,
});

setData(tableData);
setData({
assets: tableData,
cashBalance,
totalPortfolioValue: equity,
...calculateAssetValues(assets),
});
setFetchStatus("ok");
})
.catch(console.error);
Expand All @@ -77,36 +84,16 @@ export const useGetTableData = () => {
};

function convertToTableData({
assets,
balance,
equity,
data,
}: {
assets: RawPosition[];
balance: number;
equity: number;
data: {
trade: ExtendedTradesDataType;
movingAvg: number;
alpacaAsset: RawPosition;
}[];
}): {
marketValue: number;
investedValue: number;
assets: PortfolioTableAsset[];
totalPortfolioValue: number;
} {
const investedValue = assets.reduce(
(sum: number, { cost_basis }) => sum + parseFloat(cost_basis),
0,
);

const marketValue: number = assets.reduce(
(sum: number, { market_value }) =>
sum + (market_value ? parseFloat(market_value) : 0),
0,
);

const totalPortfolioValue = balance + marketValue;

}): PortfolioTableAsset[] {
const extendedAssets: PortfolioTableAsset[] = data.map(
({ trade, alpacaAsset, movingAvg }) => {
tradeHasRequiredData(trade);
Expand All @@ -123,7 +110,7 @@ function convertToTableData({
trade,
currentPrice,
movingAvg,
totalAssets: totalPortfolioValue,
totalAssets: equity,
});

const stopLossType = [
Expand All @@ -134,8 +121,7 @@ function convertToTableData({

return {
...trade,
percentOfTotalAssets:
((avgEntryPrice * trade.quantity) / totalPortfolioValue) * 100,
percentOfTotalAssets: ((avgEntryPrice * trade.quantity) / equity) * 100,
changeSinceEntry: (currentPrice - avgEntryPrice) / avgEntryPrice,
value: trade.quantity * currentPrice,
currentPrice,
Expand All @@ -151,10 +137,23 @@ function convertToTableData({
},
);

return {
investedValue,
marketValue,
assets: extendedAssets,
totalPortfolioValue,
};
return extendedAssets;
}

function calculateAssetValues(assets: RawPosition[]): {
investedValue: number;
marketValue: number;
} {
const investedValue = assets.reduce(
(sum: number, { cost_basis }) => sum + parseFloat(cost_basis),
0,
);

const marketValue: number = assets.reduce(
(sum: number, { market_value }) =>
sum + (market_value ? parseFloat(market_value) : 0),
0,
);

return { investedValue, marketValue };
}
80 changes: 80 additions & 0 deletions src/lib/navHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { getDailyStats, upsertDailyStats } from "@jaws/db/dailyStatsEntity";
import { RawActivity } from "@jaws/services/alpacaMeta";
import * as alpacaService from "@jaws/services/alpacaService";
import { calculateNAV } from "@jaws/util/calculateNAV";
import { getDateString, getTodayWithDashes, ONE_DAY_IN_MS } from "./helpers";

export type DayDateString = ``;

export const calculateTodaysNAV = async (accountId: string) => {
const todayDate = getTodayWithDashes();
const yesterdaysDate = getDateString({
date: new Date(Number(new Date(todayDate)) - ONE_DAY_IN_MS),
withDashes: true,
});

const [equity, cashActivities, [{ debugInfo, ...yesterdayStats }]] =
await Promise.all([
alpacaService.getEquity(),
alpacaService.getAccountActivities({
activity_type: "TRANS",
date: todayDate,
}),
getDailyStats({
startDate: yesterdaysDate,
endDate: yesterdaysDate,
accountId,
}),
]);

isNonTradeActivities(cashActivities);

const netDeposits = cashActivities
.filter((ct) => ct.status !== "canceled")
.reduce((sum, ct) => sum + parseFloat(ct.net_amount), 0);

const NAV = calculateNAV({
numShares: yesterdayStats.shares,
equity: parseFloat(equity),
netDeposits,
});

return {
equity,
cashActivities,
netDeposits,
yesterdayStats,
...NAV,
todayDate,
yesterdaysDate,
};
};

export const saveTodaysNAV = async () => {
// TODO: Do for all accounts
const {
NAV: nav,
todayDate: date,
newNumShares: shares,
...debugInfo
} = await calculateTodaysNAV("hejare");

await upsertDailyStats({
nav,
accountId: "hejare",
date,
shares,
/** TODO: Remove when not needed anymore :) */
debugInfo,
});

return { nav, date, shares, ...debugInfo };
};

function isNonTradeActivities(
activities: RawActivity[],
): asserts activities is (RawActivity & { net_amount: string })[] {
if (!activities.every((a) => "net_amount" in a)) {
throw new TypeError("Found activity without net_amount");
}
}
2 changes: 1 addition & 1 deletion src/lib/tradesHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export const triggerStopLossTakeProfit = async () => {

const [newFilledTrades, balance] = await Promise.all([
populateTradesData(filledTrades),
alpacaService.getPortfolioValue(),
alpacaService.getEquity(),
]);

return performActions(newFilledTrades, parseFloat(balance));
Expand Down
Loading

0 comments on commit 98188b3

Please sign in to comment.