import re import json import csv import sys import requests from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor, as_completed 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 def get_currency(market): return {"GB": "GBP", "US": "USD"}.get(market, "GBP") 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) m = re.search(r"Latest price\s*:\s*[£$€]([0-9,.]+)", text) if m: return float(m.group(1).replace(",", "")) return None holdings_file = sys.argv[1] if len(sys.argv) > 1 else "holdings.csv" holdings = [] with open(holdings_file) as f: reader = csv.reader(f) for row in reader: name, ticker, market = row[0].strip(), row[1].strip(), row[2].strip() shares = float(row[3].strip()) currency = get_currency(market) holdings.append({"name": name, "ticker": ticker, "market": market, "shares": shares, "currency": currency}) 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 print(json.dumps(holdings, indent=2))