如果我告诉你,有一个不到 300 行的 Python 脚本,能让一个虚拟经济体从繁荣、过热、滞涨走到全面崩盘,你信不信?更“反直觉”的是,这个崩溃并不是程序 bug,而是设计者故意埋进去的核心规则。
这篇文章就来拆解这样一个项目:设计一个从一开始就注定崩溃的经济系统。我们会用代理建模(Agent-Based Modeling,ABM)的方式,在一个沙盒环境里塞入居民、信贷、资产泡沫、债务清算和信心传导机制,然后观察它如何自己把自己玩崩。
适合人群有三类:一是想用 Python 练习复杂系统模拟的开发者;二是对经济学模型、量化风控感兴趣的后端工程师;三是想理解“信贷扩张—资产泡沫—债务通缩”这个链条的程序员。读完你会得到一份完整可运行的模拟代码,以及一套分析崩溃路径的方法论。
1. 为什么要设计一个“注定崩溃”的经济系统
1.1 这不是制造恐慌,而是研究脆弱性
大多数经济模拟项目,目标都是“维持增长”“避免崩溃”。而这个项目的设计目标反过来了——我们希望崩溃必然发生,并且发生在模型内部逻辑驱动下,而不是靠外部随机事件硬砸出来。
这种“反向设计”的价值在于:当你把崩溃当作系统的一个正常输出,你反而能更清楚地看到,哪些参数、哪些规则是导致不稳定的根源。比如杠杆率过高、资产价格只涨不跌的预期、债务约束失效、流动性枯竭,这些因素在真实世界里往往被各种噪音掩盖,但在一个干净的模拟环境里会被放大得非常明显。
1.2 和普通经济模拟的区别
普通经济模拟通常追求“长期均衡”,比如:
- 产量围绕某个稳态波动;
- 价格回归价值;
- 政策干预有效果;
- 系统存在负反馈。
而“注定崩溃”的模拟不一样,它要构造一个正反馈占主导的系统:
- 价格上涨 → 借贷增加 → 买入增加 → 价格继续上涨;
- 价格下跌 → 抵押物缩水 → 强制卖出 → 价格继续下跌。
正反馈系统一旦启动,就很容易走向极端。我们要做的就是让这个“走向极端”的过程可复现、可观察、可分析。
1.3 现实世界中的“崩溃配方”
真实的金融危机虽然复杂,但核心配方其实是高度相似的:
- 信贷扩张让一部分人加杠杆购买资产;
- 资产价格上升,反过来提高抵押物价值,允许更多借贷;
- 价格短期不跌,让市场参与者默认“价格永远涨”;
- 有一天新增信贷跟不上价格增长,价格滞涨;
- 信心松动,部分人抛售,价格下跌;
- 杠杆者被强制清算,导致更大抛压;
- 违约率上升,银行收紧信贷,流动性消失;
- 消费和投资同时收缩,经济全面下行。
下面我们要做的模拟,就是把上面这 8 步浓缩成 200 行左右的 Python 代码。
2. 崩溃引擎:四个核心机制
在写代码之前,先把“崩溃怎么被设计出来”这件事拆成四个可实现的机制。每个机制对应模拟里的一个具体函数。
2.1 用信贷放大购买力
如果所有代理只能用存量现金买资产,那么资产价格最多是现金的重新分配,很难形成泡沫。所以我们要加入一个“银行”,允许代理以当前资产价值为抵押去借钱。杠杆的存在让代理的购买力远高于自身收入,这是泡沫的燃料。
2.2 资产价格自我强化
资产价格不取决于“内在价值”,而取决于本期净买盘的大小:
- 买盘 > 卖盘,价格上涨;
- 买盘 < 卖盘,价格下跌。
因为借贷会带来买盘,价格上涨又会提高抵押物价值,从而带来更多借贷,所以价格和买盘之间形成了正反馈。
2.3 债务约束与强制卖出
单纯借钱不能保证崩溃,必须有一个“清算规则”。我们规定:
- 每个代理的负债不能超过
贷款比例上限 × 资产市值; - 一旦超标,必须按“超出部分”卖出资产还债。
这条规则平时不触发,但一旦价格下跌,资产市值缩水,很多代理会同时触发强制卖出,形成踩踏。
2.4 信心指数与流动性螺旋
前面三个机制是“硬规则”,第四个是“软规则”。我们定义信心指数,它受两个因素影响:
- 资产价格增长率;
- 当期违约人数。
信心高时,代理更愿意借钱;信心低时,即使有额度也不愿意借。信心下降会导致买盘消失,于是价格涨不动,信心进一步下降,这就形成了流动性螺旋。
这四个机制拼起来,就是一个完整的“内生崩溃机器”。
3. 环境准备与数据结构
3.1 开发环境
本文代码使用 Python 3.9 及以上版本即可,不需要复杂依赖。核心逻辑只用标准库,可视化部分依赖matplotlib。如果你只是想看文字结果,不装matplotlib也能跑。
# 建议创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate # 安装可视化依赖(可选) pip install matplotlib注意:版本不需要追求最新,matplotlib 3.5+均能运行本文代码。
3.2 数据结构说明
整个模拟有两大类对象:
Agent:经济中的个体,也就是“人”,属性包括现金、资产数量、负债、工资、消费倾向、生存状态。Economy:整个虚拟经济系统,属性包括代理列表、资产价格、利率、贷款比例上限、信心指数、历史数据。
我们不需要单独设计“银行”类,而是把放贷行为写成Economy的方法,这样代码更紧凑,也方便观察整个信贷池的变化。
4. 从零实现一个三主体经济模拟
4.1 完整代码
我把完整代码放在一个文件里,命名为crash_economy.py。你直接复制保存,运行后就能得到完整的崩溃过程输出。
# file: crash_economy.py import random import math from dataclasses import dataclass @dataclass class Agent: """一个经济代理:可以持有现金、资产,也可以负债。""" pid: int cash: float asset_qty: float debt: float salary: float = 40.0 consume_propensity: float = 0.85 alive: bool = True def net_worth(self, price: float) -> float: return self.cash + self.asset_qty * price - self.debt class Economy: def __init__( self, n_agents: int = 200, init_cash: float = 100.0, init_asset: float = 10.0, price: float = 10.0, interest_rate: float = 0.08, max_loan_ratio: float = 0.8, seed: int = 42, ): random.seed(seed) self.n_agents = n_agents self.agents = [ Agent( pid=i, cash=init_cash, asset_qty=init_asset, debt=0.0, salary=random.uniform(30, 50), ) for i in range(n_agents) ] self.price = price self.interest_rate = interest_rate self.max_loan_ratio = max_loan_ratio self.confidence = 1.0 self.time = 0 self.history = [] self.default_count = 0 def alive_agents(self): return [a for a in self.agents if a.alive] def distribute_salary(self): """每个存活代理领取工资,相当于经济体注入基础货币。""" for a in self.agents: if a.alive: a.cash += a.salary def consume(self) -> float: """消费:现金减少,货币退出流通,同时形成需求热度。""" total_demand = 0.0 for a in self.agents: if a.alive: spend = min(a.cash * a.consume_propensity, 50.0) a.cash -= spend total_demand += spend return total_demand def borrow_and_invest(self) -> float: """ 乐观时借钱买资产。 这是泡沫阶段的主要买盘来源。 """ buy_orders = 0.0 for a in self.agents: if not a.alive: continue asset_value = a.asset_qty * self.price existing_debt_ratio = a.debt / asset_value if asset_value > 0 else 0 if self.confidence > 0.7 and existing_debt_ratio < self.max_loan_ratio: # 可借额度 = 资产价值 * 上限 - 已有负债 borrow = asset_value * self.max_loan_ratio - a.debt borrow = min(borrow, a.salary * 5) a.debt += borrow a.cash += borrow # 将一部分借款用于购买资产 invest = borrow * random.uniform(0.5, 1.0) a.cash -= invest a.asset_qty += invest / self.price buy_orders += invest return buy_orders def forced_liquidation(self) -> float: """ 负债超过上限时必须卖出资产。 这是崩溃阶段的主要卖盘来源。 """ sell_orders = 0.0 for a in self.agents: if not a.alive: continue asset_value = a.asset_qty * self.price if asset_value > 0 and a.debt / asset_value > self.max_loan_ratio: # 超过上限的部分,需要卖掉对应价值的资产 excess = a.debt / self.price - a.asset_qty * self.max_loan_ratio sell_asset = max(0.0, excess) sell_asset = min(sell_asset, a.asset_qty) a.asset_qty -= sell_asset a.cash += sell_asset * self.price sell_orders += sell_asset * self.price return sell_orders def update_price(self, buy_orders: float, sell_orders: float): """价格由净买盘和信心共同推动。""" alive_num = max(1, len(self.alive_agents())) net_flow = buy_orders - sell_orders flow_effect = 0.05 * math.tanh(net_flow / (alive_num * 10)) confidence_effect = 0.02 * (self.confidence - 0.5) self.price *= 1.0 + flow_effect + confidence_effect self.price = max(0.5, self.price) def pay_interest(self): """支付利息;付不起时债务会继续累积,形成被动杠杆。""" for a in self.agents: if a.alive: interest = a.debt * self.interest_rate / 12.0 if a.cash >= interest: a.cash -= interest else: a.debt += interest * 0.5 def default_check(self) -> int: """ 当现金 + 资产市值 小于 负债的一半时,代理违约退场。 违约会直接造成银行坏账,压缩市场流动性。 """ defaults = 0 for a in self.agents: if not a.alive: continue asset_value = a.asset_qty * self.price if a.cash + asset_value < a.debt * 0.5: a.alive = False defaults += 1 self.default_count = defaults return defaults def update_confidence(self, default_rate: float): """ 信心由价格增长率和违约率共同决定。 价格涨得快 → 信心高 → 加杠杆; 价格滞涨或下跌 → 信心快速回落 → 买盘消失。 """ if self.history: last_price = self.history[-1]["price"] price_growth = self.price / last_price - 1 else: price_growth = 0.05 target = 0.6 + price_growth * 8.0 - default_rate * 6.0 target = max(0.05, min(1.0, target)) # 信心下降很快,恢复很慢 if target < self.confidence: self.confidence = target else: self.confidence += (target - self.confidence) * 0.1 def step(self): self.time += 1 self.distribute_salary() demand = self.consume() buy = self.borrow_and_invest() sell = self.forced_liquidation() self.update_price(buy, sell + demand * 0.1) self.pay_interest() defaults = self.default_check() alive = self.alive_agents() default_rate = defaults / max(1, len(alive)) self.update_confidence(default_rate) total_net_worth = sum(a.net_worth(self.price) for a in alive) total_debt = sum(a.debt for a in alive) total_cash = sum(a.cash for a in alive) self.history.append( { "time": self.time, "price": self.price, "confidence": self.confidence, "defaults": defaults, "alive": len(alive), "total_net_worth": total_net_worth, "total_debt": total_debt, "demand": demand, } ) def run(self, steps: int = 240): for _ in range(steps): self.step() if len(self.alive_agents()) <= 5: print( f"[提前终止] 第 {self.time} 期存活代理只剩 " f"{len(self.alive_agents())} 个" ) break return self.history if __name__ == "__main__": eco = Economy(seed=42) history = eco.run(steps=240) print("time,price,confidence,defaults,alive,total_debt,total_net_worth") for h in history: print( f"{h['time']},{h['price']:.2f},{h['confidence']:.3f}," f"{h['defaults']},{h['alive']},{h['total_debt']:.2f}," f"{h['total_net_worth']:.2f}" ) # 可选的可视化输出 try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt xs = [h["time"] for h in history] fig, axes = plt.subplots(2, 1, figsize=(10, 8)) axes[0].plot(xs, [h["price"] for h in history], label="Asset Price") axes[0].set_ylabel("Price") axes[0].legend() axes[1].plot(xs, [h["confidence"] for h in history], label="Confidence", color="red") axes[1].set_ylabel("Confidence") axes[1].set_xlabel("Time") axes[1].legend() plt.tight_layout() plt.savefig("economy_crash.png", dpi=120) print("图表已保存: economy_crash.png") except ImportError: print("未安装 matplotlib,跳过图表输出")4.2 代码逻辑逐段拆解
先看Agent这个数据类。它承担了最小粒度的状态管理:现金、资产、负债、工资、消费倾向、是否存活。net_worth()方法把资产按照当前市场价折算成净值,这是后面判断破产的主要依据。
然后是Economy类。初始化时,我们生成 200 个代理,每个代理初始有 100 元现金、10 单位资产、0 负债。资产初始价格是 10,贷款比例上限是 0.8,年利率 8%。
每一期的执行顺序是固定的,这也是模拟的关键:
distribute_salary():发工资,注入流动性;consume():消费,货币退出流通;borrow_and_invest():信心高时借钱买资产;forced_liquidation():负债超标时强制卖资产;update_price():根据净买盘和信心更新价格;pay_interest():付利息;default_check():判断违约;update_confidence():更新信心。
顺序为什么重要?因为“先注入流动性,再决定消费和借贷”,保证了每一轮的状态更新都基于一致的时间点。如果顺序乱掉,比如先强平再发工资,会出现跨期套利,破坏模拟逻辑。
borrow_and_invest里的可借额度公式是:
可借额度 = 资产市值 × 贷款比例上限 - 已有负债这个公式很关键,它让“资产涨价”和“借钱能力”绑定在一起。价格越高,可借额度越大;可借额度越大,买盘越强;买盘越强,价格越高。这就是正反馈的核心循环。
forced_liquidation里的“超额部分”计算:
需要卖出的资产数量 = 负债 / 当前价格 - 持有资产数量 × 贷款比例上限这里要注意,负债是货币单位,资产是数量单位,必须除以价格换算成数量。这是新手最容易写错的地方,后面会专门讲。
update_price没有让价格等于某种“均衡值”,而是用“净买盘”驱动价格变化。tanh函数让价格对买盘的反应有一个饱和效应,避免单期暴涨暴跌太离谱。
update_confidence是崩溃的“扳机”。价格增长率为正时信心高,大家继续借钱;价格一旦滞涨,信心目标值立刻下降,信心下降后又会让价格缺乏买盘支撑,于是进入下跌螺旋。
5. 运行结果与崩溃过程分析
5.1 运行方式
在终端执行:
python crash_economy.py如果安装了 matplotlib,会生成一张包含资产价格和信心指数两条曲线的economy_crash.png图;如果没有安装,也能从终端输出的 CSV 样式数据里看到变化趋势。
5.2 典型阶段特征
由于随机种子固定为seed=42,理论上每次运行结果完全一致。但如果你修改了代理数量、贷款比例上限等参数,具体期数会变化。总体来看,你会看到以下四个阶段:
| 阶段 | 表现 | 核心驱动 |
|---|---|---|
| 繁荣期 | 价格快速上涨,总债务和总净值同步上升,违约数为 0 | 信贷扩张 + 买盘正反馈 |
| 滞涨期 | 价格涨幅收窄,信心开始小幅下滑,部分代理借款空间耗尽 | 可借额度见顶 |
| 抛售期 | 价格转跌,强制卖出出现,信心快速下降 | 抵押物缩水 + 强平 |
| 崩盘期 | 价格深度下跌,违约集中爆发,大量代理“死亡” | 债务通缩 + 违约传染 |
这不是一个“可能”出现的路径,而是规则设计下必然出现的路径。原因很简单:膨胀依赖持续新增杠杆,而杠杆不可能无限增长,因为代理的资产有限、工资有限。一旦新增借款放缓,买盘跟不上供给,价格滞涨;信心模型会让滞涨演变成下跌;下跌又触发强平和违约。
5.3 崩溃为什么会发生
我们可以在代码层面把崩溃归因到三个“不平衡”:
- 债务累积速度 > 收入增长能力。代理的工资是 30 到 50,但单次借款可以到工资的 5 倍。初期这种“超前购买”能拉高资产价格,但债务本身不会消失,它会通过利息持续消耗现金。
- 资产价格与实体购买力脱钩。资产价格由买盘驱动,而买盘主要来自借款。当地产商和家庭的现金流都拿去还利息时,实体购买力下降,资产价格失去支撑。
- 强制清算没有下限。强平规则要求“超过上限就卖”,但在价格下跌时,卖出越多价格越跌,价格越跌负债率越高,于是进入“越卖越要卖”的循环。这是典型的流动性螺旋。
6. 常见报错与坑点
我在调这个模型时遇到过几个问题,列出来供你排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 价格突然变成负数 | update_price中没有做最小值保护 | 设置self.price = max(0.5, self.price) |
ZeroDivisionError | 某代理asset_qty = 0时仍参与计算 | 在除法前判断if asset_value > 0 |
| 模拟跑很久始终不崩溃 | 贷款比例上限过低,杠杆不足 | 把max_loan_ratio从 0.5 提高到 0.8 或 1.0 |
| 模拟在几十期就全部死亡 | 借款上限工资倍数过高、违约阈值过松 | 降低salary * 5,或提高违约判断阈值 |
| 多次运行结果不同 | 代码没有固定随机种子 | 在__init__中调用random.seed(seed) |
| 无法导入 matplotlib | 可视化依赖未安装 | 执行pip install matplotlib;不需要可视化可直接注释 |
| “强制卖出算出来是负数” | 超额资产计算里缺了max(0.0, ...) | 对excess做非负保护 |
另外有一个非常隐蔽的坑:不要在Agent里直接引用全局价格。许多初学者会把资产净值写成一个依赖全局变量的属性,这会让代码在批量模拟时很难调试。正确的做法是把价格作为参数传入,就像net_worth(self, price)一样。
7. 这类模拟的工程化建议
这个项目看起来只是一个小游戏,但如果想把它扩展到团队项目、量化研究或毕业论文,以下几点值得注意。
7.1 随机种子与可复现性
模拟类项目最重要的工程要求就是可复现。必须在入口处固定random.seed,最好在配置类中显式记录种子,输出结果时把参数和种子一起保存。否则你根本无法判断结果变化是“参数调整导致的”还是“随机波动导致的”。
7.2 参数配置与敏感性分析
把可变参数集中放到构造函数的参数列表里,而不是散落在函数内部。比如本文的interest_rate、max_loan_ratio、salary倍数,都可以提取成一个Config数据类。
然后可以试着跑一组参数扫描:
for ratio in [0.4, 0.6, 0.8, 1.0]: eco = Economy(max_loan_ratio=ratio, seed=42) hist = eco.run(steps=240) # 记录崩溃期、最大回撤、存活代理数你会发现,杠杆上限越高,繁荣期越疯狂,崩盘期越惨烈。这是一个非常直观的敏感性分析结果。
7.3 数据记录
不要只在控制台打印。建议把每一期的历史数据实时写入 CSV 文件,方便后续画图、做统计分析。
import csv with open("result.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=history[0].keys()) writer.writeheader() writer.writerows(history)这样做的好处是,即使程序崩溃,前期的模拟数据也不会丢。
7.4 安全边界与正确解读
最后必须强调一点:这是一个教学模拟,不是经济预测工具。它的意义在于展示“杠杆 + 正反馈 + 强制清算”这三者组合会产生怎样的系统脆弱性,而不是断言真实经济一定按这个路径发展。
在生产环境或金融相关工作中,需要引入更复杂的制度约束、监管规则和随机冲击,并且必须在测试环境充分验证。不要用这个小 demo 的结论直接指导真实投资或政策建议。
8. 总结与下一步扩展
这个项目最核心的收获,不是“我写出了崩溃”,而是“我能精确地指出崩溃在哪里被触发”。你掌握了四个关键机制:信贷购买力放大、资产价格正反馈、负债强平规则、信心传导。用一个不到 300 行的脚本,就能完整复现“繁荣—滞涨—抛售—崩盘”的链条。
如果你想让这个模拟更深入,可以从这几个方向扩展:
- 加入中央银行:观察货币政策在泡沫期是否有效,能否避免崩溃;
- 加入银行资产负债约束:银行也会破产,而不是无限放贷;
- 加入异质性代理:让一部分代理是保守型,一部分是投机型,观察两者比例对崩溃时间的影响;
- 加入外部冲击:比如工资随机下降、资产收益波动,观察系统在不同冲击下的表现;
- 改成实时可视化:用
matplotlib.animation把价格和信心画成动态曲线,效果会很直观。
如果你也想让这个虚拟世界“晚一点崩溃”,可以试着调低max_loan_ratio,或者提高信心恢复速度。动手改一次参数,比读十遍理论都管用。