What is a dictionary?

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["AAA"]. 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 = {"AAA": 185.40, "CCC": 410.20}  # ticker to price, two names to start

print(prices["AAA"])           # -> 185.4        <- looked up by name, not by position

prices["DDD"] = 128.75         # DDD is not there yet, so this adds it
prices["AAA"] = 186.10         # AAA is there, so this overwrites it

print(prices)                   # -> {'AAA': 186.1, 'CCC': 410.2, 'DDD': 128.75}
print(len(prices))              # -> 3
185.4
{'AAA': 186.1, 'CCC': 410.2, 'DDD': 128.75}
3

.items() hands a loop both halves of each pair at once.

for tkr, px in prices.items():  # each pass gets one key and one value
    print(tkr, px)
# -> AAA 186.1
# -> CCC 410.2
# -> DDD 128.75
AAA 186.1
CCC 410.2
DDD 128.75

The pairs come back in the order I put them in. Overwriting AAA changed its value but not its place.

Step 2. A key that is not there

prices["FFF"] 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("AAA" in prices)         # -> True
print("FFF" in prices)         # -> False

print(prices.get("FFF"))       # -> None       <- no second argument, so the default is None
print(prices.get("FFF", 0.0))  # -> 0.0        <- my own default
print(prices.get("AAA", 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 = {"AAA": 10, "CCC": 4, "DDD": 20}            # keyed by the same tickers

positions = {t: shares[t] * prices[t] for t in shares}  # value of each holding

print({t: round(v, 2) for t, v in positions.items()})
# -> {'AAA': 1861.0, 'CCC': 1640.8, 'DDD': 2575.0}

total = sum(positions.values())                         # the portfolio in one number
print(round(total, 2))                                  # -> 6076.8
{'AAA': 1861.0, 'CCC': 1640.8, 'DDD': 2575.0}
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 = {"AAA": 0.40, "CCC": 0.35, "DDD": 0.25}  # the split I want, adds to 1
capital = 100_000               # the money going in

print(sum(weights.values()))    # -> 1.0

target = {t: int(capital * w / prices[t]) for t, w in weights.items()}
print(target)                   # -> {'AAA': 214, 'CCC': 85, 'DDD': 194}

cost = sum(target[t] * prices[t] for t in target)     # what those share counts cost
print(round(cost, 2))           # -> 99669.9
print(round(capital - cost, 2)) # -> 330.1      <- left in cash by rounding down
1.0
{'AAA': 214, 'CCC': 85, 'DDD': 194}
99669.9
330.1

Your turn

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.

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)                      # -> {'AAA': 0.3996, 'CCC': 0.3498, 'DDD': 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.