upgrade_2
This commit is contained in:
parent
cb4671c3a0
commit
72986fa192
File diff suppressed because one or more lines are too long
@ -19,6 +19,8 @@
|
|||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"formidable": "1.2.2",
|
"formidable": "1.2.2",
|
||||||
"helmet": "4.1.1",
|
"helmet": "4.1.1",
|
||||||
|
"yahoo-finance2": "^2.0.0",
|
||||||
|
"node-cron": "^3.10.0",
|
||||||
"json2csv": "^5.0.7",
|
"json2csv": "^5.0.7",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|||||||
@ -12,6 +12,29 @@ const { parse } = require('json2csv');
|
|||||||
|
|
||||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||||
|
|
||||||
|
// Market data endpoints
|
||||||
|
const marketDataService = require('../services/marketDataService');
|
||||||
|
|
||||||
|
// Get historical price data for different ranges
|
||||||
|
router.get('/:id/price-history', wrapAsync(async (req, res) => {
|
||||||
|
const stock = await StocksDBApi.findBy({ id: req.params.id });
|
||||||
|
if (!stock) {
|
||||||
|
return res.status(404).send({ message: 'Stock not found' });
|
||||||
|
}
|
||||||
|
const range = req.query.range || 'monthly';
|
||||||
|
const data = await marketDataService.getHistoricalPrices(stock.ticker, range);
|
||||||
|
res.status(200).send(data);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get current market price
|
||||||
|
router.get('/:id/price-current', wrapAsync(async (req, res) => {
|
||||||
|
const stock = await StocksDBApi.findBy({ id: req.params.id });
|
||||||
|
if (!stock) {
|
||||||
|
return res.status(404).send({ message: 'Stock not found' });
|
||||||
|
}
|
||||||
|
const price = await marketDataService.getCurrentPrice(stock.ticker);
|
||||||
|
res.status(200).send({ price });
|
||||||
|
}));
|
||||||
router.use(checkCrudPermissions('stocks'));
|
router.use(checkCrudPermissions('stocks'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
32
backend/src/services/marketDataService.js
Normal file
32
backend/src/services/marketDataService.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
const yahooFinance = require('yahoo-finance2').default;
|
||||||
|
|
||||||
|
async function getCurrentPrice(ticker) {
|
||||||
|
const quote = await yahooFinance.quote(ticker);
|
||||||
|
return quote.regularMarketPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHistoricalPrices(ticker, range) {
|
||||||
|
const now = new Date();
|
||||||
|
let days;
|
||||||
|
switch (range) {
|
||||||
|
case 'daily': days = 1; break;
|
||||||
|
case 'weekly': days = 7; break;
|
||||||
|
case 'monthly': days = 30; break;
|
||||||
|
case '3m': days = 90; break;
|
||||||
|
case '6m': days = 180; break;
|
||||||
|
case '1y': days = 365; break;
|
||||||
|
case '3y': days = 365 * 3; break;
|
||||||
|
case '5y': days = 365 * 5; break;
|
||||||
|
default: days = 30;
|
||||||
|
}
|
||||||
|
const from = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||||
|
const options = { period1: from, period2: now, interval: '1d' };
|
||||||
|
const result = await yahooFinance.historical(ticker, options);
|
||||||
|
// Return array of { date, price }
|
||||||
|
return result.map(item => ({ date: item.date, price: item.close }));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCurrentPrice,
|
||||||
|
getHistoricalPrices,
|
||||||
|
};
|
||||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { ReactElement, useEffect } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
@ -17,6 +17,21 @@ import CardBox from '../../components/CardBox';
|
|||||||
import BaseButton from '../../components/BaseButton';
|
import BaseButton from '../../components/BaseButton';
|
||||||
import BaseDivider from '../../components/BaseDivider';
|
import BaseDivider from '../../components/BaseDivider';
|
||||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(LineElement, PointElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||||
|
|
||||||
import { SwitchField } from '../../components/SwitchField';
|
import { SwitchField } from '../../components/SwitchField';
|
||||||
import FormField from '../../components/FormField';
|
import FormField from '../../components/FormField';
|
||||||
|
|
||||||
@ -34,6 +49,33 @@ const StocksView = () => {
|
|||||||
function removeLastCharacter(str) {
|
function removeLastCharacter(str) {
|
||||||
console.log(str, `str`);
|
console.log(str, `str`);
|
||||||
return str.slice(0, -1);
|
return str.slice(0, -1);
|
||||||
|
|
||||||
|
// Chart timeframe controls and data state
|
||||||
|
const ranges = ['daily','weekly','monthly','3m','6m','1y','3y','5y'];
|
||||||
|
const [selectedRange, setSelectedRange] = useState('monthly');
|
||||||
|
const [chartData, setChartData] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
axios.get(`/api/stocks/${id}/price-history?range=${selectedRange}`)
|
||||||
|
.then(res => setChartData(res.data))
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}, [id, selectedRange]);
|
||||||
|
|
||||||
|
// Prepare data for Chart.js
|
||||||
|
const chartLabels = chartData.map(item => dayjs(item.date).format('YYYY-MM-DD'));
|
||||||
|
const chartPrices = chartData.map(item => item.price);
|
||||||
|
const chartJsData = {
|
||||||
|
labels: chartLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: `Price (${selectedRange})`,
|
||||||
|
data: chartPrices,
|
||||||
|
borderColor: '#4F46E5',
|
||||||
|
backgroundColor: 'rgba(79,70,229,0.5)',
|
||||||
|
fill: false,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -43,6 +85,27 @@ const StocksView = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex space-x-2 mb-4">
|
||||||
|
{ranges.map(range => (
|
||||||
|
<button
|
||||||
|
key={range}
|
||||||
|
onClick={() => setSelectedRange(range)}
|
||||||
|
className={
|
||||||
|
selectedRange === range
|
||||||
|
? 'px-3 py-1 rounded border bg-blue-500 text-white'
|
||||||
|
: 'px-3 py-1 rounded border bg-white text-blue-500'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{range.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="h-64 mb-4">
|
||||||
|
<Line data={chartJsData} options={{ responsive: true, maintainAspectRatio: false }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<title>{getPageTitle('View stocks')}</title>
|
<title>{getPageTitle('View stocks')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user