189 lines
7.6 KiB
Python
Executable File
189 lines
7.6 KiB
Python
Executable File
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()) |