Comparison page implemented with stock price chart all time

This commit is contained in:
Claudio Maggioni 2023-05-20 23:49:12 +02:00
parent c010100c45
commit 1b9e6c4396
8 changed files with 135 additions and 12 deletions

View file

@ -0,0 +1,24 @@
import os
import pandas as pd
import numpy as np
from scraper.top100_extractor import programming_crime_list
from typing import Optional
DF_HIST_PATH: str = os.path.join(os.path.dirname(__file__), '..', '..', 'Elaborated_Data', 'price_history_data.csv')
DF_HIST: Optional[pd.DataFrame] = None
def lazy_load_history():
global DF_HIST
if DF_HIST is None:
DF_HIST = pd.read_csv(DF_HIST_PATH, index_col=0)
def get_closing_price_hist(tickers: list[str]) -> list[dict]:
lazy_load_history()
ticker_series = pd.Series(tickers)
df = DF_HIST.loc[DF_HIST.symbol.isin(ticker_series), :] \
.rename(columns={"Closing Price": "price", "symbol": "ticker"}) \
.reset_index(drop=True)
df = df.pivot(index='date', columns='ticker', values='price').reset_index(drop=False)
return df.replace({ np.nan: None }).to_dict('records')

View file

@ -10,12 +10,16 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "^7.2.96", "@mdi/font": "^7.2.96",
"ag-charts-community": "^7.3.0",
"ag-charts-vue3": "^7.3.0",
"core-js": "^3.29.0", "core-js": "^3.29.0",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"roboto-fontface": "*", "roboto-fontface": "*",
"round-to": "^6.0.0", "round-to": "^6.0.0",
"vue": "^3.2.0", "vue": "^3.2.0",
"vue-class-component": "^8.0.0-beta.3",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.0.0", "vue-router": "^4.0.0",
"vuetify": "^3.2.3", "vuetify": "^3.2.3",
"webfontloader": "^1.0.0" "webfontloader": "^1.0.0"
@ -25,7 +29,7 @@
"@types/debounce": "^1.2.1", "@types/debounce": "^1.2.1",
"@types/node": "^18.15.0", "@types/node": "^18.15.0",
"@types/webfontloader": "^1.6.35", "@types/webfontloader": "^1.6.35",
"@vitejs/plugin-vue": "^3.0.3", "@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",

View file

@ -20,8 +20,16 @@ export interface Company {
'Past Performance': number; 'Past Performance': number;
} }
export interface PriceHistory {
date: string;
[ticker: string]: string | number; // really just number
}
export const getCompanies = (): Promise<Company[]> => export const getCompanies = (): Promise<Company[]> =>
fetch(BACKEND_URL + '/companies').then(r => r.json()).then(list => list.map((e: Company) => ({ fetch(BACKEND_URL + '/companies').then(r => r.json()).then(list => list.map((e: Company) => ({
...e, ...e,
logoSrc: `${BACKEND_URL}/companies/logos/${e.ticker}` logoSrc: `${BACKEND_URL}/companies/logos/${e.ticker}`
}))); })));
export const getPriceHistory = (tickers: string[]): Promise<PriceHistory[]> =>
fetch(BACKEND_URL + '/price_history/' + tickers.join(',').toUpperCase()).then(r => r.json())

View file

@ -14,6 +14,11 @@ const routes = [
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'), component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
}, },
{
path: '/comparison/:tickers([A-Z]+)+',
name: 'Comparison',
component: () => import(/* webpackChunkName: "comparison" */ '@/views/Comparison.vue'),
},
], ],
}, },
] ]

View file

@ -0,0 +1,52 @@
<template>
<v-app-bar color="primary">
<v-btn icon @click="goHome">
<v-icon>mdi-home</v-icon>
</v-btn>
<v-app-bar-title>
Stockingly
</v-app-bar-title>
</v-app-bar>
<v-container class="fill-height">
<h1>Stock price over time</h1>
<ag-charts-vue v-if="options !== null" :options="options" style="width: 100%; height: 30em" />
</v-container>
</template>
<script setup lang="ts">
import { AgChartsVue } from 'ag-charts-vue3';
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { PriceHistory, getPriceHistory } from '@/api';
const route = useRoute();
const router = useRouter();
const goHome = () => router.push('/');
const options = ref<object | null>(null);
// load chart data
onMounted(() => {
const tickers = route.params.tickers.toString().split(',');
console.log(tickers);
getPriceHistory(tickers).then((p: PriceHistory[]) => {
options.value = {
data: p.map(e => ({ ...e, date: Date.parse(e.date) })),
series: tickers.map((t: string) => ({ xKey: 'date', yKey: t })),
axes: [
{
type: 'time',
position: 'bottom',
},
{
type: 'number',
position: 'left',
},
],
};
});
});
</script>

View file

@ -75,7 +75,8 @@
<small class="ma-2">{{ selected.length }} / {{ MAX_SELECT }} companies selected:</small> <small class="ma-2">{{ selected.length }} / {{ MAX_SELECT }} companies selected:</small>
<v-chip class="ma-2" color="secondary" closable style="flex: initial !important" v-for="s in selected" :key="s" <v-chip class="ma-2" color="secondary" closable style="flex: initial !important" v-for="s in selected" :key="s"
@click:close="unselect(s)">{{ s }}</v-chip> @click:close="unselect(s)">{{ s }}</v-chip>
<v-btn class="ma-2" variant="flat" size="small" color="primary" prepend-icon="mdi-chart-areaspline-variant"> <v-btn class="ma-2" variant="flat" size="small" color="primary" prepend-icon="mdi-chart-areaspline-variant"
@click="goToComparison(selected)">
Compare Compare
</v-btn> </v-btn>
<v-btn class="ma-2" size="small" variant="flat" color="error" @click="selected.splice(0, selected.length)" <v-btn class="ma-2" size="small" variant="flat" color="error" @click="selected.splice(0, selected.length)"
@ -91,6 +92,7 @@ import { ref, reactive, computed, watch } from 'vue';
import { roundTo } from 'round-to'; import { roundTo } from 'round-to';
import debounce from 'debounce'; import debounce from 'debounce';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { useRouter } from 'vue-router';
const MAX_SELECT = 3; const MAX_SELECT = 3;
@ -99,6 +101,7 @@ const companies = ref<Company[]>([]);
const filteredCompanies = ref<(Company & { score?: number })[]>([]); const filteredCompanies = ref<(Company & { score?: number })[]>([]);
const selected = reactive<string[]>([]); const selected = reactive<string[]>([]);
const searchText = ref(""); const searchText = ref("");
const router = useRouter();
let fuse: Fuse<Company> | undefined = undefined; let fuse: Fuse<Company> | undefined = undefined;
const search = debounce(() => setTimeout(() => { const search = debounce(() => setTimeout(() => {
@ -132,6 +135,8 @@ const clearSearch = () => searchText.value = '';
const setSearch = (text: string) => searchText.value = text; const setSearch = (text: string) => searchText.value = text;
const goToComparison = (tickers: string[]) => router.push('/comparison/' + tickers.join('/').toUpperCase());
watch(filteredCompanies, () => loading.value = false); watch(filteredCompanies, () => loading.value = false);
type ColorScale = { gt?: number, color: string }[]; type ColorScale = { gt?: number, color: string }[];
@ -217,13 +222,9 @@ const metrics = computed<ExtendedMetric[]>(() => metricsData.map(e => ({
percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue), percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue),
value: (c: Company) => roundTo(e.value(c), e.decimals ?? 2), value: (c: Company) => roundTo(e.value(c), e.decimals ?? 2),
color: (c: Company) => { color: (c: Company) => {
console.log(JSON.parse(JSON.stringify(e.scale)));
const value = e.value(c); const value = e.value(c);
console.log(c.ticker, value);
for (const s of e.scale) { for (const s of e.scale) {
console.log('checking', s.gt, s.color);
if (typeof s.gt !== 'number' || value > s.gt) { if (typeof s.gt !== 'number' || value > s.gt) {
console.log(s.gt, s.color);
return s.color; return s.color;
} }
} }

View file

@ -327,10 +327,10 @@
"@typescript-eslint/types" "5.59.2" "@typescript-eslint/types" "5.59.2"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-vue@^3.0.3": "@vitejs/plugin-vue@^4.0.0":
version "3.2.0" version "4.2.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6"
integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw== integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==
"@volar/language-core@1.4.1": "@volar/language-core@1.4.1":
version "1.4.1" version "1.4.1"
@ -563,6 +563,16 @@ acorn@^8.8.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
ag-charts-community@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/ag-charts-community/-/ag-charts-community-7.3.0.tgz#575284d2ed248fc725fc6b3029c7eae9872cec39"
integrity sha512-118U6YsCMia6iZHaN06zT19rr2SYa92WB73pMVCKQlp2H3c19uKQ6Y6DfKG/nIfNUzFXZLHBwKIdZXsMWJdZww==
ag-charts-vue3@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/ag-charts-vue3/-/ag-charts-vue3-7.3.0.tgz#afb4e978fecb80c9066856dc4741bad5a811d742"
integrity sha512-ftGrgH+xfI7PzXWW0N48KZkd7qhWqaS58VsSKeWNTJRqAzUIEI103EDGdi8VcD2D91JmLCkChGR2RyP3EVN75Q==
ajv@^6.10.0, ajv@^6.12.4: ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6" version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -1613,6 +1623,11 @@ vite@^4.2.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
vue-class-component@^8.0.0-beta.3:
version "8.0.0-rc.1"
resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-8.0.0-rc.1.tgz#db692cd97656eb9a08206c03d0b7398cdb1d9420"
integrity sha512-w1nMzsT/UdbDAXKqhwTmSoyuJzUXKrxLE77PCFVuC6syr8acdFDAq116xgvZh9UCuV0h+rlCtxXolr3Hi3HyPQ==
vue-eslint-parser@^9.0.1, vue-eslint-parser@^9.1.1: vue-eslint-parser@^9.0.1, vue-eslint-parser@^9.1.1:
version "9.2.0" version "9.2.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.2.0.tgz#b397a7afae29961e5c59d80e895ab54a2e7f2f41" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.2.0.tgz#b397a7afae29961e5c59d80e895ab54a2e7f2f41"
@ -1626,6 +1641,11 @@ vue-eslint-parser@^9.0.1, vue-eslint-parser@^9.1.1:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-property-decorator@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz#266a2eac61ba6527e2e68a6933cfb98fddab5457"
integrity sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==
vue-router@^4.0.0: vue-router@^4.0.0:
version "4.1.6" version "4.1.6"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1"

View file

@ -2,7 +2,9 @@ from flask import Flask, jsonify, redirect, url_for, send_from_directory
from flask_cors import CORS from flask_cors import CORS
from backend.utils.build_frontend import build_frontend from backend.utils.build_frontend import build_frontend
from backend.api.companies import get_companies from backend.api.companies import get_companies
from backend.api.closing_price import get_closing_price_hist
import os import os
import sys
import subprocess import subprocess
@ -26,6 +28,12 @@ def companies() -> object:
return jsonify(get_companies(ROOT_DIR)) return jsonify(get_companies(ROOT_DIR))
@app.route('/price_history/<tickers>', methods=['GET'])
def price_history(tickers) -> object:
tickers: list[str] = str(tickers).split(',')
return jsonify(get_closing_price_hist(tickers))
@app.route('/companies/logos/<ticker>') @app.route('/companies/logos/<ticker>')
def get_company_logo(ticker: str): def get_company_logo(ticker: str):
logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos') logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos')
@ -38,5 +46,6 @@ def get_company_logo(ticker: str):
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 2 or sys.argv[1] != 'no-frontend':
build_frontend() build_frontend()
app.run() app.run()