perf(price): parallelize price fetching with thread pool for faster processing

- Use ThreadPoolExecutor to fetch stock prices concurrently across multiple holdings
- Pre-fetch FX rates for unique non-GBP currencies before parallel price fetching
- Group price fetching and FX calculation into separate processing phases
- Reduce I/O wait time by executing network requests in parallel instead of sequentially
This commit is contained in:
Karl Hudgell 2026-04-20 11:59:27 +01:00
parent f971708c5e
commit ddec1e3683

View File

@ -4,6 +4,7 @@ import csv
import sys
import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
FX_CACHE = {}
@ -18,6 +19,9 @@ def get_fx_rate(currency):
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={
@ -29,39 +33,37 @@ def get_price(market, ticker):
return float(m.group(1).replace(",", ""))
return None
def get_currency(market):
return {"GB": "GBP", "US": "USD"}.get(market, "GBP")
holdings_file = sys.argv[1] if len(sys.argv) > 1 else "holdings.csv"
results = []
holdings = []
with open(holdings_file) as f:
reader = csv.reader(f)
for row in reader:
ticker, market, shares = row[0].strip(), row[1].strip(), float(row[2].strip())
ticker, market = row[0].strip(), row[1].strip()
shares = float(row[2].strip())
currency = get_currency(market)
price = get_price(market, ticker)
if price is None:
results.append({
"ticker": ticker,
"market": market,
"currency": currency,
"shares": shares,
"price": None,
"fx_rate": None,
"total_value_gbp": None
})
continue
fx = get_fx_rate(currency)
total_gbp = round(price * shares * fx, 2)
results.append({
"ticker": ticker,
"market": market,
"currency": currency,
"shares": shares,
"price": price,
"fx_rate": fx,
"total_value_gbp": total_gbp
})
holdings.append({"ticker": ticker, "market": market, "shares": shares, "currency": currency})
print(json.dumps(results, indent=2))
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))