沿组合层级树向上聚合 P&L
Aggregate P&L Up a Portfolio Hierarchy Tree
开始编码某多策略基金在策略叶子层级记账,但按 *book-of-books* 层级向上汇报:单个策略汇总到 pod,pod 汇总到 desk,desk 汇总到全公司。该层级是任意 N 叉树(并非二叉——一个 desk 可包含任意数量的 pod,一个 pod 可包含任意数量的策略),任何一天全公司层级 P&L 就是所有叶子策略 P&L 之和。你的任务是接受这棵树并一次性返回根节点的聚合 P&L。
实现 solution(tree: dict) -> float,其中每个节点形如以下之一:
- 叶子:
{"name": str, "pnl": float}—— 该策略在本期实现的 P&L。 - 内部节点:
{"name": str, "children": list[node]}—— 子账本,其聚合 P&L 等于其子节点聚合之和。
返回根节点的聚合 P&L,类型为 float。
children 为空列表的非叶节点贡献为 0.0(尚未建仓的子账本——退化但合法)。同时携带 pnl 与 children、或两者均不携带的节点为非法:原始规范要求 raise ValueError,但为了与批量打分基础设施兼容(后者把抛异常视作 harness 失败而非预期结果),本参考解改为返回 math.nan。NaN 会沿求和向上传播至根节点,再由比较器的 nan_equals_nan = true 标志断言。
例如,solution({"name": "alpha", "pnl": 1500.0}) 返回 1500.0(单个叶子即根节点)。solution({"name": "pod", "children": [{"name": "a", "pnl": 100.0}, {"name": "b", "pnl": -30.0}]}) 返回 70.0。一棵三层树(firm → 2 个 desk → 各叶子策略)无论分叉形态如何,都会把所有叶子求和。
实践背景
层级 P&L 上卷是量化运营里最经典的递归树练习。同一形态出现在风险归因(子账本暴露上卷为公司级 VaR 贡献)、合规报送(交易名义本金上卷到 LEI)、业绩归因(策略级 Sharpe 贡献级联到基金级)。之所以更偏好递归形式而不是手写栈,是因为真实层级常常 *不规则*——某个 desk 下面可能一个 pod 有二十个策略,另一个 pod 只有两个策略,还有第三个"壳"pod 暂无建仓——而递推对三种情形的处理完全一致。两条非法节点路径(同时含两键、两键都不含)是标准的防御契约:若上游配置管理交付的子账本不合法,在上卷一步就大声失败,远比静悄悄把某个叶子重复计入或把已建仓节点清零要好。在真实代码库里这个"大声失败"是 ValueError;在本批量打分环境里则编码为 math.nan 哨兵,使其能沿递归传播并被比较器断言。
约束条件
- 节点总数在 1 到 1000(含)之间
- 树深度在 1 到 50(含)之间
- 每个节点是 `dict`,带 `name: str` 键,并恰好含 `pnl: float`(叶子)或 `children: list[node]`(内部)中的一个
- 叶子的 `pnl` 是有限浮点数(正、负、零均可)
- 内部节点的 `children` 可为空列表(贡献为 `0.0`),但永远不为 `None`
- 同时含 `pnl` 与 `children`、或两者都不含的节点为非法:规范要求 `raise ValueError`,本批量打分环境改为返回 `math.nan` 哨兵(可经求和向上传播)
- 浮点比较容差:`rel_tol = 1e-9`、`abs_tol = 1e-12`
样例
Case 1 · statement-example: single-leaf root returns its own pnl
输入: [{"name":"alpha","pnl":1500}]
期望: 1500
单叶子节点即根节点;聚合 P&L 就是该叶子的 pnl。
Case 2 · statement-example: internal with two leaves
输入: [{"name":"pod","children":[{"name":"a","pnl":100},{"name":"b","pnl":-30}]}]
期望: 70
聚合 = 子节点之和 = 100 + (-30) = 70。
Case 3 · typical: three-level firm to desks to strategies
输入: [{"name":"firm","children":[{"name":"desk-equities","children":[{"name":"stat-arb","pnl":250},{"name":"long-short","pnl":-120}]},{"name":"desk-rates","children":[{"name":"macro","pnl":80},{"name":"carry","pnl":45.5}]}]}]
期望: 255.5
firm = (250 + (-120)) + (80 + 45.5) = 130 + 125.5 = 255.5。三层树按层求和无差别。
Case 4 · boundary: empty children list at non-leaf contributes zero
输入: [{"name":"fund","children":[{"name":"active-pod","children":[{"name":"s","pnl":50}]},{"name":"shell-pod-no-strats-yet","children":[]}]}]
期望: 50
shell-pod 的 children 为空,贡献 0.0;总和 = 50 + 0 = 50。空 children 是退化但合法的输入。
Case 5 · adversarial: malformed root with both pnl and children returns NaN sentinel
输入: [{"name":"malformed","pnl":100,"children":[{"name":"x","pnl":5}]}]
期望: "NaN"
节点同时含 pnl 与 children:歧义,规范上抛 ValueError;本环境返回 math.nan 哨兵以兼容批量打分。
Case 6 · adversarial: neither pnl nor children returns NaN sentinel
输入: [{"name":"neither"}]
期望: "NaN"
节点既无 pnl 也无 children:无可聚合内容,规范上抛 ValueError;本环境返回 math.nan 哨兵。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: single-leaf root returns its own pnl
输入: [{"name":"alpha","pnl":1500}]
期望: 1500
单叶子节点即根节点;聚合 P&L 就是该叶子的 pnl。
Case 2 · statement-example: internal with two leaves
输入: [{"name":"pod","children":[{"name":"a","pnl":100},{"name":"b","pnl":-30}]}]
期望: 70
聚合 = 子节点之和 = 100 + (-30) = 70。
Case 3 · typical: three-level firm to desks to strategies
输入: [{"name":"firm","children":[{"name":"desk-equities","children":[{"name":"stat-arb","pnl":250},{"name":"long-short","pnl":-120}]},{"name":"desk-rates","children":[{"name":"macro","pnl":80},{"name":"carry","pnl":45.5}]}]}]
期望: 255.5
firm = (250 + (-120)) + (80 + 45.5) = 130 + 125.5 = 255.5。三层树按层求和无差别。
Case 4 · boundary: empty children list at non-leaf contributes zero
输入: [{"name":"fund","children":[{"name":"active-pod","children":[{"name":"s","pnl":50}]},{"name":"shell-pod-no-strats-yet","children":[]}]}]
期望: 50
shell-pod 的 children 为空,贡献 0.0;总和 = 50 + 0 = 50。空 children 是退化但合法的输入。
Case 5 · adversarial: malformed root with both pnl and children returns NaN sentinel
输入: [{"name":"malformed","pnl":100,"children":[{"name":"x","pnl":5}]}]
期望: "NaN"
节点同时含 pnl 与 children:歧义,规范上抛 ValueError;本环境返回 math.nan 哨兵以兼容批量打分。
Case 6 · adversarial: neither pnl nor children returns NaN sentinel
输入: [{"name":"neither"}]
期望: "NaN"
节点既无 pnl 也无 children:无可聚合内容,规范上抛 ValueError;本环境返回 math.nan 哨兵。