Skip to content

Commit

Permalink
Added bitcoin price chart to fear greed index chart
Browse files Browse the repository at this point in the history
  • Loading branch information
zourdyzou committed Oct 18, 2022
1 parent 0241208 commit 88c6768
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 23 deletions.
8 changes: 4 additions & 4 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import rateLimit from "axios-rate-limit";

export const http = rateLimit(axios.create(), { maxRequests: 1, perMilliseconds: 1500 });

type ApiConfigFunction = (server: "coinGecko" | "etherscan" | "alternative.me" | "senticrypt") => AxiosRequestConfig;
type ApiConfigFunction = (server: "coinGecko" | "etherscan" | "alternative.me" | "blockchain") => AxiosRequestConfig;

export const API_CONFIG: ApiConfigFunction = (server: "coinGecko" | "etherscan" | "alternative.me" | "senticrypt") => {
export const API_CONFIG: ApiConfigFunction = (server: "coinGecko" | "etherscan" | "alternative.me" | "blockchain") => {
switch (server) {
case "coinGecko":
return {
Expand Down Expand Up @@ -33,9 +33,9 @@ export const API_CONFIG: ApiConfigFunction = (server: "coinGecko" | "etherscan"
responseType: "json",
method: "GET",
};
case "senticrypt":
case "blockchain":
return {
baseURL: "http://api.senticrypt.com/v1",
baseURL: "https://api.blockchain.info",
responseType: "json",
method: "GET",
headers: {
Expand Down
8 changes: 5 additions & 3 deletions src/common/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { AvailableDayRanges } from "@/src/models";
import { AvailableDayRanges, AvailableIntervals } from "@/src/models";

const ETHERSCAN_API_KEY = process.env.REACT_APP_ETHERSCAN_API_KEY;

export const coinGecko = {
coins: `/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&sparkline=false`,
coinMarketChart: (coinId: string, days: AvailableDayRanges) =>
`/coins/${coinId}/market_chart?vs_currency=usd&days=${days}&interval=${days <= 30 ? "hourly" : "daily"}`,
coinMarketChart: (coinId: string, days: AvailableDayRanges, interval: AvailableIntervals) =>
`/coins/${coinId}/market_chart?vs_currency=usd&days=${days}&interval=${interval}`,
trending: `/search/trending`,
global: `/global`,
};
Expand All @@ -17,3 +17,5 @@ export const etherscan = {
export const alternativeMe = {
fearGreedIndex: (days: AvailableDayRanges) => `/fng/?limit=${days}`,
};

export const blockchain = {};
43 changes: 43 additions & 0 deletions src/components/screens/atoms/show-bitcoin-correlation-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import { Switch, Tooltip } from "@material-ui/core";
import { makeStyles, Theme, withStyles } from "@material-ui/core/styles";

import { bitcoinOrange } from "@/components/screens/molecules/history-fear-greed-chart";
import { selectFearGreedIndex, setShowBitcoinCorrelation } from "@/features/fear-greed-index-slice";
import { useAppDispatch, useAppSelector } from "@/hooks/*";

const useStyles = makeStyles((theme: Theme) => ({
bitcoinCorrelationSwitch: {
margin: "12px 12px 0 0",
},
}));

const CustomSwitch = withStyles({
switchBase: {
"&$checked": {
color: bitcoinOrange,
},
"&$checked + $track": {
backgroundColor: bitcoinOrange,
},
},
checked: {},
track: {},
})(Switch);

export const ShowBitcoinCorrelationSwitch: React.FC = () => {
const classes = useStyles();
const dispatch = useAppDispatch();

const fearGreedIndex = useAppSelector(selectFearGreedIndex);

return (
<Tooltip title={`${fearGreedIndex.showBitcoinCorrelation ? "Hide" : "Show"} Bitcoin Price`}>
<CustomSwitch
className={classes.bitcoinCorrelationSwitch}
checked={fearGreedIndex.showBitcoinCorrelation}
onChange={() => dispatch(setShowBitcoinCorrelation(!fearGreedIndex.showBitcoinCorrelation))}
/>
</Tooltip>
);
};
53 changes: 46 additions & 7 deletions src/components/screens/molecules/history-fear-greed-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { makeStyles, Theme } from "@material-ui/core/styles";
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";

import { convertTimestamp } from "@/common/helpers/date-handler";
import { roundDecimals } from "@/common/helpers/round-decimals";
import { ChartSkeleton } from "@/components/screens/atoms/chart-skeleton-loader";
import { selectFearGreedIndex } from "@/features/fear-greed-index-slice";
import { useAppSelector } from "@/hooks/*";
import { FearGreedIndex } from "@/src/models";

export const bitcoinOrange = "#f2a900";

const useStyles = makeStyles((theme: Theme) => ({
responsiveContainer: {
"& .recharts-surface": {
Expand All @@ -19,8 +22,8 @@ const useStyles = makeStyles((theme: Theme) => ({
borderRadius: 12,
padding: 12,
backgroundColor: `${theme.palette.background.default}dd`,
"& .MuiTypography-root:not(:first-child)": {
color: theme.palette.secondary.main,
"& #bitcoin-tooltip": {
color: bitcoinOrange,
},
},
}));
Expand All @@ -29,6 +32,7 @@ interface DataFormat {
date: number;
value: number;
classification: string;
bitcoinPrice: number;
}

export const HistoricFearGreedIndexChart: React.FunctionComponent = () => {
Expand All @@ -44,6 +48,7 @@ export const HistoricFearGreedIndexChart: React.FunctionComponent = () => {
date: Number(indexData.timestamp),
value: Number(indexData.value),
classification: indexData.valueClassification,
bitcoinPrice: indexData.bitcoinPrice,
});
});
return chartData;
Expand All @@ -57,22 +62,41 @@ export const HistoricFearGreedIndexChart: React.FunctionComponent = () => {
<ResponsiveContainer height="100%" width="100%" className={classes.responsiveContainer}>
<AreaChart data={formatRawData()} margin={{ top: 8, right: 8, left: 8, bottom: 8 }}>
<defs>
<linearGradient id="chart-gradient" x1="0" y1="0" x2="0" y2="1">
<linearGradient id="bitcoin-chart-gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={bitcoinOrange} stopOpacity={0.2} />
<stop offset="80%" stopColor={bitcoinOrange} stopOpacity={0} />
</linearGradient>
<linearGradient id="fear-greed-chart-gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={theme.palette.primary.main} stopOpacity={0.3} />
<stop offset="100%" stopColor={theme.palette.primary.main} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" hide />
<YAxis domain={[(dataMin: number) => dataMin * 0.95, (dataMax: number) => dataMax * 1.05]} hide />
<YAxis
yAxisId="left"
domain={[(dataMin: number) => dataMin * 0.95, (dataMax: number) => dataMax * 1.05]}
hide
/>
<YAxis
yAxisId="right"
orientation="right"
domain={[(dataMin: number) => dataMin * 0.95, (dataMax: number) => dataMax * 1.05]}
hide
/>
<Tooltip
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<Box className={classes.customTooltip}>
<Typography variant="body1">{convertTimestamp(label * 1000, true)}</Typography>
<Typography variant="body2">
{payload[0].payload.classification} ({payload[0].value}%)
<Typography variant="body2" color="primary">
{payload[0].payload.classification} ({payload[0].payload.value}%)
</Typography>
{fearGreedIndex.showBitcoinCorrelation && (
<Typography variant="body2" id="bitcoin-tooltip">
Bitcoin Price: US${roundDecimals(payload[0].payload.bitcoinPrice, 0)}
</Typography>
)}
</Box>
);
} else {
Expand All @@ -84,12 +108,27 @@ export const HistoricFearGreedIndexChart: React.FunctionComponent = () => {
type="monotone"
dataKey="value"
dot={false}
yAxisId="left"
animationDuration={3000}
strokeWidth={2}
stroke={theme.palette.primary.main}
fillOpacity={1}
fill={`url(#chart-gradient)`}
fill={`url(#fear-greed-chart-gradient)`}
/>
{fearGreedIndex.showBitcoinCorrelation && (
<Area
type="monotone"
dataKey="bitcoinPrice"
yAxisId="right"
dot={false}
animationDuration={3000}
strokeWidth={2}
strokeOpacity={0.3}
stroke={bitcoinOrange}
fillOpacity={0.5}
fill={`url(#bitcoin-chart-gradient)`}
/>
)}
</AreaChart>
</ResponsiveContainer>
)}
Expand Down
2 changes: 2 additions & 0 deletions src/components/screens/organisms/fear-greed-index-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Skeleton } from "@material-ui/lab";

import { HelpIconHeader } from "@/components/screens/atoms/HelpIconHeader";
import { MappedSentimentIcon } from "@/components/screens/atoms/mapped-sentiment-icon";
import { ShowBitcoinCorrelationSwitch } from "@/components/screens/atoms/show-bitcoin-correlation-switch";
import { CardLayout } from "@/components/screens/molecules/card-layout";
import { FearGreedIndexGaugeChart } from "@/components/screens/molecules/fear-greed-index-gaugeChart";
import { HistoricFearGreedIndexChart } from "@/components/screens/molecules/history-fear-greed-chart";
Expand Down Expand Up @@ -82,6 +83,7 @@ export const FearGreedIndexCard: React.FunctionComponent = () => {
{fearGreedIndex.value.length === 0 ? <FaceRounded /> : <MappedSentimentIcon />}
</Avatar>
}
action={<ShowBitcoinCorrelationSwitch />}
/>
<CardContent className={classes.contentWrapper}>
<FearGreedIndexGaugeChart />
Expand Down
2 changes: 1 addition & 1 deletion src/features/dominance-chart-list-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const fetchDominanceChartList = createAsyncThunk("dominanceChartList", as
for (let i = 0; i < coinIdList.length; i++) {
const response = await http.request({
...config("coinGecko"),
url: API.coinMarketChart(coinIdList[i], 30),
url: API.coinMarketChart(coinIdList[i], 30, "hourly"),
cancelToken: canceler.token,
});

Expand Down
46 changes: 39 additions & 7 deletions src/features/fear-greed-index-slice.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
import { createAsyncThunk, createSlice, Slice } from "@reduxjs/toolkit";
import { createAsyncThunk, createSlice, PayloadAction, Slice, SliceCaseReducers } from "@reduxjs/toolkit";
import axios from "axios";

import { API_CONFIG as config } from "@/common/constants";
import { alternativeMe as API } from "@/common/endpoints";
import { API_CONFIG as config, http } from "@/common/constants";
import { alternativeMe as API, coinGecko as coinAPI } from "@/common/endpoints";
import { toCamelCase } from "@/common/helpers/case-transformer";
import { RootState } from "@/components/app/store";
import { FearGreedIndex, FearGreedIndexRootObject, FearGreedIndexState } from "@/src/models";
import {
AvailableDayRanges,
CoinMarketChart,
FearGreedIndex,
FearGreedIndexRootObject,
FearGreedIndexState,
} from "@/src/models";

interface Reducers extends SliceCaseReducers<FearGreedIndexState> {
setShowBitcoinCorrelation: (state: FearGreedIndexState, action: PayloadAction<boolean>) => void;
}

const initialState: FearGreedIndexState = {
value: [],
today: null,
status: "IDLE",
showBitcoinCorrelation: false,
};

export const fetchFearGreedIndex = createAsyncThunk("fearGreedIndex", async () => {
const canceler = axios.CancelToken.source();

const dayRange: AvailableDayRanges = 30;

const response = await axios.request({
...config("alternative.me"),
url: API.fearGreedIndex(30),
url: API.fearGreedIndex(dayRange),
cancelToken: canceler.token,
});

const bitcoinMarketChart = await http.request({
...config("coinGecko"),
url: coinAPI.coinMarketChart("bitcoin", dayRange, "daily"),
cancelToken: canceler.token,
});

const normalizedResponse = toCamelCase(response.data) as FearGreedIndexRootObject;
// Sort here since endpoint returns data in date descending order, which does NOT work for the charts
normalizedResponse.data.sort((a: FearGreedIndex, b: FearGreedIndex) => Number(a.timestamp) - Number(b.timestamp));

const normalizedBitcoinMarketChart = toCamelCase(bitcoinMarketChart.data) as CoinMarketChart;
normalizedResponse.data.forEach((indexData: FearGreedIndex, index: number) => {
indexData.bitcoinPrice = normalizedBitcoinMarketChart.prices[index][1];
});

return normalizedResponse.data as FearGreedIndex[];
});

const fearGreedIndexSlice: Slice<FearGreedIndexState, {}, "fearGreedIndex"> = createSlice({
const fearGreedIndexSlice: Slice<FearGreedIndexState, Reducers, "fearGreedIndex"> = createSlice({
name: "fearGreedIndex",
initialState,
reducers: {},
reducers: {
setShowBitcoinCorrelation: (state: FearGreedIndexState, action: PayloadAction<boolean>) => {
state.showBitcoinCorrelation = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchFearGreedIndex.pending, (state) => {
Expand All @@ -51,4 +81,6 @@ const fearGreedIndexSlice: Slice<FearGreedIndexState, {}, "fearGreedIndex"> = cr
export const selectFearGreedIndex: (state: RootState) => FearGreedIndexState = (state: RootState) =>
state.fearGreedIndex;

export const { setShowBitcoinCorrelation } = fearGreedIndexSlice.actions;

export default fearGreedIndexSlice.reducer;
2 changes: 1 addition & 1 deletion src/features/market-chart-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const fetchCoinMarketChartList = createAsyncThunk(
for (let i = 0; i < params.coinIdList.length; i++) {
const response = await http.request({
...config("coinGecko"),
url: API.coinMarketChart(params.coinIdList[i], params.dayRange),
url: API.coinMarketChart(params.coinIdList[i], params.dayRange, params.dayRange <= 30 ? "hourly" : "daily"),
cancelToken: canceler.token,
});

Expand Down
1 change: 1 addition & 0 deletions src/models/api/coin-market-chart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GenericState } from "@/src/models";

export type AvailableDayRanges = 1 | 7 | 30 | 90 | 365 | 730 | "max";
export type AvailableIntervals = "minutely" | "hourly" | "daily";

export interface CoinMarketChart {
marketCaps: [number, number][];
Expand Down
2 changes: 2 additions & 0 deletions src/models/api/fear-greed-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface FearGreedIndex {
valueClassification: string;
timestamp: string;
timeUntilUpdate: string;
bitcoinPrice: number;
}

export interface FearGreedIndexMetadata {
Expand All @@ -19,4 +20,5 @@ export interface FearGreedIndexRootObject {

export interface FearGreedIndexState extends GenericState<FearGreedIndex[]> {
today: FearGreedIndex | null;
showBitcoinCorrelation: boolean;
}
2 changes: 2 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AvailableDayRanges as AvailableDayRangesInterface,
AvailableIntervals as AvailableIntervalsInterface,
CoinMarketChart as CoinMarketChartInterface,
CoinMarketChartList as CoinMarketChartListInterface,
CoinMarketChartListState as CoinMarketChartListStateInterface,
Expand Down Expand Up @@ -31,6 +32,7 @@ import { Page as PageInterface, RootModule as RootModuleInterface } from "./comm

export type AppState = AppStateInterface;
export type AvailableDayRanges = AvailableDayRangesInterface;
export type AvailableIntervals = AvailableIntervalsInterface;
export type DominanceChartList = DominanceChartListInterface;
export type GenericState<T> = GenericStateInterface<T>;
export type Coin = CoinInterface;
Expand Down

0 comments on commit 88c6768

Please sign in to comment.