backend/app/cron_portfolio.py
2024-05-26 19:51:33 +02:00

189 lines
7.6 KiB
Python

import pytz
from datetime import datetime, timedelta
from urllib.request import urlopen
import certifi
import json
import ujson
import schedule
import time
import subprocess
from pocketbase import PocketBase # Client also works the same
import asyncio
import aiohttp
import pytz
import pandas as pd
import numpy as np
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv('FMP_API_KEY')
pb_admin_email = os.getenv('POCKETBASE_ADMIN_EMAIL')
pb_password = os.getenv('POCKETBASE_PASSWORD')
#berlin_tz = pytz.timezone('Europe/Berlin')
new_york_tz = pytz.timezone('America/New_York')
pb = PocketBase('http://127.0.0.1:8090')
admin_data = pb.admins.auth_with_password(pb_admin_email, pb_password)
# Set the system's timezone to Berlin at the beginning
subprocess.run(["timedatectl", "set-timezone", "Europe/Berlin"])
async def get_quote_of_stocks(ticker_list):
ticker_str = ','.join(ticker_list)
async with aiohttp.ClientSession() as session:
url = f"https://financialmodelingprep.com/api/v3/quote/{ticker_str}?apikey={api_key}"
async with session.get(url) as response:
df = await response.json()
return df
def check_number_of_shares(holdings, trading_history):
# Create a dictionary to track the current number of shares for each symbol
share_count = {}
# Update share count based on history
for transaction in trading_history:
symbol = transaction["symbol"]
num_shares = transaction["numberOfShares"]
if transaction["type"] == "buy":
# Increment the share count for the symbol
share_count[symbol] = share_count.get(symbol, 0) + num_shares
elif transaction["type"] == "sell":
# Decrement the share count for the symbol
share_count[symbol] = share_count.get(symbol, 0) - num_shares
# Update the holdings list based on the share count
for holding in holdings:
symbol = holding["symbol"]
if symbol in share_count:
holding["numberOfShares"] = share_count[symbol]
return holdings
def compute_available_cash(transactions):
available_cash = 100000 # Initial available cash
for transaction in transactions:
if transaction['type'] == 'buy':
shares_bought = transaction['numberOfShares']
price_per_share = transaction['price']
total_cost = shares_bought * price_per_share
available_cash -= total_cost
elif transaction['type'] == 'sell':
shares_sold = transaction['numberOfShares']
price_per_share = transaction['price']
total_gain = shares_sold * price_per_share
available_cash += total_gain
return available_cash
def compute_overall_return(initial_budget, transactions):
current_budget = initial_budget
for transaction in transactions:
if transaction["type"] == "buy":
current_budget -= transaction["numberOfShares"] * transaction["price"]
elif transaction["type"] == "sell":
current_budget += transaction["numberOfShares"] * transaction["price"]
overall_return = (current_budget - initial_budget) / initial_budget * 100
#print('overall return: ', overall_return)
return overall_return
async def update_portfolio():
current_time = datetime.now(new_york_tz)
current_weekday = current_time.weekday()
initial_budget = 100000
opening_hour = 9
opening_minute = 30
closing_hour = 17
is_market_open = ( current_time.hour > opening_hour or (current_time.hour == opening_hour and current_time.minute >= opening_minute)) and current_time.hour < closing_hour
if current_weekday <= 5 and is_market_open:
# Get the current date
current_month = datetime.today()
# Set the day to 1 to get the beginning of the current month
beginning_of_month = current_month.replace(day=1)
# Format it as a string if needed
formatted_date = beginning_of_month.strftime("%Y-%m-%d")
result = pb.collection("portfolios").get_full_list(query_params = {"filter": f'created >= "{formatted_date}"'})
ranking_list = []
ticker_list = []
if len(result) != 0:
#get all tickers from all portfolios
for port in result:
if len(port.holdings) != 0:
ticker_list += [i['symbol'] for i in port.holdings]
ticker_list = list(set(ticker_list))
#unique ticker_list
data = await get_quote_of_stocks(ticker_list)
#Get all quotes in bulks to save api calls
for x in result:
if len(x.trading_history) > 0:
try:
if len(x.holdings) != 0:
#compute the correct available cash to avoid bugs
x.available_cash = compute_available_cash(x.trading_history)
account_value = x.available_cash
quote_data_dict = {dd['symbol']: dd for dd in data}
#compute the correct number of shares to avoid bugs
x.holdings = check_number_of_shares(x.holdings, x.trading_history)
for item in x.holdings:
dd = quote_data_dict.get(item['symbol'])
if dd:
current_price = dd['price']
since_bought_change = round((current_price/ item['boughtPrice'] - 1) * 100, 2)
account_value += current_price * item['numberOfShares']
# Update holdings_list
item['currentPrice'] = current_price
item['sinceBoughtChange'] = since_bought_change
overall_return = round( ( account_value/initial_budget -1) * 100 ,2)
#Update Pocketbase with new values
pb.collection("portfolios").update(x.id, {
"accountValue": account_value,
"overallReturn": overall_return,
"availableCash": x.available_cash,
"holdings": x.holdings,
})
else:
#overall_return = x.overall_return
overall_return = compute_overall_return(initial_budget, x.trading_history)
account_value = round(initial_budget*(1+overall_return/100),2)
available_cash = account_value
pb.collection("portfolios").update(x.id, {
"accountValue": account_value,
"overallReturn": overall_return,
"availableCash": available_cash,
})
ranking_list.append({'userId': x.id, 'overallReturn': overall_return})
except Exception as e:
print(e)
else:
pass
#Apply ranking to each user
sorted_ranking_list = sorted(ranking_list, key=lambda x: x['overallReturn'], reverse=True)
for rank, item in enumerate(sorted_ranking_list):
pb.collection("portfolios").update(item['userId'], {
"rank": rank+1,
})
print('Done')
else:
print('Market Closed')
asyncio.run(update_portfolio())