コードは書かれるよりはるかに多く読まれる
トレーディングのコードベースは何年も生き、書いていない人が、しばしば時間に追われて、あるいは半年後の自分が変更します。クリーンなコードとは、すばやく理解でき、安全に変更できるコードです。スタイル規則の話ではなく、誤った変更を目に見えるようにすることです。名前が悪く、3つのことをし、式の中に上限を隠した関数は、高くつくバグの温床です。たとえば誰もハードコードされていると覚えていないリスク上限です。
# The same calculation, written two ways
def f(d, e, m, w):
r = []
if sum([abs(x) for x in d.values()]) / e > m:
r.append("LEVERAGE")
for k in sorted(d):
if abs(d[k]) / sum([abs(x) for x in d.values()]) > w:
r.append("CONCENTRATION:" + k)
return r
book = {"AAPL": 80_000.0, "MSFT": -10_000.0, "NVDA": 10_000.0}
print(f(book, 50_000.0, 1.5, 0.5)) # works, but what is d? e? m? w?名前、小さな関数、隠れた数値なし
効果の大半は3つの習慣から生まれます。実体どおりに名前を付ける:f ではなく gross_exposure、m ではなく max_leverage。関数を小さく、目的を1つに:各関数は数語で言い表せる仕事を1つだけ行うので、単独でテストでき再利用でき、大きな関数はそれらを呼んで作ります。数値を埋め込まない:式の中に書いた 1.5 のような上限は、誰も安全に見つけたり変更したりできないマジックナンバーです。引数で渡す(または定数に名前を付ける)ようにします。総エクスポージャーを専用の関数で一度だけ計算すれば、繰り返される式もなくなります。
# Small functions with names that say what they return; the limits come in as arguments
def gross_exposure(positions):
return sum(abs(value) for value in positions.values())
def leverage(positions, equity):
return gross_exposure(positions) / equity
def risk_flags(positions, equity, max_leverage, max_weight):
flags = []
if leverage(positions, equity) > max_leverage:
flags.append("LEVERAGE")
gross = gross_exposure(positions)
for symbol in sorted(positions):
if abs(positions[symbol]) / gross > max_weight:
flags.append(f"CONCENTRATION:{symbol}")
return flags
book = {"AAPL": 80_000.0, "MSFT": -10_000.0, "NVDA": 10_000.0}
print(risk_flags(book, 50_000.0, 1.5, 0.5)) # ['LEVERAGE', 'CONCENTRATION:AAPL']
print(risk_flags(book, 50_000.0, 3.0, 0.9)) # [] the same code, looser limits境界を守り、理由を書く
不正な入力は先に処理して、すぐに return か raise します(ガード節)。そうすれば主な流れはネストなしで上から読めます。資本ゼロでの gross / equity は、奥深くからの ZeroDivisionError ではなく明確な ValueError にし、空のポートフォリオはクラッシュではなく 0.0 を返します。コメントは、コードが語れない理由(# 取引所はセント単位で丸める)を説明するもので、コードがすでに示している内容を繰り返してはいけません。docstring は関数が受け取るものと返すものを述べます。最後に純粋関数を好みましょう。引数だけに依存し副作用がないので、少数の入力でテストできます。
# A guard clause stops bad input at the door and says exactly what is wrong
def leverage(positions, equity):
if equity <= 0:
raise ValueError("equity must be positive")
return sum(abs(value) for value in positions.values()) / equity
print(leverage({"AAPL": 60_000.0, "MSFT": -30_000.0}, 45_000.0)) # 2.0
print(leverage({}, 45_000.0)) # 0.0 an empty book is fine
try:
leverage({"AAPL": 60_000.0}, 0.0)
except ValueError as error:
print("rejected:", error) # rejected: equity must be positive# A pure function answers the same way every time; hidden state does not
history = []
def running_notional(price, quantity): # remembers earlier calls: the answer depends on the past
history.append(price * quantity)
return sum(history)
def notional(price, quantity): # depends only on its arguments
return price * quantity
print(running_notional(10.0, 5), running_notional(10.0, 5)) # 50.0 100.0 same call, different answers
print(notional(10.0, 5), notional(10.0, 5)) # 50.0 50.0 always the same- 実体どおりに名前を付け、関数は小さく目的を1つにし、大きな関数はそれらを呼んで作る
- マジックナンバーを使わない。上限は引数か名前付き定数で渡し、繰り返される値は一度だけ計算する
- 不正な入力は明確なエラーで先に防ぎ、コメントは内容でなく理由を書き、テストしやすい純粋関数を好む
次は型ヒントと静的チェックです。コードを実行する前にツールにミスを見つけてもらいます。
ラボ:採点される内容
net_exposureでネットのエクスポージャーを求める25点gross_exposureで全ポジションを合計する25点leverageで分母を守る25点risk_flagsでチェックを組み合わせる25点