Map a key to a value, look it up by name, and use two dictionaries keyed by ticker to value a portfolio.
A dictionary maps a key to a value. I write it as {key: value, key: value}, and I read a value back by its key: prices["AAPL"]. A list finds things by position, a dictionary finds them by name.
In this lesson I build a dictionary of ticker to price, add and overwrite entries, loop over the pairs, and handle a key that is not there. Then I put two dictionaries side by side to value a portfolio and to work out how many shares to buy.
Step 1. Ticker to price
The key is the ticker, the value is the price. Assigning to a key that already exists overwrites it. Assigning to a key that does not exist adds it. The syntax is the same for both.
prices = {"AAPL": 185.40, "MSFT": 410.20}print(prices["AAPL"]) # -> 185.4 <- looked up by name, not by positionprices["NVDA"] =128.75# NVDA is not there yet, so this adds itprices["AAPL"] =186.10# AAPL is there, so this overwrites itprint(prices) # -> {'AAPL': 186.1, 'MSFT': 410.2, 'NVDA': 128.75}print(len(prices)) # -> 3
.items() hands a loop both halves of each pair at once.
for tkr, px in prices.items():print(tkr, px)# -> AAPL 186.1# -> MSFT 410.2# -> NVDA 128.75
AAPL 186.1
MSFT 410.2
NVDA 128.75
The pairs come back in the order I put them in. Overwriting AAPL changed its value but not its place.
Step 2. A key that is not there
prices["TSLA"] raises a KeyError and stops the program. in tests for a key without reading it, and .get() returns a value I choose instead of raising.
print("AAPL"in prices) # -> Trueprint("TSLA"in prices) # -> Falseprint(prices.get("TSLA")) # -> None <- no second argument, so the default is Noneprint(prices.get("TSLA", 0.0)) # -> 0.0 <- my own defaultprint(prices.get("AAPL", 0.0)) # -> 186.1 <- the key exists, so the default is ignored
True
False
None
0.0
186.1
.get() never raises. A missing ticker becomes 0.0, so I use .get() when a default is what I want, and prices[t] when I want the error.
Step 3. Two dictionaries, one portfolio
A second dictionary holds the share counts, keyed by the same tickers. Because both are keyed by ticker, t picks the matching price and the matching share count with no lining up by hand.
shares = {"AAPL": 10, "MSFT": 4, "NVDA": 20}positions = {t: shares[t] * prices[t] for t in shares}print({t: round(v, 2) for t, v in positions.items()})# -> {'AAPL': 1861.0, 'MSFT': 1640.8, 'NVDA': 2575.0}total =sum(positions.values())print(round(total, 2)) # -> 6076.8
{t: ... for t in shares} is a dictionary comprehension: it builds a new dictionary by looping, one key and one value per pass. .values() gives the values on their own for sum().
Step 4. From weights to share counts
Given target weights and an amount of capital, the money for each name is capital * weight, and the share count is that money divided by the price. int() drops the fraction, since I cannot buy part of a share.
weights = {"AAPL": 0.40, "MSFT": 0.35, "NVDA": 0.25}capital =100_000print(sum(weights.values())) # -> 1.0target = {t: int(capital * w / prices[t]) for t, w in weights.items()}print(target) # -> {'AAPL': 214, 'MSFT': 85, 'NVDA': 194}cost =sum(target[t] * prices[t] for t in target)print(round(cost, 2)) # -> 99669.9print(round(capital - cost, 2)) # -> 330.1 <- left in cash by rounding down
Using target and prices, build a dictionary of each holding’s share of the money actually invested, rounded to four decimals. Check that the rounded weights add to 1.
TipShow answer
values = {t: n * prices[t] for t, n in target.items()}invested =sum(values.values())actual = {t: round(v / invested, 4) for t, v in values.items()}print(actual) # -> {'AAPL': 0.3996, 'MSFT': 0.3498, 'NVDA': 0.2506}print(round(sum(actual.values()), 4)) # -> 1.0
The gap from 0.40 / 0.35 / 0.25 comes from rounding each share count down to a whole number.