 Coffee Space
Coffee Space 

 
I am thinking about the possibility of buying silver and/or gold, and want to explain my thinking here so that others may inspect my rationale.
I have been earning an okay city wage for over a year now, and I have paid off debts I had accumulated whilst studying my PhD (that I will one day finish). I now have money sitting in the bank, and I am coming close to exhausting all investment options that they offer.
Even so, I have several concerns holding money in the bank:
There are many other reasons, but these will suffice for now.
The final objective is to store wealth in a robust way, that could be later be used to make a large purchase, such as a house.
Like any good project, we should define some research questions:
The first thing we can do is grab the historic values for silver and gold (using this website) as 20 year tracking (the largest they provide).
Note: In 2010 there were two January closes, skipping February and onto March. Given the low resolution we treat the data, the of January 2010 was manually set to of February 2010. In the grand scheme of things it makes no difference to the result.
0001 import csv
0002 from datetime import datetime
0003 import math
0004 # Process silver closing prices
0005 history_silver = { "date": [], "close": [] }
0006 with open("silver-and-gold/AGX-USD-2592000-20241014230807.csv") as silver_csv :
0007   silver_reader = csv.reader(silver_csv, delimiter = ",", quotechar= '"')
0008   for row in silver_reader :
0009     if row[0] == "Date" : continue
0010     row[0] = datetime.strptime(row[0], "%H:%M:%S %d-%b-%Y")
0011     for i in range(1, len(row)) :
0012       if len(row[i]) > 0 : row[i] = float(row[i])
0013     history_silver["date"] += [ row[0].year + ((1 / 12.0) * (row[0].month - 1)) ]
0014     history_silver["close"] += [ math.log(row[3], 10) ]
0015 # Process gold closing prices
0016 history_gold = { "date": [], "close": [] }
0017 with open("silver-and-gold/AUX-USD-2592000-20241014230833.csv") as gold_csv :
0018   gold_reader = csv.reader(gold_csv, delimiter = ",", quotechar= '"')
0019   for row in gold_reader :
0020     if row[0] == "Date" : continue
0021     row[0] = datetime.strptime(row[0], "%H:%M:%S %d-%b-%Y")
0022     for i in range(1, len(row)) :
0023       if len(row[i]) > 0 : row[i] = float(row[i])
0024     history_gold["date"] += [ row[0].year + ((1 / 12.0) * (row[0].month - 1)) ]
0025     history_gold["close"] += [ math.log(row[3], 10) ]
0026 # Plot that graph
0027 p = Plott(
0028   title = "Gold and Silver vs USD",
0029   x_title = "Date (as float)",
0030   y_title = "Close (USD, log base 10)",
0031   fg = "#FFF",
0032   bg = "#222"
0033 )
0034 p.plot(history_silver["date"], history_silver["close"], "Silver", colour = "#C0C0C0")
0035 p.plot(history_gold["date"], history_gold["close"], "Gold", colour = "#FFD700")
0036 print(p.to_uri())Now we want a function to be able to retrieve the price of silver or gold given a year or month:
0037 # get_close()
0038 #
0039 # For a given year, get the closing value for a given metal.
0040 #
0041 # @param metal The chosen metal, silver or gold.
0042 # @param year The year to be checked.
0043 # @param month The month to be checked.
0044 # @return The closing value for the given metal, or None if not found.
0045 def get_close(metal = "silver", year = 2024, month = 1) :
0046   close = None
0047   date = year + ((1 / 12.0) * (month - 1))
0048   data = { "date": [], "close": [] }
0049   if metal == "silver" :
0050     data["date"] = history_silver["date"]
0051     data["close"] = history_silver["close"]
0052   elif metal == "gold" :
0053     data["date"] = history_gold["date"]
0054     data["close"] = history_gold["close"]
0055   for i in range(len(data["date"])) :
0056     if data["date"][i] == date :
0057       close = 10 ** data["close"][i]
0058       break
0059   return close
0060 
0061 print("January 2007 gold was " + str(get_close("gold", 2007, 1)))
0062 print("January 2007 silver was " + str(get_close("silver", 2007, 1)))January 2007 gold was 21498.709999999985 January 2007 silver was 449.4699999999999
Manually inspecting the result, this looks good!
For this, I manually had to pull the values from the Royal Mint. I wanted to use their JSON API, but it literally returns HTML! There are two raw files of text I scraped, one for silver and one for gold.
This isn’t perfect, but it’s the easiest data to collect. We parse it like so:
0063 # read_royal_mint()
0064 #
0065 # Read the text manually copied and pasted from the Royal Mint product
0066 # listings. A reasonable attempt is then made to parse the data and pull out
0067 # important information.
0068 #
0069 # @param files The files to be parsed.
0070 # @return The parsed data.
0071 def read_royal_mint(files = []) :
0072   weights = [
0073     " 1/4oz ",
0074     " 1oz ", " 2oz ", " 5oz ", " 10oz ", " 20oz ", " 50oz ", " 100oz ",
0075     " 1g ", " 2g ", " 5g ", " 10g ", " 20g ", " 50g ", " 100g ", " 500g ", " 1kg ",
0076     " Kilo ",
0077     " 50p ", " £5 ",
0078     " $2.5 ", " $5 ", " $10 ", " $20 ",
0079     " Sixpence ", " Dollar ", "Penny", "Quarter", "Half", " Francs ",
0080     "Sovereign", "Coin", "Medal", "Set", "Crown", "Guinea", "Unite", "Mohur", "Shield", "Africa", "Birthday",
0081   ]
0082   data = []
0083   group = { "break": False, "line": 1, "other": [] }
0084   for f in files :
0085     with open(f) as file :
0086       line_no = 1
0087       for line in file :
0088         line = line.strip()
0089         if not group["break"] :
0090           if len(line) <= 0 :
0091             if not "type" in group : group["type"] = "Unknown"
0092             if not "title" in group : group["title"] = "Unknown"
0093             if not "title_full" in group : group["title_full"] = "Unknown"
0094             if not "edition" in group : group["edition"] = "Unknown"
0095             if "price" in group : group.pop("price")
0096             group["break"] = True
0097           elif not "type" in group :
0098             group["type"] = line
0099             if "Silver" in line : group["metal"] = "Silver"
0100             elif "Gold" in line : group["metal"] = "Gold"
0101             else :
0102               #print(f + "::" + str(line_no) + " Unknown metal '" + line + "'")
0103               group["metal"] = "Unknown"
0104           elif not "title" in group :
0105             # Sometimes the type is not provided
0106             if line.startswith(group["type"]) :
0107               group["title_full"] = line
0108               group["title"] = group["type"]
0109               group["metal"] = "Unknown"
0110               group["type"] = "Unknown"
0111               #print(f + "::" + str(line_no) + " Unknown metal (late)")
0112             else :
0113               group["title"] = line
0114           elif not "title_full" in group and line.startswith(group["title"]) :
0115             group["title_full"] = line
0116           elif not "title_full" in group :
0117             group["other"] += [ line ]
0118           else :
0119             group["other"] += [ line ]
0120         else :
0121           if not "price" in group and line.startswith("Price") :
0122             group["price"] = line
0123             group["price_raw"] = float(line.split(" ")[-1].replace(",", "")[1:])
0124           elif "price" in group : # Group collected
0125             group["stock"] = line
0126             # Pull out the weight
0127             msg = group["title"]
0128             if "title_full" in group : msg += " " + group["title_full"]
0129             for weight in weights :
0130               if weight in msg :
0131                 group["weight"] = weight.strip()
0132                 break
0133             if not "weight" in group :
0134               print(f + "::" + str(line_no) + " No weight found: " + str(group))
0135               group["weight"] = "Unknown"
0136             # Store it
0137             data += [ group ]
0138             group = { "break": False, "line": line_no + 1, "other": [] }
0139           else : print(f + "::" + str(line_no) + " Parse failure '" + line + "'")
0140         line_no += 1
0141   return data
0142 
0143 # Process files
0144 royal_mint = read_royal_mint(["silver-and-gold/royal_mint_silver_2024_10_15.txt", "silver-and-gold/royal_mint_gold_2024_10_15.txt"])silver-and-gold/royal_mint_gold_2024_10_15.txt::676 No weight found: {'break': True, 'line': 670, 'other': ['The Great Seals of the Realm - King Henry VIII - 1oz Fine Gold', 'Limited Edition 175'], 'type': 'The Great Seals of the Realm - King Henry VIII - 1oz Fine Gold', 'metal': 'Gold', 'title': 'only 175 available', 'title_full': 'Unknown', 'edition': 'Unknown', 'price': 'Price: £3,500.00', 'price_raw': 3500.0, 'stock': 'In Stock'}
There are some errors and it is not a great parsing, but it will do. Next let’s get some data.
0145 royal_mint_silver = []
0146 royal_mint_gold = []
0147 royal_mint_rejected = []
0148 for r in royal_mint :
0149   if r["metal"] == "Silver" or r["metal"] == "Gold" :
0150     if r["weight"] == "1/4oz" :
0151       r["weight_kg"] = 0.02834952 * 0.25
0152     elif r["weight"].endswith("oz") :
0153       # NOTE: Calculation is in troy ounces!
0154       r["weight_kg"] = float(r["weight"][:-2]) * 0.0311034768
0155     elif r["weight"] == "Kilo" :
0156       r["weight_kg"] = 1
0157     elif r["weight"].endswith("kg") :
0158       r["weight_kg"] = float(r["weight"][:-2])
0159     elif r["weight"].endswith("g") :
0160       r["weight_kg"] = float(r["weight"][:-1]) * 0.001
0161     if not "weight_kg" in r : royal_mint_rejected += [ r ]
0162     else :
0163       if r["metal"] == "Silver" : royal_mint_silver += [ r ]
0164       elif r["metal"] == "Gold" : royal_mint_gold += [ r ]
0165       else : royal_mint_rejected += [ r ]
0166   else : royal_mint_rejected += [ r ]
0167 
0168 print("Useful data: " + str(
0169   100.0 * (
0170   (len(royal_mint_silver) + len(royal_mint_gold)) /
0171   (len(royal_mint_silver) + len(royal_mint_gold) + len(royal_mint_rejected))
0172   )) + "%")Useful data: 49.72067039106145%
Near enough 50% is usable, that’ll do. Let’s process this data:
0173 rm_silver_cost = [] 0174 rm_silver_weight = [] 0175 rm_gold_cost = [] 0176 rm_gold_weight = [] 0177 for r in royal_mint_silver : 0178 rm_silver_cost += [ math.log(r["price_raw"], 10) ] 0179 rm_silver_weight += [ math.log(r["weight_kg"], 10) ] 0180 for r in royal_mint_gold : 0181 rm_gold_cost += [ math.log(r["price_raw"], 10) ] 0182 rm_gold_weight += [ math.log(r["weight_kg"], 10) ] 0183 # Plot that graph 0184 p = Plott( 0185 title = "Gold and Silver: Weight per Cost", 0186 x_title = "Cost (log base 10 GBP)", 0187 y_title = "Weight (log base 10 kg)", 0188 fg = "#FFF", 0189 bg = "#222" 0190 ) 0191 p.plot(rm_silver_cost, rm_silver_weight, "Silver", colour = "#C0C0C0") 0192 p.plot(rm_gold_cost, rm_gold_weight, "Gold", colour = "#FFD700") 0193 print(p.to_uri())
To get good value for money, you want the largest weight possible with the lowest cost possible - or put more simply, the best weight per cost ratio. Let’s see it:
0194 def sort_key_kg_gbp(e) :
0195   return e["weight_kg"] / e["price_raw"]
0196 
0197 royal_mint_silver.sort(key = sort_key_kg_gbp, reverse = True)
0198 royal_mint_gold.sort(key = sort_key_kg_gbp, reverse = True)
0199 
0200 print("Weight\tCost\tg/GBP\tTitle")
0201 for r in (royal_mint_silver[:5] + royal_mint_gold[:5]) :
0202   print(
0203     str(round(r["weight_kg"], 3)) + "kg\t£" +
0204     str(int(r["price_raw"])) + "\t" +
0205     str(round(r["weight_kg"] * 1000 / r["price_raw"], 5)) + "\t" +
0206     r["title"]
0207   )Weight Cost g/GBP Title 0.311kg £273 1.13616 The Royal Tudor Beasts 2024 Tudor Dragon Silver 10oz Bullion Coin 0.311kg £273 1.13616 The Royal Tudor Beasts 2024 Seymour Unicorn 10oz Silver Bullion Coin 0.311kg £273 1.13616 Little John 2024 10oz Silver Bullion Coin 0.031kg £28 1.10413 Best Value 1oz Silver Bullion Coin 0.031kg £28 1.10413 The Best Value Britannia 1oz Silver Bullion Coin 1.0kg £66906 0.01495 1kg Gold Bullion Cast Bar 0.5kg £33518 0.01492 500g Gold Bullion Cast Bar 0.031kg £2085 0.01492 Best Value Gold 1oz Bullion Coin 0.031kg £2117 0.01469 Britannia 2025 1oz Gold Bullion Coin 0.031kg £2117 0.01469 Britannia 2024 1oz Gold Bullion Coin
This was a useful exercise! I realised the following:
These values overlayed on the original graph:
0208 rm_best_silver_cost = [] 0209 rm_best_silver_weight = [] 0210 rm_best_gold_cost = [] 0211 rm_best_gold_weight = [] 0212 for r in royal_mint_silver[:5] : 0213 rm_best_silver_cost += [ math.log(r["price_raw"], 10) ] 0214 rm_best_silver_weight += [ math.log(r["weight_kg"], 10) ] 0215 for r in royal_mint_gold[:5] : 0216 rm_best_gold_cost += [ math.log(r["price_raw"], 10) ] 0217 rm_best_gold_weight += [ math.log(r["weight_kg"], 10) ] 0218 p.plot(rm_best_silver_cost, rm_best_silver_weight, "Silver (Best)", colour = "#FF0088") 0219 p.plot(rm_best_gold_cost, rm_best_gold_weight, "Gold (Best)", colour = "#00CC00") 0220 print(p.to_uri())
And we can see we have correctly pulled out the most top left values quite well.
An interesting fact is that with gold you don’t pay VAT, and is CGT exempt on coins, whereas with silver you pay VAT, but don’t pay CGT. Let us write a function to allow us to predict the potential gains (or losses) given some simulations:
0221 vat = 0.2 # 20%
0222 cgt = 0.28 # 28%
0223 cgt_limit = 12300
0224 
0225 # sim_buy_sell()
0226 #
0227 # Simulate buying and selling of precious metals using historic data.
0228 #
0229 # @param metal The precious metal being traded, silver or gold.
0230 # @param kg The amount of precious metal being traded.
0231 # @param buy_year The buy-in year.
0232 # @param buy_month The buy-in month.
0233 # @param sell_year The sell-out year.
0234 # @param sell_month The sell-out month.
0235 # @return Key metrics, including the buy-in price, the sell-out price, the
0236 # costs in trading (if it applies) and the profist/loss difference made
0237 # overall.
0238 def sim_buy_sell(metal = "silver", kg = 1, buy_year = 2024, buy_month = 1, sell_year = 2024, sell_month = 1) :
0239   buy_market = get_close(metal, buy_year, buy_month)
0240   sell_market = get_close(metal, sell_year, sell_month)
0241   ratio_market = sell_market / buy_market
0242   costs = 0
0243   buy_price = 0
0244   if metal == "silver" :
0245     buy_price = (royal_mint_silver[0]["price_raw"] / royal_mint_silver[0]["weight_kg"]) * kg
0246     costs += buy_price * vat
0247   elif metal == "gold" :
0248     buy_price = (royal_mint_gold[0]["price_raw"] / royal_mint_gold[0]["weight_kg"]) * kg
0249   # NOTE: We avoid CGT for both as coins.
0250   sell_price = ratio_market * buy_price
0251   return { "buy": buy_price, "sell": sell_price, "diff": sell_price - (buy_price + costs), "costs": costs }
0252 
0253 print(sim_buy_sell("silver", 1, 2007, 1, 2024, 1))
0254 print(sim_buy_sell("gold", 0.01325, 2007, 1, 2024, 1)){'buy': 880.1588380627595, 'sell': 1474.829530084248, 'diff': 418.63892440893665, 'costs': 176.0317676125519}
{'buy': 886.5088725, 'sell': 2669.2656048860813, 'diff': 1782.7567323860812, 'costs': 0}
We assume that we buy in at ~£880 in January 2007 and sell January 2024. I roughly balanced the two metals buy value (not considering tax). We see that our return on silver is ~50%, but our return on gold is ~200%. Of that profit, as it is CGT exempt, it is non-taxable too!
Possibly more useful is being able to define an investment
gbp, i.e. an investment available, which would account for
any payable taxes.
Normally tax is calculated at (where (20%)):
Instead we know how much we want our total investment to be, so we calculate as follows:
0255 # sim_invest()
0256 #
0257 # Similar to sim_buy_sell(), except we set an investment amount rather than
0258 # weight. We then simulate buying and selling of precious metals using historic
0259 # data.
0260 #
0261 # @param metal The precious metal being traded, silver or gold.
0262 # @param gbp The total investment, including incurred costs.
0263 # @param buy_year The buy-in year.
0264 # @param buy_month The buy-in month.
0265 # @param sell_year The sell-out year.
0266 # @param sell_month The sell-out month.
0267 # @return Key metrics, including the buy-in price, the sell-out price, the
0268 # costs in trading (if it applies) and the profist/loss difference made
0269 # overall.
0270 def sim_invest(metal = "silver", gbp = 1000, buy_year = 2024, buy_month = 1, sell_year = 2024, sell_month = 1) :
0271   kg = 0
0272   if metal == "silver" :
0273     kg = (royal_mint_silver[0]["weight_kg"] / royal_mint_silver[0]["price_raw"]) * gbp
0274     kg = kg / (1.0 + vat)
0275   elif metal == "gold" :
0276     kg = (royal_mint_gold[0]["weight_kg"] / royal_mint_gold[0]["price_raw"]) * gbp
0277   return sim_buy_sell(metal, kg, buy_year, buy_month, sell_year, sell_month)
0278 
0279 print(sim_invest("silver", 1000, 2007, 1, 2024, 1))
0280 print(sim_invest("gold", 1000, 2007, 1, 2024, 1)){'buy': 833.3333333333333, 'sell': 1396.3668320466318, 'diff': 396.3668320466319, 'costs': 166.66666666666666}
{'buy': 1000.0, 'sell': 3010.9857754255936, 'diff': 2010.9857754255936, 'costs': 0}
We see gold has a buy-in of £1000, whereas silver has a buy-in of ~£833 + ~£167 of costs for a total of £1000. Great!
The market data goes back to February 2005, and up to October 2024. We can make some predictions about different holding times based on historic data.
Additionally, let’s consider how the money may fair in the bank with some simplistic flat interest returns:
Note: Money made over a CGT threshold in interest is subject to CGT (capital gains tax), especially these high-interest accounts where stocks and investments are concerned.
Let’s write a method to plot multi-year holdings and simulate!
0281 # plot_hold()
0282 #
0283 # A helper function for plotting the outcome of holding onto precious metals
0284 # over time given historic data.
0285 #
0286 # @param beg_year The year to run the simulations from.
0287 # @param beg_month The month to run the simulations from.
0288 # @param end_year The year to run the simulations to.
0289 # @param end_month The month to run the simulations to.
0290 # @param hold_years The number of years to hold the precious metal before selling.
0291 # @param bank_interest Compare against leaving money in the bank at different rates.
0292 def plot_hold(beg_year = 2005, beg_month = 2, end_year = 2024, end_month = 10, hold_years = 2, bank_interest = [ 0.005, 0.01, 0.02, 0.05 ]) :
0293   investment = 1000
0294   silver_x = []
0295   silver_y = []
0296   gold_x = []
0297   gold_y = []
0298   zero_y = []
0299   # Calculate metals investment
0300   for year in range(beg_year, end_year + 1 - hold_years) :
0301     for month in range(1, 12 + 1) :
0302       if beg_year == year and beg_month > month : continue
0303       if end_year - hold_years == year and end_month < month : break
0304       x = year + ((1.0 / 12) * (month - 1))
0305       silver_predict = sim_invest("silver", investment, year, month, year + hold_years, month)
0306       silver_x += [ x ]
0307       if silver_predict["diff"] >= 0 : silver_y += [ math.log(silver_predict["diff"], 10) ]
0308       elif silver_predict["diff"] == 0 : silver_y += [ silver_predict["diff"] ]
0309       else : silver_y += [ -math.log(-silver_predict["diff"], 10) ]
0310       gold_predict = sim_invest("gold", investment, year, month, year + hold_years, month)
0311       gold_x += [ x ]
0312       if gold_predict["diff"] >= 0 : gold_y += [ math.log(gold_predict["diff"], 10) ]
0313       elif gold_predict["diff"] == 0 : gold_y += [ gold_predict["diff"] ]
0314       else : gold_y += [ -math.log(-gold_predict["diff"], 10) ]
0315   zero_y = [ 0 ] * len(silver_x)
0316   bank = []
0317   # Show equivalent simplisitc bank investment
0318   for interest in bank_interest :
0319     i = investment
0320     for year in range(hold_years) :
0321       i *= 1.0 + interest
0322     profit = i - investment
0323     profit -= profit * cgt # Apply CGT (capital gains tax)
0324     bank += [ [ math.log(profit, 10) ] * len(silver_x) ]
0325   # Plot that data
0326   p = Plott(
0327     title = "Gold and Silver: " + str(hold_years) + " Year Hold Historic Predictions",
0328     x_title = "Buy Year",
0329     y_title = "Profit (log base 10 GBP)",
0330     fg = "#FFF",
0331     bg = "#222"
0332   )
0333   p.plot(silver_x, zero_y, "Zero line", colour = "#FF0000")
0334   p.plot(silver_x, silver_y, "Silver", colour = "#C0C0C0")
0335   p.plot(gold_x, gold_y, "Gold", colour = "#FFD700")
0336   for z in range(len(bank)) :
0337     p.plot(silver_x, bank[z], "Bank flat interest " + str(round(bank_interest[z] * 100, 2)) + "%")
0338   print(p.to_uri())
0339   return
Now let’s take a look!
0340 plot_hold(hold_years = 1)
Holding onto silver or gold for just a year does not seem wise. Unless you are lucky at you buy-in, you could very easily make a loss after holding for just a year. Why is this important? It means you are unlikely confidently hold for just a year and be able to liquidate your investment if you need it.
Over just a year, holding silver or gold could even be a worse investment than considering a fixed-interest investment with the bank.
0341 plot_hold(hold_years = 2)
With a two year hold on silver and gold it becomes clearer that gold falls last and recovers first. Events where silver outperforms gold exist, but are rare. You could be left holding silver or gold for a long time, which may represent significantly less value than banking the money.
0342 plot_hold(hold_years = 5)
When holding for 5 years, it seems clear that there could be large periods where you are unable to sell your investment without making a significant loss. Gold is begins to display significantly greater stability too. If you find yourself holding during a recession, you may be holding for a long time, especially if the buy-in was wrong.
Interestingly, the best thing you could probably do is double-down on your position and buy the metals at the new lower price.
0343 plot_hold(hold_years = 10)
Holding gold over a period of 10 years is far more stable in gold than silver. Gold represents nearly as good an investment as a fixed high interest account.
0344 plot_hold(hold_years = 15)
Holding gold for longer than 15 years is almost always a better investment than a high interest account. Bare in mind that this doesn’t account for risk, something that our historic simulations attempt to account for!
Let us consider each of the original research questions…
- Which precious metal realises a greater return on investment, silver or gold?
Other than some weird short-term edge-cases, it appears that gold is consistently a better investment than silver, especially when holding for two years or more.
- How historically volatile is each metal, when compared to a fixed-interest rate with a bank?
We have ultimately made a massive assumption with the interest rates that banks can provide, and yet have a relatively realistic expectation of how volatile silver and gold can be expected to be.
- How long may each metal need to be held to safely realise a profit on investment?
It really depends on the buy-in market. With a 5 year hold the market is relatively stable, but this position could translate to a 10 year or 15 year hold to realise full gains.
If I do invest in silver and gold, it will be a case of watching the stock market and investing at a good price. Ideally I would wait for a drop in the price of precious metals.