Skip to content

Commit

Permalink
Use nextjs actions for price fetching (#907)
Browse files Browse the repository at this point in the history
  • Loading branch information
argaen authored May 24, 2024
1 parent 269c935 commit c2233fa
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 24 deletions.
4 changes: 2 additions & 2 deletions jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const config = {
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
coverageThreshold: {
global: {
lines: 92.5,
branches: 85.5,
lines: 91,
branches: 85.2,
},
},
coverageReporters: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@tanstack/react-table": "^8.17.3",
"@testing-library/jest-dom": "^6.4.5",
"aws-amplify": "^5.3.12",
"axios": "^1.7.2",
"chart.js": "^4.4.3",
"chartjs-adapter-luxon": "^1.3.1",
"chartjs-chart-sankey": "^0.12.1",
Expand Down
5 changes: 0 additions & 5 deletions src/__tests__/app/user/login/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ jest.mock('@auth0/auth0-react', () => ({
...jest.requireActual('@auth0/auth0-react'),
}));

jest.mock('@/lib/Stocker', () => ({
__esModule: true,
...jest.requireActual('@/lib/Stocker'),
}));

jest.mock('@/helpers/errors', () => ({
__esModule: true,
...jest.requireActual('@/helpers/errors'),
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/hooks/useDataSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ jest.mock('@/lib/queries', () => ({
...jest.requireActual('@/lib/queries'),
}));

jest.mock('@/lib/Stocker', () => ({
jest.mock('@/lib/prices', () => ({
__esModule: true,
...jest.requireActual('@/lib/Stocker'),
...jest.requireActual('@/lib/prices'),
insertTodayPrices: jest.fn(),
}));

Expand Down
11 changes: 11 additions & 0 deletions src/app/actions/getTodayPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use server';

import getPrices from '@/lib/yahoo';
import type { Price as PriceInfo } from '@/lib/yahoo';

export default async function getTodayPrices(
tickers: string[],
): Promise<{ [ticker: string]: PriceInfo }> {
const result = await getPrices(tickers);
return result;
}
2 changes: 1 addition & 1 deletion src/app/dashboard/commodities/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useCommodities } from '@/hooks/api';
import Loading from '@/components/Loading';
import FormButton from '@/components/buttons/FormButton';
import CommodityForm from '@/components/forms/commodity/CommodityForm';
import { insertTodayPrices } from '@/lib/Stocker';
import { insertTodayPrices } from '@/lib/prices';
import CommodityCard from '@/components/CommodityCard';

export default function CommoditiesPage(): JSX.Element {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDataSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import initSqlJs from 'sql.js';
import { Settings } from 'luxon';
import pako from 'pako';

import { insertTodayPrices } from '@/lib/Stocker';
import { insertTodayPrices } from '@/lib/prices';
import useBookStorage from '@/hooks/useBookStorage';
import { migrate as migrateFromGnucash } from '@/lib/gnucash';
import {
Expand Down
15 changes: 2 additions & 13 deletions src/lib/Stocker.ts → src/lib/prices.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { Amplify, API } from 'aws-amplify';
import { DateTime } from 'luxon';

import { Commodity, Price } from '@/book/entities';
import { getMainCurrency } from '@/lib/queries';
import { toAmountWithScale } from '@/helpers/number';
import { IS_PAID_PLAN } from '@/helpers/env';

import awsExports from '../aws-exports';

Amplify.configure(awsExports);

const API_NAME = 'stocker';
import getTodayPrices from '@/app/actions/getTodayPrices';

/**
* Connect to Stocker API and retrieve current prices for
Expand Down Expand Up @@ -40,12 +34,7 @@ export async function insertTodayPrices(): Promise<void> {
...commodityTickers,
];

const options = {
queryStringParameters: {
ids: Array.from(new Set(tickers)).toString(),
},
};
const resp = await API.get(API_NAME, '/api/prices', options) as { [key: string]:LiveSummary; };
const resp = await getTodayPrices(Array.from(new Set(tickers)));

const now = DateTime.now().startOf('day');

Expand Down
151 changes: 151 additions & 0 deletions src/lib/yahoo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import axios, { AxiosError, AxiosResponse } from 'axios';
import { DateTime } from 'luxon';

export type Price = {
price: number,
currency: string,
changePct: number,
changeAbs: number,
};

export class HTTPError extends Error {
status: number;
code: string;

constructor(message: string, status: number, code: string) {
super(message);
this.message = message;
this.status = status;
this.code = code;
}
}
export class YahooError extends HTTPError {}

const HOST = 'https://query2.finance.yahoo.com';

export default async function getPrices(
tickers: string[],
): Promise<{ [ticker: string]: Price }> {
const result: { [ticker: string]: Price } = {};

async function callAndSave(ticker: string) {
try {
result[ticker] = await getPrice(ticker);
} catch (e) {
console.warn(`A price retrieval failed: ${e}`);
}
}

const promises: Promise<void>[] = [];

try {
tickers.forEach(ticker => promises.push(callAndSave(ticker)));
await Promise.all(promises);
} catch (error) {
return {};
}

console.log(result);
return result;
}

export async function getPrice(t: string, when?: number): Promise<Price> {
const { ticker, transform } = formatTicker(t);
let url = `${HOST}/v8/finance/chart/${ticker}?interval=1d&includePrePost=false`;

if (when) {
let date = DateTime.fromSeconds(when, { zone: 'utc' });
// Yahoo api returns an error when we query price for Sunday
if (date.weekday === 6) {
date = date.minus({ days: 1 });
}
const end = date.endOf('day');
const start = end.startOf('day');

url = `${url}&period1=${Math.floor(start.toSeconds())}&period2=${Math.floor(end.toSeconds())}`;
}

let resp: AxiosResponse;
try {
resp = await axios.get(url);
} catch (error: unknown) {
const e = error as AxiosError;
throw new YahooError(
`${ticker} failed: ${e.message}. url: ${url}`,
e.response?.status || 0,
'UNKNOWN',
);
}

const { result, error } = resp.data.chart;

if (error !== null) {
if (error.code === 'Not Found') {
throw new YahooError(
`ticker '${ticker}' not found`,
404,
'NOT_FOUND',
);
}

throw new YahooError(
`unknown error '${error.description}'`,
resp.status,
'UNKNOWN',
);
}

const { currency } = result[0].meta;
const price = transform(toStandardUnit(result[0].meta.regularMarketPrice, currency));
const previousClose = transform(
toStandardUnit(result[0].meta.chartPreviousClose, currency),
) || price;
const change = price - previousClose;

return {
price,
currency: toStandardCurrency(currency),
changePct: parseFloat(((change / previousClose) * 100).toFixed(2)),
changeAbs: parseFloat(change.toFixed(2)),
};
}

function formatTicker(ticker: string): { ticker: string, transform: Function } {
if (ticker === 'SGDCAD=X') {
return {
ticker: 'SGDCAX=X',
transform: (n: number) => n,
};
}

if (
/[EUR|USD|SGD]=X$/.test(ticker)
&& !(/^[EUR|USD|SGD]/.test(ticker))
) {
return {
ticker: `${ticker.slice(3, 6)}${ticker.slice(0, 3)}=X`,
transform: (n: number) => 1 / n,
};
}

return {
ticker,
transform: (n: number) => n,
};
}

function toStandardUnit(n: number, currency: string) {
if (currency === 'GBp') {
return n * 0.01;
}

return n;
}

function toStandardCurrency(currency: string) {
if (currency === 'GBp') {
return 'GBP';
}

return currency;
}

0 comments on commit c2233fa

Please sign in to comment.