feat: add stock screener
This commit is contained in:
@@ -1,43 +1,116 @@
|
||||
import { MIN_STOCK_SEARCH_LENGTH, useStockSearch } from "@/entities/stock";
|
||||
import { useInstrumentSearch } from "@/features/instrument-search";
|
||||
import { useScreenSync } from "@/features/tbank-connection";
|
||||
import { apiErrorCode } from "@/shared/api";
|
||||
import { instrumentLogoUrl } from "@/shared/lib";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { apiClient } from "@/shared/api";
|
||||
|
||||
type Criteria = {
|
||||
countryOfRisk: string;
|
||||
currency: string;
|
||||
maxLot: string;
|
||||
query: string;
|
||||
realExchange: string;
|
||||
sector: string;
|
||||
shareType: string;
|
||||
};
|
||||
|
||||
type ScreenerPage = Awaited<ReturnType<typeof readPage>>;
|
||||
|
||||
type StocksPageVm = ReturnType<typeof useStocksPageVm>;
|
||||
|
||||
const emptyCriteria: Criteria = {
|
||||
countryOfRisk: "",
|
||||
currency: "",
|
||||
maxLot: "",
|
||||
query: "",
|
||||
realExchange: "",
|
||||
sector: "",
|
||||
shareType: "",
|
||||
};
|
||||
|
||||
async function createSearch(criteria: Criteria) {
|
||||
const { data } = await apiClient.POST("/api/v1/stocks/screener/search", {
|
||||
body: {
|
||||
...(criteria.countryOfRisk && { countryOfRisk: criteria.countryOfRisk }),
|
||||
...(criteria.currency && { currency: criteria.currency }),
|
||||
...(criteria.maxLot && { maxLot: Number(criteria.maxLot) }),
|
||||
...(criteria.query && { query: criteria.query }),
|
||||
...(criteria.realExchange && { realExchange: criteria.realExchange }),
|
||||
...(criteria.sector && { sector: criteria.sector }),
|
||||
...(criteria.shareType && { shareType: criteria.shareType }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) throw new Error("Stock screener search failed");
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function readPage(searchId: string, page: number) {
|
||||
const { data } = await apiClient.GET(
|
||||
"/api/v1/stocks/screener/search/{searchId}",
|
||||
{ params: { path: { searchId }, query: { page } } },
|
||||
);
|
||||
|
||||
if (!data) throw new Error("Stock screener page failed");
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function useStocksPageVm() {
|
||||
const search = useInstrumentSearch(MIN_STOCK_SEARCH_LENGTH);
|
||||
const { canSearch, debounced, isWaiting, query } = search;
|
||||
const result = useStockSearch(debounced);
|
||||
useScreenSync({ query: debounced, screen: "stocks" }, [result]);
|
||||
const [criteria, setCriteria] = useState(emptyCriteria);
|
||||
const [page, setPage] = useState<null | ScreenerPage>(null);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
const search = async () => {
|
||||
const currentRequestId = requestIdRef.current + 1;
|
||||
requestIdRef.current = currentRequestId;
|
||||
setPending(true);
|
||||
setError(false);
|
||||
|
||||
try {
|
||||
const result = await createSearch(criteria);
|
||||
if (requestIdRef.current === currentRequestId) setPage(result);
|
||||
}
|
||||
catch {
|
||||
if (requestIdRef.current === currentRequestId) setError(true);
|
||||
}
|
||||
finally {
|
||||
if (requestIdRef.current === currentRequestId) setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const changePage = async (nextPage: number) => {
|
||||
if (!page) return;
|
||||
|
||||
setPending(true);
|
||||
setError(false);
|
||||
|
||||
try {
|
||||
setPage(await readPage(page.searchId, nextPage));
|
||||
}
|
||||
catch {
|
||||
setError(true);
|
||||
}
|
||||
finally {
|
||||
setPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
canSearch,
|
||||
code: apiErrorCode(result.error),
|
||||
error:
|
||||
!result.data
|
||||
&& (result.isError || result.meta?.lastAttempt.state === "failed"),
|
||||
items:
|
||||
result.data?.map(stock => ({
|
||||
...stock,
|
||||
logoSrc: instrumentLogoUrl(stock.logoName),
|
||||
to: `/stocks/${encodeURIComponent(stock.slug)}`,
|
||||
})) ?? [],
|
||||
loading:
|
||||
canSearch
|
||||
&& (isWaiting
|
||||
|| (!result.data
|
||||
&& !result.isError
|
||||
&& result.meta?.lastAttempt.state !== "failed")),
|
||||
onQueryChange: search.onQueryChange,
|
||||
partial: result.meta?.completeness === "incomplete",
|
||||
query,
|
||||
ready:
|
||||
debounced.length >= MIN_STOCK_SEARCH_LENGTH
|
||||
&& !isWaiting
|
||||
&& Boolean(result.data),
|
||||
criteria,
|
||||
error,
|
||||
items: page?.items ?? [],
|
||||
loading: pending,
|
||||
onCriteriaChange: (name: keyof Criteria, value: string) =>
|
||||
setCriteria(current => ({ ...current, [name]: value })),
|
||||
onPage: (nextPage: number) => void changePage(nextPage),
|
||||
onSearch: () => void search(),
|
||||
page: page?.page,
|
||||
resultTime: page?.asOf,
|
||||
searched: page !== null,
|
||||
totalPages: page ? page.page + Number(page.hasNext) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export { type StocksPageVm, useStocksPageVm };
|
||||
export { type Criteria, type StocksPageVm, useStocksPageVm };
|
||||
|
||||
Reference in New Issue
Block a user