2026-04-20 11:38:27 +01:00
|
|
|
import re
|
|
|
|
|
import json
|
|
|
|
|
import csv
|
|
|
|
|
import sys
|
|
|
|
|
import requests
|
|
|
|
|
from bs4 import BeautifulSoup
|
2026-04-20 11:59:27 +01:00
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
2026-04-20 11:38:27 +01:00
|
|
|
|
2026-04-20 11:48:15 +01:00
|
|
|
FX_CACHE = {}
|
|
|
|
|
|
|
|
|
|
def get_fx_rate(currency):
|
|
|
|
|
if currency in FX_CACHE:
|
|
|
|
|
return FX_CACHE[currency]
|
|
|
|
|
if currency == "GBP":
|
|
|
|
|
return 1.0
|
|
|
|
|
url = f"https://open.er-api.com/v6/latest/{currency}"
|
|
|
|
|
r = requests.get(url, timeout=20)
|
|
|
|
|
rate = r.json()["rates"]["GBP"]
|
|
|
|
|
FX_CACHE[currency] = rate
|
|
|
|
|
return rate
|
|
|
|
|
|
2026-04-20 11:59:27 +01:00
|
|
|
def get_currency(market):
|
|
|
|
|
return {"GB": "GBP", "US": "USD"}.get(market, "GBP")
|
|
|
|
|
|
2026-04-20 11:38:27 +01:00
|
|
|
def get_price(market, ticker):
|
|
|
|
|
url = f"https://web.freetrade.io/universe/{market}/{ticker}"
|
|
|
|
|
response = requests.get(url, headers={
|
|
|
|
|
"User-Agent": "Mozilla/5.0"
|
|
|
|
|
}, timeout=20)
|
|
|
|
|
text = BeautifulSoup(response.text, "html.parser").get_text(" ", strip=True)
|
2026-04-20 11:48:15 +01:00
|
|
|
m = re.search(r"Latest price\s*:\s*[£$€]([0-9,.]+)", text)
|
2026-04-20 11:38:27 +01:00
|
|
|
if m:
|
|
|
|
|
return float(m.group(1).replace(",", ""))
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
holdings_file = sys.argv[1] if len(sys.argv) > 1 else "holdings.csv"
|
|
|
|
|
|
2026-04-20 11:59:27 +01:00
|
|
|
holdings = []
|
2026-04-20 11:38:27 +01:00
|
|
|
with open(holdings_file) as f:
|
|
|
|
|
reader = csv.reader(f)
|
|
|
|
|
for row in reader:
|
2026-04-20 12:32:55 +01:00
|
|
|
name, ticker, market = row[0].strip(), row[1].strip(), row[2].strip()
|
|
|
|
|
shares = float(row[3].strip())
|
2026-04-20 11:48:15 +01:00
|
|
|
currency = get_currency(market)
|
2026-04-20 12:32:55 +01:00
|
|
|
holdings.append({"name": name, "ticker": ticker, "market": market, "shares": shares, "currency": currency})
|
2026-04-20 11:59:27 +01:00
|
|
|
|
|
|
|
|
unique_currencies = {h["currency"] for h in holdings if h["currency"] != "GBP"}
|
|
|
|
|
for currency in unique_currencies:
|
|
|
|
|
get_fx_rate(currency)
|
|
|
|
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
|
|
|
|
future_to_idx = {
|
|
|
|
|
pool.submit(get_price, h["market"], h["ticker"]): i
|
|
|
|
|
for i, h in enumerate(holdings)
|
|
|
|
|
}
|
|
|
|
|
for future in as_completed(future_to_idx):
|
|
|
|
|
idx = future_to_idx[future]
|
|
|
|
|
holdings[idx]["price"] = future.result()
|
|
|
|
|
|
|
|
|
|
for h in holdings:
|
|
|
|
|
if h["price"] is not None:
|
|
|
|
|
fx = FX_CACHE.get(h["currency"], 1.0)
|
|
|
|
|
h["fx_rate"] = fx
|
|
|
|
|
h["total_value_gbp"] = round(h["price"] * h["shares"] * fx, 2)
|
|
|
|
|
else:
|
|
|
|
|
h["fx_rate"] = None
|
|
|
|
|
h["total_value_gbp"] = None
|
2026-04-20 11:38:27 +01:00
|
|
|
|
2026-04-20 11:59:27 +01:00
|
|
|
print(json.dumps(holdings, indent=2))
|