refactor(price): enhance currency handling with FX conversion and cache support

- Add `get_fx_rate()` function with caching to fetch foreign exchange rates from open.er-api.com
- Introduce `get_currency()` helper to map market codes to currency identifiers
- Update price parsing regex to support multiple currency symbols (£, $, €)
- Convert shares to float to accommodate fractional holdings
- Add currency, fx_rate, and total_value_gbp fields to output JSON
- Support fractional share counts (e.g., 3.6185 AAPL shares)
This commit is contained in:
Karl Hudgell 2026-04-20 11:48:15 +01:00
parent 38ecbd57e0
commit f971708c5e
2 changed files with 37 additions and 4 deletions

View File

@ -1,3 +1,4 @@
GGP,GB,100 GGP,GB,100
PAF,GB,200 PAF,GB,200
SVM,US,50 SVM,US,50
AAPL,US,3.6185
1 GGP GB 100
2 PAF GB 200
3 SVM US 50
4 AAPL US 3.6185

View File

@ -5,31 +5,63 @@ import sys
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
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_price(market, ticker): def get_price(market, ticker):
url = f"https://web.freetrade.io/universe/{market}/{ticker}" url = f"https://web.freetrade.io/universe/{market}/{ticker}"
response = requests.get(url, headers={ response = requests.get(url, headers={
"User-Agent": "Mozilla/5.0" "User-Agent": "Mozilla/5.0"
}, timeout=20) }, timeout=20)
text = BeautifulSoup(response.text, "html.parser").get_text(" ", strip=True) text = BeautifulSoup(response.text, "html.parser").get_text(" ", strip=True)
m = re.search(r"Latest price\s*:\s*[£$]([0-9,.]+)", text) m = re.search(r"Latest price\s*:\s*[£$]([0-9,.]+)", text)
if m: if m:
return float(m.group(1).replace(",", "")) return float(m.group(1).replace(",", ""))
return None 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" holdings_file = sys.argv[1] if len(sys.argv) > 1 else "holdings.csv"
results = [] results = []
with open(holdings_file) as f: with open(holdings_file) as f:
reader = csv.reader(f) reader = csv.reader(f)
for row in reader: for row in reader:
ticker, market, shares = row[0].strip(), row[1].strip(), int(row[2].strip()) ticker, market, shares = row[0].strip(), row[1].strip(), float(row[2].strip())
currency = get_currency(market)
price = get_price(market, ticker) price = get_price(market, ticker)
if price is None:
results.append({ results.append({
"ticker": ticker, "ticker": ticker,
"market": market, "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, "shares": shares,
"price": price, "price": price,
"total_value": round(price * shares, 2) if price is not None else None "fx_rate": fx,
"total_value_gbp": total_gbp
}) })
print(json.dumps(results, indent=2)) print(json.dumps(results, indent=2))