代码被阅读的次数远多于被编写的次数
交易代码库会存活多年,并被没写过它的人修改,常常在压力之下,也常常是半年后的你自己。整洁代码是能被快速读懂、能被安全修改的代码。它不是风格规则,而是让错误的修改变得显眼。一个命名糟糕、一次做三件事、把限额藏在公式里的函数,正是昂贵 bug 的藏身之处,比如没人记得被写死的风险限额。
# 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?名称、小函数、没有隐藏的数字
三个习惯完成了大部分工作。名称要名副其实:用 gross_exposure 而不是 f,用 max_leverage 而不是 m。函数要小且单一职责:每个函数做一件几个词就能说清的事,因此可以单独测试和复用,更大的函数通过调用它们构建。不要埋藏数字:写在公式里的 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守住边界并说明原因
先处理不良输入,并立即返回或抛出异常(卫语句),这样主路径就能不加嵌套地从上读到下。资本为零时的杠杆 gross / equity 应当是清晰的 ValueError,而不是从深处冒出的 ZeroDivisionError;空的投资组合应返回 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整洁不等于聪明不要为了显得聪明而压缩代码:大家都看得懂的三行循环,胜过需要注释的一行技巧。也不要拆成几十个微小函数;当函数读起来像一个清晰的步骤时就停下。整理代码与改变行为是两种不同的修改:在测试前后都通过的情况下重构,这样看到的任何差异都来自新逻辑,而不是整理。
- 名称要名副其实,函数要小而单一职责,更大的函数通过调用它们来构建。
- 不用魔法数字:限额作为参数或命名常量传入;重复的值只计算一次。
- 先用清晰的错误挡住不良输入,注释写为什么而非是什么,并优先使用可测试的纯函数。
下一节:类型提示与静态检查,让工具在代码运行之前发现错误。
实验:评分内容
- 用
net_exposure计算净敞口25 分 - 用
gross_exposure汇总每个头寸25 分 - 在
leverage中保护分母25 分 - 在
risk_flags中组合各项检查25 分