diff --git a/backend/api/closing_price.py b/backend/api/closing_price.py new file mode 100644 index 0000000..13f1839 --- /dev/null +++ b/backend/api/closing_price.py @@ -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') \ No newline at end of file diff --git a/stockingly-frontend/package.json b/stockingly-frontend/package.json index 7ce9d2e..7ce9a15 100644 --- a/stockingly-frontend/package.json +++ b/stockingly-frontend/package.json @@ -10,12 +10,16 @@ }, "dependencies": { "@mdi/font": "^7.2.96", + "ag-charts-community": "^7.3.0", + "ag-charts-vue3": "^7.3.0", "core-js": "^3.29.0", "debounce": "^1.2.1", "fuse.js": "^6.6.2", "roboto-fontface": "*", "round-to": "^6.0.0", "vue": "^3.2.0", + "vue-class-component": "^8.0.0-beta.3", + "vue-property-decorator": "^9.1.2", "vue-router": "^4.0.0", "vuetify": "^3.2.3", "webfontloader": "^1.0.0" @@ -25,7 +29,7 @@ "@types/debounce": "^1.2.1", "@types/node": "^18.15.0", "@types/webfontloader": "^1.6.35", - "@vitejs/plugin-vue": "^3.0.3", + "@vitejs/plugin-vue": "^4.0.0", "@vue/eslint-config-typescript": "^11.0.0", "eslint": "^8.22.0", "eslint-plugin-vue": "^9.3.0", diff --git a/stockingly-frontend/src/api/index.ts b/stockingly-frontend/src/api/index.ts index 27a3c15..f2f1024 100644 --- a/stockingly-frontend/src/api/index.ts +++ b/stockingly-frontend/src/api/index.ts @@ -20,8 +20,16 @@ export interface Company { 'Past Performance': number; } +export interface PriceHistory { + date: string; + [ticker: string]: string | number; // really just number +} + export const getCompanies = (): Promise => fetch(BACKEND_URL + '/companies').then(r => r.json()).then(list => list.map((e: Company) => ({ ...e, logoSrc: `${BACKEND_URL}/companies/logos/${e.ticker}` - }))); \ No newline at end of file + }))); + +export const getPriceHistory = (tickers: string[]): Promise => + fetch(BACKEND_URL + '/price_history/' + tickers.join(',').toUpperCase()).then(r => r.json()) \ No newline at end of file diff --git a/stockingly-frontend/src/router/index.ts b/stockingly-frontend/src/router/index.ts index ff15763..4b85f5f 100644 --- a/stockingly-frontend/src/router/index.ts +++ b/stockingly-frontend/src/router/index.ts @@ -14,6 +14,11 @@ const routes = [ // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'), }, + { + path: '/comparison/:tickers([A-Z]+)+', + name: 'Comparison', + component: () => import(/* webpackChunkName: "comparison" */ '@/views/Comparison.vue'), + }, ], }, ] diff --git a/stockingly-frontend/src/views/Comparison.vue b/stockingly-frontend/src/views/Comparison.vue new file mode 100644 index 0000000..b88b4cf --- /dev/null +++ b/stockingly-frontend/src/views/Comparison.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/stockingly-frontend/src/views/Home.vue b/stockingly-frontend/src/views/Home.vue index 193ce57..98d5e2f 100644 --- a/stockingly-frontend/src/views/Home.vue +++ b/stockingly-frontend/src/views/Home.vue @@ -75,7 +75,8 @@ {{ selected.length }} / {{ MAX_SELECT }} companies selected: {{ s }} - + Compare ([]); const filteredCompanies = ref<(Company & { score?: number })[]>([]); const selected = reactive([]); const searchText = ref(""); +const router = useRouter(); let fuse: Fuse | undefined = undefined; const search = debounce(() => setTimeout(() => { @@ -132,6 +135,8 @@ const clearSearch = () => searchText.value = ''; const setSearch = (text: string) => searchText.value = text; +const goToComparison = (tickers: string[]) => router.push('/comparison/' + tickers.join('/').toUpperCase()); + watch(filteredCompanies, () => loading.value = false); type ColorScale = { gt?: number, color: string }[]; @@ -217,13 +222,9 @@ const metrics = computed(() => metricsData.map(e => ({ percentage: (c: Company) => (e.value(c) - e.minValue) * 100 / (e.maxValue - e.minValue), value: (c: Company) => roundTo(e.value(c), e.decimals ?? 2), color: (c: Company) => { - console.log(JSON.parse(JSON.stringify(e.scale))); const value = e.value(c); - console.log(c.ticker, value); for (const s of e.scale) { - console.log('checking', s.gt, s.color); if (typeof s.gt !== 'number' || value > s.gt) { - console.log(s.gt, s.color); return s.color; } } diff --git a/stockingly-frontend/yarn.lock b/stockingly-frontend/yarn.lock index a573c91..ae6726e 100644 --- a/stockingly-frontend/yarn.lock +++ b/stockingly-frontend/yarn.lock @@ -327,10 +327,10 @@ "@typescript-eslint/types" "5.59.2" eslint-visitor-keys "^3.3.0" -"@vitejs/plugin-vue@^3.0.3": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54" - integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw== +"@vitejs/plugin-vue@^4.0.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6" + integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw== "@volar/language-core@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" 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: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1613,6 +1623,11 @@ vite@^4.2.0: optionalDependencies: 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: version "9.2.0" 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" 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: version "4.1.6" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1" diff --git a/stockingly.py b/stockingly.py index 069840a..16fadbe 100644 --- a/stockingly.py +++ b/stockingly.py @@ -2,7 +2,9 @@ from flask import Flask, jsonify, redirect, url_for, send_from_directory from flask_cors import CORS from backend.utils.build_frontend import build_frontend from backend.api.companies import get_companies +from backend.api.closing_price import get_closing_price_hist import os +import sys import subprocess @@ -26,6 +28,12 @@ def companies() -> object: return jsonify(get_companies(ROOT_DIR)) +@app.route('/price_history/', methods=['GET']) +def price_history(tickers) -> object: + tickers: list[str] = str(tickers).split(',') + return jsonify(get_closing_price_hist(tickers)) + + @app.route('/companies/logos/') def get_company_logo(ticker: str): logo_dir: str = os.path.join(ROOT_DIR, 'scraper', 'logos', 'logos') @@ -38,5 +46,6 @@ def get_company_logo(ticker: str): if __name__ == '__main__': - build_frontend() + if len(sys.argv) < 2 or sys.argv[1] != 'no-frontend': + build_frontend() app.run()