Skip to content

Commit

Permalink
228a dashboard add market info graph and jaws performance stats (#237)
Browse files Browse the repository at this point in the history
* Add link to Dashboard in navbar

Closes #229

* Add StatsCompare component to dashboard

* Add endpoint to get historical bars for symbols

* StatsCompare: Add stats for tickers
  • Loading branch information
mold authored Feb 16, 2023
1 parent 98188b3 commit 0c148b4
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 43 deletions.
3 changes: 3 additions & 0 deletions src/components/organisms/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ const Navbar = () => {
<LogoContainer />
</Link>
<LinksContainer>
<Link href={`/`} passHref>
<NavBarItem active={pathName === "/"}>Dashboard</NavBarItem>
</Link>
<Link href={`/daily-runs/${today}`} passHref>
<NavBarItem active={pathName.startsWith("/daily-runs")}>
Todays run
Expand Down
125 changes: 125 additions & 0 deletions src/components/organisms/StatsCompare.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getDateString, ONE_DAY_IN_MS } from "@jaws/lib/helpers";
import { getDailyStats, getTickerBars } from "@jaws/services/backendService";
import { useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import PercentageDisplay from "../molecules/PercentageDisplay";

const Wrapper = styled.div<{ loading: boolean }>`
display: flex;
margin-bottom: 15px;
opacity: ${(props) => (props.loading ? 0.7 : 1)};
`;

const StatWrapper = styled.div`
display: flex;
gap: 5px;
padding: 0 10px;
border-right: 1px solid #888;
&:last-child {
border-right: none;
}
`;

export function StatsCompare() {
const [dateRange, setDateRange] = useState<{
startDate: string;
endDate: string;
}>();

const [isLoading, setIsLoading] = useState<boolean>(true);

const [stats, setStats] = useState<{ ticker: string; nav: number }[]>([]);

const ranges = useMemo(getRanges, []);

if (!dateRange) {
setDateRange(ranges[0][1]);
}

useEffect(() => {
if (!dateRange) {
return;
}

setIsLoading(true);

void Promise.all([
getDailyStats(dateRange),
getTickerBars({ symbols: ["SPY", "SDY", "IWM"], ...dateRange }),
]).then(([jawsStats, tickerStats]) => {
setIsLoading(false);
setStats([
{
ticker: "Jaws NAV",
nav:
(jawsStats[jawsStats.length - 1].nav / jawsStats[0].nav - 1) * 100,
},
...Object.entries(tickerStats).map(([ticker, bars]) => {
return {
ticker,
nav: (bars[bars.length - 1].c / bars[0].c - 1) * 100,
};
}),
]);
});
}, [dateRange]);

return (
<Wrapper loading={isLoading}>
<>
<label>
<select
disabled={isLoading}
onChange={(e: any) => {
setDateRange(ranges[e.target.value][1]);
}}
>
{ranges.map(([name], i) => (
<option value={i} key={name}>
{name}
</option>
))}
</select>
</label>
{stats.map((stat) => (
<StatWrapper key={stat.ticker}>
<span>{stat.ticker}</span>
<PercentageDisplay value={stat.nav} indicatorOrigin={0} />
</StatWrapper>
))}
</>
</Wrapper>
);
}

type RangeName =
| "Yesterday"
| "1 week"
| "2 weeks"
| "1 month"
| "2 months"
| "3 months";

type DateRanges = [RangeName, { startDate: string; endDate: string }][];

function getRanges(): DateRanges {
const yesterdayMs = Number(new Date()) - ONE_DAY_IN_MS;

const ranges = {
Yesterday: new Date(yesterdayMs - ONE_DAY_IN_MS),
"1 week": new Date(yesterdayMs - 7 * ONE_DAY_IN_MS),
"2 weeks": new Date(yesterdayMs - 2 * 7 * ONE_DAY_IN_MS),
"1 month": new Date(yesterdayMs - 30 * ONE_DAY_IN_MS),
"2 months": new Date(yesterdayMs - 60 * ONE_DAY_IN_MS),
"3 months": new Date(yesterdayMs - 90 * ONE_DAY_IN_MS),
};

return Object.entries(ranges).map(([name, date]) => [
name as RangeName,
{
startDate: getDateString({ date, withDashes: true }),
endDate: getDateString({ date: new Date(yesterdayMs), withDashes: true }),
},
]);
}
58 changes: 17 additions & 41 deletions src/pages/api/data/daily-stats/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getDailyStats } from "@jaws/db/dailyStatsEntity";
import { getTodayWithDashes } from "@jaws/lib/helpers";
import { NextApiRequest, NextApiResponse } from "next";
import { ResponseDataType } from "../../ResponseDataMeta";

Expand All @@ -9,14 +8,14 @@ interface DailyStatsResponseData {
}

export interface DailyStatsResponse extends ResponseDataType {
data?: DailyStatsResponseData | DailyStatsResponseData[];
data: DailyStatsResponseData[];
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const response: DailyStatsResponse = { status: "INIT" };
const response: DailyStatsResponse = { status: "INIT", data: [] };

// TODO: get from middleware
const accountId = "hejare";
Expand All @@ -27,19 +26,7 @@ export default async function handler(
}

const dates = getValidDateRange(req.query);

if (dates) {
response.data = await getStats({ ...dates, accountId });
} else {
response.data = await getStats(
{
startDate: getTodayWithDashes(),
endDate: getTodayWithDashes(),
accountId,
},
true,
);
}
response.data = await getStats({ ...dates, accountId });

response.status = "OK";
return res.status(200).json(response);
Expand All @@ -50,38 +37,27 @@ export default async function handler(
}
}

async function getStats(
params: {
startDate: string;
endDate: string;
accountId: string;
},
justOne?: boolean,
) {
async function getStats(params: {
startDate: string;
endDate: string;
accountId: string;
}) {
const stats = (await getDailyStats(params)).map(({ nav, date }) => ({
nav,
date,
}));
return justOne ? stats[0] : stats;
return stats;
}

function getValidDateRange(dates: {
startDate?: string;
endDate?: string;
}): { startDate: string; endDate: string } | undefined {
function getValidDateRange(dates: { startDate?: string; endDate?: string }): {
startDate: string;
endDate: string;
} {
if (
typeof dates.startDate === "string" ||
typeof dates.endDate === "string"
!(typeof dates.startDate === "string" && typeof dates.endDate === "string")
) {
if (
typeof dates.startDate !== "string" ||
typeof dates.endDate !== "string"
) {
throw new Error("Need two dates for date range");
} else {
return { ...dates } as { startDate: string; endDate: string };
}
throw new Error("Need two dates for date range");
} else {
return { ...dates } as { startDate: string; endDate: string };
}

return;
}
50 changes: 50 additions & 0 deletions src/pages/api/market/bars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getTickerBars } from "@jaws/services/alpacaService";
import { RawBar } from "@master-chief/alpaca/@types/entities";
import { NextApiRequest, NextApiResponse } from "next";
import { ResponseDataType } from "../ResponseDataMeta";

export type BarsResponse = ResponseDataType & {
bars: { [k: string]: RawBar[] };
};

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
const dates = getValidDateRange(req.query);
const symbols = req.query.symbols as string[];

const bars = await getTickerBars(symbols, dates);

const response: BarsResponse = {
...bars,
status: "OK",
};

res.status(200).json(response);
} catch (e: any) {
const response: ResponseDataType = {
status: "NOK",
message: e?.message || e,
};
res.status(500).json(response);
}
}

function getValidDateRange({
startDate,
endDate,
}: {
startDate?: string;
endDate?: string;
}): {
startDate: string;
endDate: string;
} {
if (!(typeof startDate === "string" && typeof endDate === "string")) {
throw new Error("Need two dates for date range");
} else {
return { startDate, endDate } as { startDate: string; endDate: string };
}
}
6 changes: 4 additions & 2 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useState } from "react";
import styled from "styled-components";
import PageContainer from "@jaws/components/atoms/PageContainer";
import Widget from "@jaws/components/atoms/Widget";
import LatestOrders from "@jaws/components/molecules/LatestOrders";
import SummedPositions from "@jaws/components/molecules/SummedPositions";
import TriggerDailyRunButton from "@jaws/components/molecules/TriggerDailyRunButton";
import WalletBalance from "@jaws/components/molecules/WalletBalance";
import { StatsCompare } from "@jaws/components/organisms/StatsCompare";
import WidgetGrid from "@jaws/components/organisms/WidgetGrid";
import { getServerSidePropsAllPages } from "@jaws/lib/getServerSidePropsAllPages";
import { useState } from "react";
import styled from "styled-components";

const ContentContainer = styled.div`
display: flex;
Expand All @@ -22,6 +23,7 @@ function StartPage() {

return (
<PageContainer>
<StatsCompare />
<ContentContainer>
<WidgetGrid>
<Widget>
Expand Down
35 changes: 35 additions & 0 deletions src/services/alpacaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GetPortfolioHistory } from "@master-chief/alpaca";
import {
PortfolioHistory,
RawAccount,
RawBar,
RawOrder,
RawPosition,
} from "@master-chief/alpaca/@types/entities";
Expand All @@ -19,6 +20,8 @@ import { RawActivity, Side } from "./alpacaMeta";
const {
ALPACA_API_KEY_ID = "[NOT_DEFINED_IN_ENV]",
ALPACA_API_KEY_VALUE = "[NOT_DEFINED_IN_ENV]",
ALPACA_MARKET_API_KEY = "[NOT_DEFINED_IN_ENV]",
ALPACA_MARKET_API_KEY_SECRET = "[NOT_DEFINED_IN_ENV]",
} = process.env;

const buff = Buffer.from(
Expand All @@ -27,8 +30,14 @@ const buff = Buffer.from(
);
const base64EncodedKeys = buff.toString("base64");

const base64EncodedMarketKeys = Buffer.from(
`${ALPACA_MARKET_API_KEY}:${ALPACA_MARKET_API_KEY_SECRET}`,
"utf-8",
).toString("base64");

const accountId = "b75acdbc-3fb6-3fb3-b253-b0bf7d86b8bb"; // public info
const brokerApiBaseUrl = "https://broker-api.sandbox.alpaca.markets/v1";
const marketDataApiBaseUrl = "https://data.alpaca.markets/v2";

const getHoldingInTicker = async (ticker: string) => {
const assetInfo = await getAssetByTicker(ticker);
Expand Down Expand Up @@ -307,6 +316,32 @@ export async function getAccountActivities({
);
}

export async function getTickerBars(
symbols: string[],
dates: { startDate: string; endDate: string },
) {
const params = new URLSearchParams({
timeframe: "23Hour",
start: dates.startDate,
end: dates.endDate,
});

const res = await fetch(
`${marketDataApiBaseUrl}/stocks/bars?${params.toString()}&symbols=${symbols.join(
",",
)}`,
{
headers: {
"APCA-API-KEY-ID": ALPACA_MARKET_API_KEY,
"APCA-API-SECRET-KEY": ALPACA_MARKET_API_KEY_SECRET,
content: "application/json",
},
},
);

return handleResult<{ bars: { [k: string]: RawBar[] } }>(res);
}

async function sendAlpacaRequest<T = any>(path: string, options?: RequestInit) {
const res = await fetch(`${brokerApiBaseUrl}/${path}`, {
...options,
Expand Down
Loading

0 comments on commit 0c148b4

Please sign in to comment.