Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

page to interact with financial data #9

Merged
merged 1 commit into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/api/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ def analytics():
return flask.render_template("analytics.html")


@bp.get('/yahoo')
def yahoo():
return flask.render_template("yahoo.html")


@bp.get('/logout')
def logout():
logout_user()
Expand Down
36 changes: 36 additions & 0 deletions app/api/v1/data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import json

import flask
Expand All @@ -7,6 +8,7 @@
import numpy as np

from app.api.v1 import bp
from app.extensions import reqs


@bp.get('/data/random')
Expand Down Expand Up @@ -65,6 +67,7 @@ def death_data():
return """
<script>
new Tabulator("#death-table", {
index: "cause",
layout: "fitColumns",
data: %s,
frozenRowsField: "cause",
Expand All @@ -82,3 +85,36 @@ def death_data():
</script>
""" % (data['data'], col_props)
return data


USER_AGENT_HEADERS = {
# required or yahoo will block
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/39.0.2171.95 Safari/537.36'
}


@bp.get('/data/market/prices')
def get_prices():
ticker = request.args['ticker']
interval = request.args.get('interval', '1d')
start = request.args.get('start', int(datetime.datetime(2023, 1, 1).timestamp()))
stop = request.args.get('stop', int(datetime.datetime.now().timestamp()))
events = request.args.get('events', 'capitalGain|div|split')
res = reqs.session.get(
f'https://query2.finance.yahoo.com/v8/finance/chart/{ticker}',
headers=USER_AGENT_HEADERS,
params={'interval': interval, 'events': events,
'period1': start, 'period2': stop})
data = res.json()['chart']['result'][0]
data['timestamp'] = np.datetime_as_string(
np.asarray(data['timestamp'], dtype='datetime64[s]'), unit='D').tolist()
if request.headers.get('Hx-Request'):
resp = flask.Response()
resp = flask.Response(
'<script id="priceData" type="application/json">%s</script>' % (json.dumps(data)))
resp.headers['HX-Trigger-After-Swap'] = json.dumps(
{'displayPrices': {'target': 'priceChart', 'dataId': 'priceData'}})
return resp
return data
3 changes: 2 additions & 1 deletion app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from app import config
from app.cli import show
from app.core.security import RegisterForm
from app.extensions import csrf, migrate, security
from app.extensions import csrf, migrate, reqs, security
from app.models import auth as models
from app.storage.db import db

Expand All @@ -23,6 +23,7 @@ def create_app(conf=None):
else:
app.config.from_object(conf)

reqs.init_app(app)
db.init_app(app)
migrate.init_app(app, db)
user_datastore = SQLAlchemyUserDatastore(db, models.User, models.Role)
Expand Down
21 changes: 21 additions & 0 deletions app/extensions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
from flask import Flask
from flask_migrate import Migrate
from flask_security import Security
from flask_wtf import CSRFProtect
import requests


class Requests:
"""create request session for reuse across appcontext"""

def __init__(self, app: Flask = None):
# alternatively, https://flask.palletsprojects.com/en/2.3.x/appcontext/#storing-data
self.app = app
self.session = requests.Session()

if self.app is not None:
self.init_app(app)

def init_app(self, app: Flask):
app.teardown_appcontext(self.teardown)

def teardown(self, exc: BaseException | None):
self.session.close()


migrate = Migrate()
security = Security()
csrf = CSRFProtect()
reqs = Requests()
87 changes: 42 additions & 45 deletions app/templates/analytics.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,58 +62,55 @@ <h1 class="title">

<script>
// chartjs + htmx with payload in HX-Trigger
document.body.addEventListener("drawChart", function(evt){
const chart = Chart.getChart(evt.detail.target)
document.body.addEventListener("drawChart", function (evt) {
const chart = Chart.getChart(evt.detail.target);
// https://www.reddit.com/r/htmx/comments/10sdk43/comment/j72m2j7/
data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
addData(chart, data.datasets, data.labels);
document.getElementById(evt.detail.dataId).remove()
})
document.getElementById(evt.detail.dataId).remove();
});

new Chart(
document.getElementById("chartId"),
{
type: "bar",
options: {
responsive: true,
title: {
display: false,
text: ""
}
new Chart(document.getElementById("chartId"), {
type: "bar",
options: {
responsive: true,
title: {
display: false,
text: "",
},
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange']
}
}
);
},
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
},
});

new Chart(
document.getElementById("lineChartId"),
{
type: "line",
plugins: [arbLines],
options: {
events: ['mousedown', 'mouseup', 'mousemove'],
responsive: true,
plugins: {
legend: {
position: "top",
},
title: {
display: true,
text: "Vehicles Sales"
},
tooltip: {
position: 'average',
}
new Chart(document.getElementById("lineChartId"), {
type: "line",
plugins: [arbLines],
options: {
events: ["mousedown", "mouseup", "mousemove"],
responsive: true,
plugins: {
colors: {
forceOverride: true,
},
legend: {
position: "top",
},
title: {
display: true,
text: "Vehicles Sales",
},
tooltip: {
position: "average",
},
interaction: {
axis: 'x',
intersect: false,
mode: 'nearest',
}
},
}
);
interaction: {
axis: "x",
intersect: false,
mode: "nearest",
},
},
});
</script>
{% endblock %}
114 changes: 114 additions & 0 deletions app/templates/yahoo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}finance{% endblock %}
{% block head_css %}
<link rel="stylesheet" href="https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator_bulma.min.css">
{% endblock %}
{% block head_scripts %}
<script src="https://unpkg.com/chart.js@4.4.0"></script>
<script src="https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js"></script>
<script src="../../static/js/utils.js"></script>
{% endblock %}
{% block body %}
<section class="section">
<div class="container">
<h1 class="title">
Finance
</h1>
<hr></hr>
<form
hx-boost="true"
hx-push-url="false"
hx-swap="beforeend"
hx-target="body"
action="/api/v1/data/market/prices"
method="GET">
<div class="field has-addons">
<div class="control">
<input class="input" id="tickerSearch" type="text" name="ticker" placeholder="Ticker">
</div>
<div class="control">
<button class="button is-primary">Submit</button>
</div>
</div>
</form>
<canvas id="priceChart"></canvas>
</div>
</section>
<section class="section">
<div class="container">
<div class="is-striped" id="priceTable"></div>
</div>
</section>

<script>
document.body.addEventListener("displayPrices", function (evt) {
const chart = Chart.getChart(evt.detail.target);
const table = Tabulator.findTable("#priceTable")[0];
const data = JSON.parse(document.getElementById(evt.detail.dataId).textContent);
displayPrices(chart, table, data);
document.getElementById(evt.detail.dataId).remove();
});

new Chart(document.getElementById("priceChart"), {
type: "line",
options: {
responsive: true,
plugins: {
colors: {
forceOverride: true,
},
legend: {
position: "top",
},
title: {
display: false,
text: "Security Prices",
},
tooltip: {
position: "average",
},
},
interaction: {
axis: "x",
intersect: false,
mode: "nearest",
},
},
});

new Tabulator("#priceTable", {
layout: "fitData",
index: "date",
pagination: true, //enable.
paginationSize: 10, // this option can take any positive integer value
placeholder: function () {
//set placeholder based on if there are currently any header filters
return this.getHeaderFilters().length ? "No Matching Data" : "No Data";
},
});

function displayPrices(chart, table, data) {
const prices = data.indicators.quote[0].close;
// ideally, should check overlap of labels between datasets
table.updateOrAddData(data.timestamp.map((a, i) => ({ date: a, [data.meta.symbol]: prices[i] })));
if (!table.columnManager.columns.length) {
table.addColumn({ title: "Date", field: "date", frozen: true }, true);
}
table.addColumn({
title: data.meta.symbol,
field: data.meta.symbol,
hozAlign: "right",
headerSort: false,
formatter: "money",
formatterParams: { thousand: false },
});
chart.data.labels = data.timestamp;
// change prices to percentage so it's comparable
chart.data.datasets.push({
label: data.meta.symbol,
data: prices.map((x) => ((x - prices[0]) / prices[0]) * 100),
});
chart.update();
}
</script>
{% endblock %}