简介:该资源围绕数字人民币对商业银行利润的影响,提供基于DSGE模型的完整研究复现材料,面向金融研究人员、政策制定者及商业银行从业者,帮助理解央行数字货币在替代现金与替代存款两个阶段对银行利润的差异化冲击。压缩包内仅含1个PDF文件,约850KB,集中呈现论文复现说明、模型推导与Python及Dynare代码解释,便于读者按图索骥地理解建模思路与实验流程。内容涵盖参数校准、稳态求解、动态方程构建与冲击响应模拟,并给出20%持有比例等关键阈值结论,可帮助读者掌握数字人民币技术冲击下货币需求变化、银行利润短期先降后升及中长期新利润空间的机制。已有97人学习,适合具备宏观经济建模基础、希望快速复现并拓展相关研究的学习者参考。
1. 数字人民币冲击银行利润这件事,用两个 DSGE 模型能算清吗
数字人民币到底会不会把商业银行的利润打穿?这个问题在 2020 年深圳试点之后就一直在被讨论,但多数讨论停留在定性层面——有人说存款会搬家,有人说银行多了数据资产,吵来吵去没有量化结论。石建勋、江鸿那篇《数字人民币对商业银行利润的影响研究》做了一件很具体的事:把数字人民币的推广拆成「替代现金」和「替代存款」两个阶段,各建一个 DSGE 模型,分别算冲击响应。替代现金阶段,数字人民币技术冲击削弱货币需求,但银行利润几乎不动,而且持有比例越高,对现金的挤出反而越低;替代存款阶段,银行利润短期先降后升,中长期反而多出一块新利润空间。这套代码用 Python 把两个模型的稳态、动态方程、冲击模拟和可视化全串起来了,适合做宏观建模、央行数字货币政策评估、银行资产负债表分析的从业者直接跑起来验证。下面我按「模型怎么搭 → 代码怎么跑 → 参数怎么调 → 哪里容易翻车」的顺序拆一遍。
2. 两个 DSGE 模型的骨架:从家庭效用函数到银行利润方程
2.1 为什么拆成两个阶段而不是一个模型
数字人民币对银行的冲击不是一条直线。替代现金阶段,数字人民币抢的是 M0,也就是流通中的现金,这部分本来就不在银行体系内生息,挤出去对银行利润表的影响很小。替代存款阶段就不一样了,数字人民币直接和活期存款竞争,存款是银行负债端的核心来源,一旦搬家,贷款投放能力、利差收入、准备金规模全跟着动。用一个模型硬套两个阶段,参数含义会打架——替代弹性 ε 在现金阶段小于 1,在存款阶段大于 1,符号方向都不一样。所以论文的做法是建两个独立模型,共享家庭部门和生产部门的基础设定,但在货币需求函数和银行利润方程上分叉。
这个设计思路在代码里体现得很清楚:Parameters类里同时放了theta_c(现金持有比例初始值)、theta_d(替代现金阶段的数字人民币持有比例)、theta_d_prime(替代存款阶段的持有比例),三个参数各管一个场景,互不干扰。steady_state函数里也分别算了pi_b_ss和pi_b_prime_ss两个稳态利润。
2.2 家庭部门:消费欧拉方程和劳动供给怎么进模型
家庭部门是整个 DSGE 的微观基础。代表性家庭在预算约束下最大化一生效用,效用函数里消费和劳动分开,风险规避系数 σ 控制跨期替代弹性,劳动供给弹性倒数 φ 控制劳动意愿对工资的反应。代码里对应的两个核心方程是:
# 消费欧拉方程:当期边际效用 = 贴现后的下期边际效用 × 资本回报 eq2 = c**(-params.sigma) - params.beta * c**(-params.sigma) * (1 + r_k - params.delta) # 劳动供给:工资的边际效用 = 劳动的边际负效用 eq3 = w * c**(-params.sigma) - n**params.varphibeta = 0.99是季度贴现因子,对应年化约 4% 的无风险利率,这是 RBC/DSGE 文献里的标准取值。sigma = 2意味着家庭相对风险规避系数为 2,跨期替代弹性 0.5,属于中等偏保守的设定。varphi = 3表示劳动供给弹性倒数,Frisch 弹性约 0.33,和国内宏观计量文献的常见估计接近。
这两个方程的逻辑是:家庭在当期多消费一单位,损失的是下期多消费带来的效用,均衡时两者相等。劳动供给方程则是工资带来的边际效用正好补偿多工作一单位带来的负效用。参数改动会直接影响稳态附近的动态响应幅度——σ 越大,消费对利率变化越不敏感,冲击传导越弱。
2.3 货币需求函数:替代弹性的方向决定结论
货币需求是这篇论文的核心机制。代码里现金需求、替代现金阶段的数字人民币需求、替代存款阶段的数字人民币需求分别写成:
# 现金需求稳态 m_c_ss = params.theta_c * (params.r_d - params.r_c)**(-params.epsilon) # 数字人民币需求(替代现金阶段) m_d_ss = params.theta_d * (params.r_d - params.r_e)**(-params.epsilon) # 数字人民币需求(替代存款阶段) m_d_prime_ss = params.theta_d_prime * (params.r_d - params.r_e)**(-params.epsilon)这里的关键参数是epsilon = 0.8,替代弹性小于 1。含义是:数字人民币利率 r_e 和存款利率 r_d 之间的利差变化,对货币需求的影响是递减的——利差扩大 1%,需求增加不到 1%。这个设定在替代现金阶段是合理的,因为现金本来就没有利息,数字人民币利率再高,能替代的现金量也有限。但到了替代存款阶段,论文认为替代弹性应该大于 1,因为存款和数字人民币之间的转换成本更低、便利性更高。代码里没有直接改 epsilon,而是通过提高theta_d_prime到 0.5 来体现替代强度的跃升。
注意:如果你要复现论文里「替代弹性 ε>1」的设定,需要手动把
epsilon改成 1.2 或更高,同时观察稳态利润是否出现符号反转。这是论文结论的敏感点之一。
2.4 银行利润方程:利差收入加准备金收益
银行利润的稳态表达式在代码里是两行:
# 替代现金阶段 pi_b_ss = (params.r_l - params.r_d) * (1 - params.phi) * (1 - m_d_ss) + params.r_d * params.phi * (1 - m_d_ss) # 替代存款阶段 pi_b_prime_ss = (params.r_l - params.r_e) * (1 - params.phi) * (1 - m_d_prime_ss) + params.r_e * params.phi * (1 - m_d_prime_ss)拆开看,第一项是贷款利差收入:(r_l - r_d)是存贷利差,(1 - phi)是可贷资金比例,(1 - m_d_ss)是扣除数字人民币分流后的存款基数。第二项是准备金收益:r_d * phi是准备金利率乘以准备金规模。替代存款阶段把r_d换成r_e,因为数字人民币计息后,银行负债端成本参照的是数字人民币利率而不是存款利率。
phi = 0.15是存款准备金率,r_d = 0.015是存款利率,r_l = 0.045是贷款利率,r_e = 0.03是数字人民币利率。这几个参数直接决定利润水平。利差 3 个百分点、准备金率 15%,算出来的稳态利润大概在什么量级,跑一遍steady_state就能看到。
3. 把代码跑起来:从参数校准到冲击响应模拟
3.1 环境准备和依赖安装
代码依赖 numpy、scipy、matplotlib、pandas 四个库。如果你用的是干净的 Python 环境,先装依赖:
pip install numpy scipy matplotlib pandasPython 版本建议 3.8 以上,scipy 的fsolve在 1.5 之后的版本对初值敏感度有优化。如果你用 vscode 配 Python 环境,记得选对解释器路径,不然pip install装到系统 Python 里、vscode 用的是虚拟环境,跑起来会报ModuleNotFoundError。这是新手最常翻车的地方。
3.2 参数校准:哪些值可以改、哪些最好别动
Parameters类里的参数分三组:家庭部门、数字人民币相关、商业银行部门。我的建议是:
| 参数 | 默认值 | 可调范围 | 调整影响 |
|---|---|---|---|
| beta | 0.99 | 0.96–0.995 | 影响稳态利率和资本回报 |
| sigma | 2 | 1–5 | 影响消费跨期替代弹性 |
| varphi | 3 | 1–5 | 影响劳动供给弹性 |
| alpha | 0.33 | 0.3–0.4 | 资本产出弹性,对应劳动收入份额 |
| delta | 0.025 | 0.02–0.03 | 季度折旧率,年化约 10% |
| theta_c | 0.2 | 0.1–0.3 | 现金持有比例初始值 |
| theta_d | 0.3 | 0.1–0.5 | 替代现金阶段数字人民币份额 |
| theta_d_prime | 0.5 | 0.3–0.9 | 替代存款阶段数字人民币份额 |
| epsilon | 0.8 | 0.5–1.5 | 替代弹性,跨 1 时结论可能反转 |
| phi | 0.15 | 0.1–0.2 | 存款准备金率 |
| r_d | 0.015 | 0.01–0.02 | 存款利率 |
| r_l | 0.045 | 0.03–0.06 | 贷款利率 |
| r_e | 0.03 | 0.01–0.04 | 数字人民币利率 |
alpha = 0.33对应资本收入份额约三分之一,和中国宏观数据里劳动收入份额偏低的特征一致。delta = 0.025是季度折旧率,年化 10%,比发达国家略高,符合国内资本折旧较快的现实。rho_tech = 0.9和rho_digital = 0.7是冲击持续性参数,技术冲击衰减慢、数字人民币冲击衰减快,这个设定在文献里常见。
3.3 稳态求解:先看数字合不合理再跑动态
稳态计算是steady_state函数干的活。跑完ss = steady_state(params)之后,打印ss字典,重点看几个数:
ss = steady_state(params) for key, value in ss.items(): print(f"{key}: {value:.6f}")r_k_ss应该在 0.01–0.02 之间,对应年化 4%–8% 的资本回报。w_ss是工资稳态,量级取决于生产函数标定。pi_b_ss和pi_b_prime_ss是两个阶段的银行利润,正常情况下pi_b_prime_ss应该低于pi_b_ss,因为数字人民币替代存款后,银行负债成本上升、可贷资金减少。如果跑出来pi_b_prime_ss反而更高,检查r_e是不是设得太低,或者theta_d_prime是不是太小。
提示:稳态求解不涉及动态迭代,秒出结果。如果这一步就报错,大概率是参数导致分母为零或负数,比如
r_d - r_c在r_c = 0时没问题,但如果你把r_c改成大于r_d的值,(r_d - r_c)**(-epsilon)就会出复数。
3.4 冲击模拟:fsolve 逐期求解的坑
simulate_shock函数用fsolve逐期解非线性方程组。核心逻辑是:第 t 期用第 t-1 期的解作为初值,求解 11 个方程组成的系统。代码里:
for t in range(1, periods): guess = history[t-1, :] solution = fsolve(lambda x: model_equations(x, params, shock_type), guess) history[t, :] = solution if shock_type == 'tech': history[t, 10] = params.rho_tech * history[t-1, 10] else: history[t, 11] = params.rho_digital * history[t-1, 11]这里有两个容易翻车的点。第一,fsolve对初值敏感,如果第 t-1 期的解离真实解太远,第 t 期可能不收敛,solution会返回初值附近的一个局部解,导致后续所有期都偏。第二,冲击过程是手动叠加的——history[t, 10]和history[t, 11]在fsolve求解后被覆盖,这意味着model_equations里的eq10和eq11实际上被绕过了。这种写法能跑出图形,但严格来说不是标准的 DSGE 求解流程。
常见做法是用 Dynare 的stoch_simul命令做一阶扰动求解,或者用 Python 的linearsolve库做线性化。但论文给的代码是教学向的,用fsolve逐期迭代更容易理解每一步在算什么。如果你要发论文,建议把冲击过程写成外生 AR(1) 直接进方程,而不是事后覆盖。
3.5 可视化:六宫格图怎么看
plot_results画的是 3×2 的子图,左边一列是技术冲击响应,右边一列是数字人民币冲击响应。每列从上到下分别是产出、银行利润、数字人民币需求。
技术冲击那列,产出应该先上后下,银行利润跟着产出走但幅度更小,数字人民币需求变化不大——因为技术冲击不直接改变数字人民币和存款的相对吸引力。数字人民币冲击那列,产出可能微降,银行利润在替代现金阶段几乎不动,在替代存款阶段先降后升。这个「先降后升」是论文的核心结论:短期银行负债成本上升挤压利润,中长期数字人民币带来的数据资产和精准放贷能力创造新利润来源。
注意:代码里
plot_results只画了替代现金阶段的银行利润(tech_shock[:,8]和digital_shock[:,8]),替代存款阶段的利润动态没有单独画。如果你想看替代存款阶段的冲击响应,需要把model_equations里的eq9改成pi_b_prime_ss的表达式,重新跑一遍。
4. 替代存款阶段的专项分析:持有比例阈值到底在哪
4.1 theta 扫描:从 0.1 到 0.9 看利润曲线
deposit_substitution_analysis函数做了一件很实用的事:把theta_d_prime从 0.1 扫到 0.9,每次算一遍稳态利润,画出一条利润随持有比例变化的曲线。
theta_values = np.linspace(0.1, 0.9, 9) profits = [] for theta in theta_values: params.theta_d_prime = theta ss = steady_state(params) profits.append(ss['pi_b_prime_ss'])这条曲线如果是单调下降的,说明数字人民币替代存款对银行利润是纯负面;如果是先降后升的 U 型,说明存在一个阈值,超过之后数据资产的正面效应开始占主导。论文提到的「20% 持有比例临界点」大概对应曲线斜率由负转正的位置。你可以把np.linspace(0.1, 0.9, 9)改成np.linspace(0.05, 0.5, 46),步长缩到 0.01,精确定位拐点。
4.2 数据资产项怎么加进利润方程
论文在替代存款阶段引入了数据资产,但代码里的pi_b_prime_ss没有显式包含数据资产项。如果你要复现论文的完整设定,需要在利润方程里加一项:
# 数据资产收益:交易数据量 × 风险权重倒数 data_asset_yield = theta * transactions_data * (risk_weight ** (-1)) pi_b_prime_ss = (r_l - r_e) * (1 - phi) * (1 - m_d_prime_ss) + r_e * phi * (1 - m_d_prime_ss) + data_asset_yieldtransactions_data可以标定为稳态产出的一个比例,risk_weight反映数据资产的风险调整系数。论文正文里给的data_assets = θ·transactions_data·(risk_weight)^(-1)就是这个形式。加上这一项之后,theta 扫描曲线会更明显地出现 U 型,因为数据资产收益随 theta 线性增长,而利差收入随 theta 线性下降,两者叠加产生非线性。
4.3 政策情景模拟:利率和准备金率的组合
论文的扩展建议里提到了政策情景模拟,代码框架可以这样搭:
policy_scenarios = { 'baseline': {'r_e': 0.01, 'phi': 0.10}, 'interest_floor': {'r_e': 0.00, 'phi': 0.15}, 'data_driven': {'r_e': 0.005, 'phi': 0.12} } for name, policy in policy_scenarios.items(): params.r_e = policy['r_e'] params.phi = policy['phi'] ss = steady_state(params) print(f"{name}: pi_b_prime = {ss['pi_b_prime_ss']:.6f}")这个模拟能回答一个实际问题:如果央行把数字人民币利率压到零、同时提高准备金率,银行利润会掉多少?如果反过来,数字人民币利率设为正、准备金率下调,银行能不能靠数据资产补回来?跑一遍就知道。
5. 避坑与排查:跑这套 DSGE 代码最容易翻车的五个地方
5.1 fsolve 不收敛,输出全是初值
现象:simulate_shock跑完,history矩阵里所有值都接近初始猜测,图形是一条直线。
原因:fsolve默认的容差是 1.49e-8,如果方程组在初值附近梯度很小,或者方程之间有线性相关,求解器会认为已经收敛。另一个可能是model_equations返回的列表长度和变量数不匹配——代码里vars解包了 12 个变量,但return只返回了 11 个方程,fsolve会报错或静默失败。
解决:把fsolve的full_output打开,检查ier标志位。ier=1表示收敛,其他值都是异常。同时确认model_equations返回的方程数和vars长度一致。代码里n_vars = 12但只返回 11 个方程,这是一个隐藏 bug,需要补一个方程或者把n_vars改成 11。
5.2 稳态利润为负,曲线画不出来
现象:pi_b_ss或pi_b_prime_ss算出负数,plt.plot画出来在零轴以下。
原因:r_l - r_d利差太小,或者phi准备金率太高,导致(1 - phi) * (1 - m_d_ss)的可贷资金比例被压得过低。另一个可能是m_d_ss大于 1,货币需求超过了总存款基数,这在theta_d设得过大时会发生。
解决:检查r_l是否大于r_d,phi是否在 0.1–0.2 之间,theta_d是否小于 0.5。如果m_d_ss > 1,把epsilon调大或者theta_d调小。
5.3 图形横坐标太密集,看不清冲击路径
现象:periods = 40时,x 轴标签挤在一起,matplotlib 自动旋转后还是重叠。
原因:默认的刻度间隔太密,40 个点全标出来。
解决:用ax.set_xticks(range(0, periods, 5))手动设置刻度间隔,或者用plt.xticks(rotation=45)旋转标签。更彻底的做法是把periods降到 20,冲击响应通常在 20 期内已经衰减到稳态附近。
5.4 替代存款阶段的利润曲线不出现 U 型
现象:deposit_substitution_analysis画出来是一条单调下降的线,没有拐点。
原因:利润方程里没有加数据资产项,或者theta_d_prime的扫描范围太窄,没覆盖到拐点。论文的 U 型依赖数据资产收益随 theta 增长,如果只算利差收入,利润必然随 theta 单调下降。
解决:在pi_b_prime_ss里加上data_asset_yield项,把theta_values范围扩到 0.05–0.95,步长缩到 0.01。
5.5 Dynare 和 Python 版本不兼容
现象:如果你试图用 PyDynare 接口跑这套代码,报ImportError: cannot import name 'dynare'。
原因:Dynare 的 Python 接口需要单独安装,而且对 Python 版本有要求。Dynare 5.x 支持 Python 3.7–3.9,Dynare 6.x 支持 3.9–3.11。如果你用的是 Python 3.12,PyDynare 可能还没适配。
解决:论文代码实际上没有依赖 Dynare,纯 numpy + scipy 就能跑。如果你要用 Dynare 做一阶扰动求解,建议单独建一个 conda 环境,装 Python 3.10 + Dynare 6.x。或者直接用 Python 的linearsolve库替代,安装pip install linearsolve,用linearsolve.model做线性化。
6. 进阶技巧:用 linearsolve 替代 fsolve 做一阶扰动
fsolve逐期迭代的写法适合教学,但做政策分析时不够稳。我一般会用linearsolve库把模型线性化,然后算脉冲响应。步骤是:先把model_equations写成linearsolve要求的equations字典格式,指定变量名和方程,然后调model.solve()和model.impulse()。
import linearsolve as ls # 定义变量和方程 equations = { 'y': 'y - a * k**alpha * n**(1-alpha)', 'c': 'c**(-sigma) - beta * c**(-sigma) * (1 + r_k - delta)', 'n': 'w * c**(-sigma) - n**varphi', 'k': 'k - (1-delta)*k - i', 'r_k': 'r_k - alpha * y / k', 'w': 'w - (1-alpha) * y / n', 'm_c': 'm_c - theta_c * (r_d - r_c)**(-epsilon) * c', 'm_d': 'm_d - theta_d * (r_d - r_e)**(-epsilon) * c', 'pi_b': 'pi_b - ((r_l - r_d) * (1 - phi) * (1 - m_d) + r_d * phi * (1 - m_d))', 'a': 'a - rho_tech * a(-1)', 'epsilon_d': 'epsilon_d - rho_digital * epsilon_d(-1)' } # 指定稳态值 ss_values = { 'y': 1.0, 'c': 0.8, 'n': 0.5, 'k': 5.0, 'r_k': 0.015, 'w': 1.2, 'm_c': 0.1, 'm_d': 0.15, 'pi_b': 0.02, 'a': 1.0, 'epsilon_d': 0.0 } model = ls.model(equations=equations, n_states=2, n_controls=9) model.assign_ss_values(ss_values) model.solve() model.impulse(T=40, t0=0, shocks={'a': 0.01}) model.plot_impulse()linearsolve的好处是自动做一阶泰勒展开,求解线性理性预期模型,比fsolve逐期迭代快一个数量级,而且不会出现不收敛的问题。n_states=2对应两个外生冲击状态变量a和epsilon_d,n_controls=9是其余内生变量。impulse函数直接算脉冲响应,plot_impulse出图。
我自己的习惯是:先用论文的fsolve代码跑一遍,确认稳态和冲击方向对得上,再用linearsolve做正式的政策模拟。两套结果如果差异很大,说明模型设定有问题——通常是方程线性化后丢掉了非线性项,或者稳态值标定不一致。从那以后我每次复现 DSGE 论文都强制走一遍「fsolve 验证 + linearsolve 正式跑」的流程,省得在审稿人那里翻车。希望帮到你。
本文还有配套的精品资源,点击获取