Skip to content

Commit

Permalink
page to interact with financial data
Browse files Browse the repository at this point in the history
- add an extension to reuse request session across flask
  • Loading branch information
chungg committed Dec 13, 2023
1 parent 6be7046 commit 36cf562
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 46 deletions.
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 %}

0 comments on commit 36cf562

Please sign in to comment.