add retail volume component
This commit is contained in:
parent
6db97c89a0
commit
53fcd7990b
315
src/lib/components/RetailVolume.svelte
Normal file
315
src/lib/components/RetailVolume.svelte
Normal file
@ -0,0 +1,315 @@
|
||||
|
||||
<script lang ='ts'>
|
||||
import { retailVolumeComponent,displayCompanyName, stockTicker, assetType, etfTicker, screenWidth, userRegion, getCache, setCache} from '$lib/store';
|
||||
import InfoModal from '$lib/components/InfoModal.svelte';
|
||||
import { Chart } from 'svelte-echarts'
|
||||
import { abbreviateNumber } from "$lib/utils";
|
||||
|
||||
import Lazy from 'svelte-lazy';
|
||||
let isLoaded = false;
|
||||
const usRegion = ['cle1','iad1','pdx1','sfo1'];
|
||||
|
||||
let apiURL;
|
||||
|
||||
userRegion.subscribe(value => {
|
||||
|
||||
if (usRegion.includes(value)) {
|
||||
apiURL = import.meta.env.VITE_USEAST_API_URL;
|
||||
} else {
|
||||
apiURL = import.meta.env.VITE_EU_API_URL;
|
||||
}
|
||||
});
|
||||
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
let rawData = [];
|
||||
let optionsData;
|
||||
let avgVolume;
|
||||
let avgSentiment;
|
||||
|
||||
function normalizer(value) {
|
||||
if (Math?.abs(value) >= 1e18) {
|
||||
return { unit: 'Q', denominator: 1e18 };
|
||||
} else if (Math?.abs(value) >= 1e12) {
|
||||
return { unit: 'T', denominator: 1e12 };
|
||||
} else if (Math?.abs(value) >= 1e9) {
|
||||
return { unit: 'B', denominator: 1e9 };
|
||||
} else if (Math?.abs(value) >= 1e6) {
|
||||
return { unit: 'M', denominator: 1e6 };
|
||||
} else if (Math?.abs(value) >= 1e5) {
|
||||
return { unit: 'K', denominator: 1e5 };
|
||||
} else {
|
||||
return { unit: '', denominator: 1 };
|
||||
}
|
||||
}
|
||||
function getPlotOptions() {
|
||||
let dates = [];
|
||||
let tradingList = [];
|
||||
let sentimentList = [];
|
||||
// Iterate over the data and extract required information
|
||||
rawData?.forEach(item => {
|
||||
|
||||
dates?.push(item?.date);
|
||||
tradingList?.push(item?.traded);
|
||||
sentimentList?.push(item?.sentiment)
|
||||
|
||||
});
|
||||
|
||||
// Compute the average of item?.traded
|
||||
const totalTraded = tradingList?.reduce((acc, traded) => acc + traded, 0);
|
||||
avgVolume = totalTraded / tradingList?.length;
|
||||
|
||||
const totalSentiment = sentimentList?.reduce((acc, sentiment) => acc + sentiment, 0);
|
||||
avgSentiment = totalSentiment / tradingList?.length > 1 ? 'Bullish' : 'Bearish';
|
||||
|
||||
const {unit, denominator } = normalizer(Math.max(...tradingList) ?? 0)
|
||||
|
||||
|
||||
const option = {
|
||||
silent: true,
|
||||
animation: $screenWidth < 640 ? false: true,
|
||||
grid: {
|
||||
left: $screenWidth < 640 ? '0%' : '2%',
|
||||
right: $screenWidth < 640 ? '5%' : '2%',
|
||||
bottom: $screenWidth < 640 ? '0%' : '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
// Assuming dates are in the format 'yyyy-mm-dd'
|
||||
// Extract the month and day from the date string and convert the month to its abbreviated name
|
||||
const dateParts = value.split('-');
|
||||
const day = dateParts[2].substring(0); // Extracting the last two digits of the year
|
||||
const monthIndex = parseInt(dateParts[1]) - 1; // Months are zero-indexed in JavaScript Date objects
|
||||
return `${day} ${monthNames[monthIndex]}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false, // Disable x-axis grid lines
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#6E7079', // Change label color to white
|
||||
formatter: function (value) {
|
||||
value = Math.max(value, 0);
|
||||
return '$'+(value / denominator)?.toFixed(1) + unit; // Format value in millions
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
show: false,
|
||||
},
|
||||
|
||||
],
|
||||
series: [
|
||||
{
|
||||
data: tradingList,
|
||||
type: 'line',
|
||||
itemStyle: {
|
||||
color: '#fff' // Change bar color to white
|
||||
},
|
||||
showSymbol: false
|
||||
},
|
||||
{
|
||||
name: 'Sentiment',
|
||||
data: sentimentList,
|
||||
type: 'bar',
|
||||
smooth: true,
|
||||
yAxisIndex: 1,
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
// Set color based on positive or negative value
|
||||
return params.data >= 0 ? '#10DB06' : '#FF2F1F';
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
const getRetailVolume = async (ticker) => {
|
||||
// Get cached data for the specific tickerID
|
||||
const cachedData = getCache(ticker, 'getRetailVolume');
|
||||
if (cachedData) {
|
||||
rawData = cachedData;
|
||||
} else {
|
||||
|
||||
const postData = {'ticker': ticker};
|
||||
// make the POST request to the endpoint
|
||||
const response = await fetch(apiURL + '/retail-volume', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
|
||||
rawData = await response.json();
|
||||
|
||||
// Cache the data for this specific tickerID with a specific name 'getRetailVolume'
|
||||
setCache(ticker, rawData, 'getRetailVolume');
|
||||
}
|
||||
if(rawData?.length !== 0) {
|
||||
$retailVolumeComponent = true;
|
||||
} else {
|
||||
$retailVolumeComponent = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$: {
|
||||
if($assetType === 'stock' ? $stockTicker :$etfTicker && typeof window !== 'undefined') {
|
||||
isLoaded=false;
|
||||
const ticker = $assetType === 'stock' ? $stockTicker :$etfTicker
|
||||
const asyncFunctions = [
|
||||
getRetailVolume(ticker)
|
||||
];
|
||||
Promise.all(asyncFunctions)
|
||||
.then((results) => {
|
||||
optionsData = getPlotOptions()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred:', error);
|
||||
});
|
||||
isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-white h-full pb-8">
|
||||
<main class="overflow-hidden ">
|
||||
|
||||
<div class="flex flex-row items-center">
|
||||
<label for="retailTraderTrackerInfo" class="mr-1 cursor-pointer flex flex-row items-center text-white text-xl sm:text-3xl font-bold">
|
||||
Retail Volume Tracker
|
||||
</label>
|
||||
<InfoModal
|
||||
title={"Retail Volume Tracker"}
|
||||
content={"Understand retail investor activity: The green bar shows the daily volume trend, indicating if it was more bullish (above the axis) or bearish (below the axis). The white line represents the daily volume of retail investors."}
|
||||
id={"retailTraderTrackerInfo"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if isLoaded}
|
||||
|
||||
{#if rawData?.length !== 0}
|
||||
<div class="p-3 sm:p-0 mt-2 pb-8 sm:pb-2 rounded-lg bg-[#202020] sm:bg-[#0F0F0F]">
|
||||
|
||||
<div class="w-full flex flex-col items-start">
|
||||
<div class="text-white text-sm sm:text-[1rem] mt-1 sm:mt-3 mb-1 w-full">
|
||||
In the past six months, the {$displayCompanyName} had an average retail volume of <span class="font-semibold">{abbreviateNumber(avgVolume,true)}</span>, with a prevailing
|
||||
{#if avgSentiment === 'Bullish' }
|
||||
<span class="text-[#10DB06]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="#10db06" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"><path d="m3 17l6-6l4 4l8-8"/><path d="M17 7h4v4"/></g></svg>
|
||||
{avgSentiment}
|
||||
</span>
|
||||
|
||||
{:else if avgSentiment === 'Bearish' }
|
||||
<span class="text-[#E57C34]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#ff2f1f" d="M244 136v64a12 12 0 0 1-12 12h-64a12 12 0 0 1 0-24h35l-67-67l-31.51 31.52a12 12 0 0 1-17 0l-72-72a12 12 0 0 1 17-17L96 127l31.51-31.52a12 12 0 0 1 17 0L220 171v-35a12 12 0 0 1 24 0Z"/></svg>
|
||||
{avgSentiment}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-[#FF2F1F]">
|
||||
<svg class="w-6 h-6 sm:w-7 sm:h-7 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#e57c34" d="m22 12l-4-4v3H3v2h15v3l4-4Z"/></svg>
|
||||
Neutral
|
||||
</span>
|
||||
{/if} trend.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Lazy height={300} fadeOption={{delay: 100, duration: 500}} keep={true}>
|
||||
<div class="app w-full h-[300px] ">
|
||||
<Chart options={optionsData} class="chart" />
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2 class="mt-10 mr-1 flex flex-row items-center text-white text-xl sm:text-2xl font-bold mb-3">
|
||||
Latest Information
|
||||
</h2>
|
||||
<div class="flex justify-start items-center w-full m-auto mt-6 ">
|
||||
<table class="w-full" data-test="statistics-table">
|
||||
<tbody>
|
||||
<tr class="border-y border-gray-800 odd:bg-[#202020]">
|
||||
<td class="px-[5px] py-1.5 xs:px-2.5 xs:py-2">
|
||||
<span>Date</span>
|
||||
</td>
|
||||
<td class="px-[5px] py-1.5 text-right font-medium xs:px-2.5 xs:py-2">
|
||||
{new Date(rawData?.slice(-1)?.at(0)?.date)?.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', daySuffix: '2-digit' })}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-y border-gray-800 odd:bg-[#202020]">
|
||||
<td class="px-[5px] py-1.5 xs:px-2.5 xs:py-2">
|
||||
<span>Volume</span>
|
||||
</td>
|
||||
<td class="px-[5px] py-1.5 text-right font-medium xs:px-2.5 xs:py-2">
|
||||
{abbreviateNumber(rawData?.slice(-1)?.at(0)?.traded,true)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-y border-gray-800 odd:bg-[#202020]">
|
||||
<td class="px-[5px] py-1.5 xs:px-2.5 xs:py-2">
|
||||
<span>Retail Sentiment</span>
|
||||
</td>
|
||||
<td class="px-[5px] py-1.5 text-right font-medium xs:px-2.5 xs:py-2 { rawData?.slice(-1)?.at(0)?.sentiment > 0 ? 'text-[#10DB06]' : 'text-[#E57C34]'} ">
|
||||
{rawData?.slice(-1)?.at(0)?.sentiment > 0 ? 'Bullish' : 'Bearish'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-80">
|
||||
<div class="relative">
|
||||
<label class="bg-[#202020] rounded-xl h-14 w-14 flex justify-center items-center absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.app {
|
||||
height: 300px;
|
||||
max-width: 100%; /* Ensure chart width doesn't exceed the container */
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
height: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -63,6 +63,7 @@ export const priceAnalysisComponent = writable(<boolean>(false));
|
||||
export const revenueSegmentationComponent = writable(<boolean>(false));
|
||||
export const trendAnalysisComponent = writable(<boolean>(false));
|
||||
export const shareholderComponent = writable(<boolean>(false));
|
||||
export const retailVolumeComponent = writable(<boolean>(false));
|
||||
|
||||
|
||||
export const strategyId = writable(<string> (""));
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import {AreaSeries, Chart, PriceLine, CandlestickSeries} from 'svelte-lightweight-charts';
|
||||
|
||||
import { TrackingModeExitMode } from 'lightweight-charts';
|
||||
import {getCache, setCache, trendAnalysisComponent, priceAnalysisComponent, assetType, screenWidth, globalForm, userRegion, numberOfUnreadNotification, displayCompanyName, isCrosshairMoveActive, realtimePrice, priceIncrease, currentPortfolioPrice, currentPrice, clientSideCache, etfTicker, isOpen, isBeforeMarketOpen, isWeekend} from '$lib/store';
|
||||
import {getCache, setCache, retailVolumeComponent, trendAnalysisComponent, priceAnalysisComponent, assetType, screenWidth, globalForm, userRegion, numberOfUnreadNotification, displayCompanyName, isCrosshairMoveActive, realtimePrice, priceIncrease, currentPortfolioPrice, currentPrice, clientSideCache, etfTicker, isOpen, isBeforeMarketOpen, isWeekend} from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import ETFKeyInformation from '$lib/components/ETFKeyInformation.svelte';
|
||||
import Lazy from '$lib/components/Lazy.svelte';
|
||||
@ -1334,6 +1334,14 @@ async function initializePrice() {
|
||||
</div>
|
||||
<!--End SectorSegmentation -->
|
||||
|
||||
<Lazy>
|
||||
<div class="w-full mt-10 sm:mt-5 m-auto sm:pl-6 sm:pb-6 sm:pt-6 {!$retailVolumeComponent ? 'hidden' : ''}">
|
||||
{#await import('$lib/components/RetailVolume.svelte') then {default: Comp}}
|
||||
<svelte:component this={Comp} />
|
||||
{/await}
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import {AreaSeries, Chart, PriceLine, CandlestickSeries} from 'svelte-lightweight-charts';
|
||||
|
||||
import { TrackingModeExitMode } from 'lightweight-charts';
|
||||
import {getCache, setCache, screenWidth, displayCompanyName, numberOfUnreadNotification, globalForm, shareholderComponent, trendAnalysisComponent, revenueSegmentationComponent, priceAnalysisComponent, fundamentalAnalysisComponent, userRegion, isCrosshairMoveActive, realtimePrice, priceIncrease, currentPortfolioPrice, currentPrice, clientSideCache, stockTicker, isOpen, isBeforeMarketOpen, isWeekend} from '$lib/store';
|
||||
import {getCache, setCache, screenWidth, displayCompanyName, numberOfUnreadNotification, globalForm, retailVolumeComponent, shareholderComponent, trendAnalysisComponent, revenueSegmentationComponent, priceAnalysisComponent, fundamentalAnalysisComponent, userRegion, isCrosshairMoveActive, realtimePrice, priceIncrease, currentPortfolioPrice, currentPrice, clientSideCache, stockTicker, isOpen, isBeforeMarketOpen, isWeekend} from '$lib/store';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import StockKeyInformation from '$lib/components/StockKeyInformation.svelte';
|
||||
import BullBearSay from '$lib/components/BullBearSay.svelte';
|
||||
@ -1339,6 +1339,14 @@ function changeChartType() {
|
||||
<!--End RevenueSegmentation-->
|
||||
|
||||
|
||||
<Lazy>
|
||||
<div class="w-full mt-10 sm:mt-5 m-auto sm:pl-6 sm:pb-6 sm:pt-6 {!$retailVolumeComponent ? 'hidden' : ''}">
|
||||
{#await import('$lib/components/RetailVolume.svelte') then {default: Comp}}
|
||||
<svelte:component this={Comp} />
|
||||
{/await}
|
||||
</div>
|
||||
</Lazy>
|
||||
|
||||
|
||||
<Lazy>
|
||||
<div class="w-full mt-10 sm:mt-5 m-auto sm:pl-6 sm:pb-6 sm:pt-6">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user