249 lines
6.4 KiB
Python
249 lines
6.4 KiB
Python
import io
|
|
import json
|
|
import os
|
|
import pickle
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from enum import Enum
|
|
|
|
import pandas as pd
|
|
import requests
|
|
from flask import Flask, jsonify, render_template, request, redirect, make_response
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
TREASURY_URL = "https://home.treasury.gov/resource-center/data-chart-center/interest-rates/daily-treasury-rates.csv/2024/all?type=daily_treasury_yield_curve&field_tdr_date_value=2024&page&_format=csv"
|
|
|
|
CURRENT_DATE = 20240524 # Set to friday since market is closed
|
|
|
|
RATES = None
|
|
|
|
|
|
class Side(str, Enum):
|
|
BUY = "BUY"
|
|
SELL = "SELL"
|
|
|
|
|
|
# Since we know all the types of treasuries, just enumerate them
|
|
class Instrument(str, Enum):
|
|
I_1MO = "1 Mo"
|
|
I_2MO = "2 Mo"
|
|
I_3MO = "3 Mo"
|
|
I_4MO = "4 Mo"
|
|
I_6MO = "6 Mo"
|
|
I_1YR = "1 Yr"
|
|
I_2YR = "2 Yr"
|
|
I_3YR = "3 Yr"
|
|
I_5YR = "5 Yr"
|
|
I_7YR = "7 Yr"
|
|
I_10YR = "10 Yr"
|
|
I_20YR = "12 Yr"
|
|
I_30YR = "30 Yr"
|
|
|
|
@classmethod
|
|
def from_text(cls, txt):
|
|
if txt == "1 Mo":
|
|
return Instrument.I_1MO
|
|
elif txt == "2 Mo":
|
|
return Instrument.I_2MO
|
|
elif txt == "3 Mo":
|
|
return Instrument.I_3MO
|
|
elif txt == "4 Mo":
|
|
return Instrument.I_4MO
|
|
elif txt == "6 Mo":
|
|
return Instrument.I_6MO
|
|
elif txt == "1 Yr":
|
|
return Instrument.I_1YR
|
|
elif txt == "2 Yr":
|
|
return Instrument.I_2YR
|
|
elif txt == "3 Yr":
|
|
return Instrument.I_3YR
|
|
elif txt == "5 Yr":
|
|
return Instrument.I_5YR
|
|
elif txt == "7 Yr":
|
|
return Instrument.I_7YR
|
|
elif txt == "10 Yr":
|
|
return Instrument.I_20YR
|
|
elif txt == "12 Yr":
|
|
return Instrument.I_20YR
|
|
return Instrument.I_30YR
|
|
|
|
|
|
class Transaction:
|
|
def __init__(
|
|
self, user: str, instrument: Instrument, quantity: int, date: int, side: Side, rate: float
|
|
):
|
|
self.user = user
|
|
self.instrument: str = str(instrument.value)
|
|
self.side: str = str(side.value)
|
|
self.quantity = quantity
|
|
self.date = date
|
|
self.rate = rate
|
|
|
|
|
|
class User:
|
|
def __init__(self, balance: int = 100000) -> None:
|
|
self.ID = str(uuid.uuid4())
|
|
self.balance = balance
|
|
|
|
|
|
USERS: dict[str, User] = {}
|
|
TRANSACTIONS: dict[str, list[Transaction]] = {}
|
|
|
|
|
|
def background_saver():
|
|
while True:
|
|
with open("users.p", "wb") as fp:
|
|
pickle.dump(USERS, fp)
|
|
with open("transactions.p", "wb") as fp:
|
|
pickle.dump(TRANSACTIONS, fp)
|
|
|
|
time.sleep(5)
|
|
|
|
|
|
def get_treasury_rates_for_year(year: int) -> pd.DataFrame | None:
|
|
global RATES
|
|
|
|
use_cache = False
|
|
if RATES is None:
|
|
if os.path.isfile("./cache.csv"):
|
|
if time.time() - os.path.getmtime("./cache.csv") < 60000:
|
|
use_cache = True
|
|
else:
|
|
use_cache = True
|
|
|
|
if use_cache:
|
|
csv: pd.DataFrame = pd.read_csv("./cache.csv")
|
|
RATES = csv
|
|
else:
|
|
response = requests.get(TREASURY_URL)
|
|
response.raise_for_status()
|
|
raw_csv = response.content.decode("UTF-8")
|
|
csv_io = io.StringIO(raw_csv)
|
|
csv: pd.DataFrame = pd.read_csv(csv_io)
|
|
csv.to_csv("./cache.csv", index=False)
|
|
RATES = csv
|
|
|
|
ret = {}
|
|
cols = list(RATES.columns.values)
|
|
for col in cols:
|
|
ret[col] = RATES[col].tolist()
|
|
|
|
dates = ret["Date"]
|
|
del ret["Date"]
|
|
return ret, dates
|
|
|
|
|
|
@app.route("/api/transactions/<id>")
|
|
def get_transactions_for_user(id):
|
|
t = TRANSACTIONS.get(id)
|
|
if t is None:
|
|
return jsonify([])
|
|
|
|
return jsonify([{"term": tx.instrument, "quantity": tx.quantity, "side": tx.side, "date": tx.date, "rate": tx.rate} for tx in t])
|
|
|
|
|
|
@app.route("/api/buy", methods=["POST"])
|
|
def buy():
|
|
content = request.json
|
|
print(content)
|
|
q = int(content["quantity"])
|
|
i = content["instrument"]
|
|
user_id = content["user_id"]
|
|
cost = q * 100
|
|
if USERS.get(user_id) is not None:
|
|
if cost > USERS[user_id].balance:
|
|
return "You do not have the funds for this transaction", 400
|
|
else:
|
|
return "User does not exist", 404
|
|
|
|
current_rate = RATES[i].tolist()[-1]
|
|
|
|
USERS[user_id].balance = USERS[user_id].balance - cost
|
|
t = Transaction(user_id, Instrument.from_text(i), q, time.time(), Side.BUY, current_rate)
|
|
|
|
if TRANSACTIONS.get(user_id) is None:
|
|
TRANSACTIONS[user_id] = [t]
|
|
else:
|
|
TRANSACTIONS[user_id].append(t)
|
|
|
|
return jsonify(t)
|
|
|
|
|
|
@app.route("/api/login", methods=["POST"])
|
|
def login():
|
|
content = request.json
|
|
id = content.get("id")
|
|
if id is None:
|
|
return "ID cannot be empty", 400
|
|
u = USERS.get(id)
|
|
if u is None:
|
|
return "User does not exist", 404
|
|
resp = make_response(redirect("/"))
|
|
resp.set_cookie("userid", u.ID)
|
|
return resp
|
|
|
|
|
|
@app.route("/api/createuser", methods=["POST"])
|
|
def createuser():
|
|
u = User()
|
|
USERS[u.ID] = u
|
|
resp = make_response(redirect("/"))
|
|
resp.set_cookie("userid", u.ID)
|
|
return resp
|
|
|
|
|
|
@app.route("/register")
|
|
def register():
|
|
if "userid" in request.cookies:
|
|
return redirect("/")
|
|
return render_template("register.html")
|
|
|
|
|
|
@app.route("/logout")
|
|
def logout():
|
|
if "userid" not in request.cookies:
|
|
return redirect("/register")
|
|
resp = make_response(redirect("/register"))
|
|
resp.set_cookie("userid", "", expires=0)
|
|
return resp
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
if "userid" not in request.cookies:
|
|
return redirect("/register")
|
|
uid = request.cookies.get("userid")
|
|
if uid not in USERS:
|
|
resp = make_response(redirect("/register"))
|
|
resp.set_cookie("userid", "", expires=0)
|
|
return resp
|
|
rates, dates = get_treasury_rates_for_year(2024)
|
|
return render_template("index.html", rates=[rates], dates=dates, userid=uid, balance=USERS[uid].balance)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Load up files if they exist
|
|
if os.path.isfile("./users.p"):
|
|
print("Loading users")
|
|
with open("./users.p", "rb") as fp:
|
|
USERS = pickle.load(fp)
|
|
|
|
if os.path.isfile("./transactions.p"):
|
|
print("Loading transactions")
|
|
with open("./transactions.p", "rb") as fp:
|
|
TRANSACTIONS = pickle.load(fp)
|
|
|
|
# Start persistence
|
|
thread = threading.Thread(target=background_saver)
|
|
thread.start()
|
|
|
|
# Start Server
|
|
app.run(host='0.0.0.0', port=8008)
|
|
|
|
|
|
thread = threading.Thread(target=background_saver)
|
|
thread.start()
|