← 返回编程题库
coding-position-fingerprint-grouping中等免费版2000ms未尝试

组合持仓指纹聚类

Position Fingerprint Grouping

开始编码

风险中台每天收盘后会汇总几千个内部基金、子账户、做市策略上报的 EOD 持仓文件。很多组合其实是同一套模型在不同账户上的复制——比如同一个统计套利策略可能在三个法律实体下各跑一份,三套书的净持仓应当是字符级别一致的。找出所有“持仓指纹相同”的组合分组,是后续做风控合并、保证金优化、相关性去重的第一步:如果两个组合的净持仓多重集合完全一致,那它们的因子敞口、VaR、压力测试结果也应当一致,risk 引擎可以只算一次。

请实现 solution(portfolios: list[dict]) -> list[list[str]]portfolios 中每个元素是一个 dict,包含 name(组合名)和 positions(持仓明细列表)。同一只 ticker 在 positions 里可能被拆成多条(长仓 / 短仓 / 多笔买入分别上报),需要把 qty 累加得到净仓;累加后净仓为 0 的 ticker 视为已平仓,不进入指纹。两个组合“等价”当且仅当它们的净持仓多重集合((ticker, net_qty) 的集合)完全一致——上报顺序、原始拆分方式、是否含其它字段一律无关。

规范化规则:组合的 name 字段先 .strip();若缺失、为 None、或剥空白后为空,整个组合丢弃。positions 内每条 position 取出 ticker.strip(),若缺失 / 为 None / 为空则该条 position 忽略;qty 缺失按 0 处理;ticker 区分大小写。返回 list[list[str]],外层按每组里“字典序最小的组合名”升序排序,内层组合名也按字典序升序排序。

举例:solution([{"name": "alpha-A", "positions": [{"ticker": "TICK-A", "qty": 100}, {"ticker": "TICK-B", "qty": -50}]}, {"name": "alpha-B", "positions": [{"ticker": "TICK-B", "qty": -50}, {"ticker": "TICK-A", "qty": 100}]}, {"name": "beta-1", "positions": [{"ticker": "TICK-A", "qty": 60}, {"ticker": "TICK-A", "qty": 40}, {"ticker": "TICK-B", "qty": -50}]}, {"name": "gamma", "positions": [{"ticker": "TICK-A", "qty": 100}]}]) 应返回 [["alpha-A", "alpha-B", "beta-1"], ["gamma"]]alpha-Aalpha-B 顺序不同、beta-1 把 TICK-A 拆成两笔上报——三者归一化后的净持仓多重集合都是 {(TICK-A, 100), (TICK-B, -50)},归为同一组;gamma 只持有 TICK-A,自成一组;外层按组内字典序最小的名字(alpha-A < gamma)排序。

约束条件

  • 0 ≤ len(portfolios) ≤ 5000
  • 每个组合是 `{"name": str, "positions": list[dict]}`,`positions` 中每条形如 `{"ticker": str, "qty": int}`,可能还有别的字段,忽略即可
  • 若组合的 `name` 字段缺失、为 `None`、或剥掉首尾空白后为空字符串,则整个组合被丢弃,既不参与分组也不会出现在结果里
  • 组合内同一只 `ticker` 可能出现多次,需把 `qty` 累加得到净仓;若 ticker 缺失 / 为 `None` / 剥掉空白后为空,该 position 条目被忽略;若 `qty` 缺失,按 0 处理
  • ticker 大小写敏感、剥掉首尾空白;净仓为 0 的 ticker 不进入指纹(包括正负相抵后为 0 的情况)
  • 返回 `list[list[str]]`:外层按每组的“字典序最小的成员名”升序排序,内层按字典序升序排序;只包含被保留下来的组合

样例

Case 1 · example from statement

输入: [[{"name":"alpha-A","positions":[{"ticker":"TICK-A","qty":100},{"ticker":"TICK-B","qty":-50}]},{"name":"alpha-B","positions":[{"ticker":"TICK-B","qty":-50},{"ticker":"TICK-A","qty":100}]},{"name":"beta-1","positions":[{"ticker":"TICK-A","qty":60},{"ticker":"TICK-A","qty":40},{"ticker":"TICK-B","qty":-50}]},{"name":"gamma","positions":[{"ticker":"TICK-A","qty":100}]}]]

期望: [["alpha-A","alpha-B","beta-1"],["gamma"]]

alpha-A 与 alpha-B 仅上报顺序不同;beta-1 把 TICK-A 拆成 60+40 上报,累加后净仓 100,与前两者完全一致——三者归为一组。gamma 只持有 TICK-A 100 股,没有 TICK-B,自成一组。外层按组内字典序最小的名字排序:'alpha-A' < 'gamma'。

Case 2 · two equivalent and one distinct, all clean

输入: [[{"name":"fund-X","positions":[{"ticker":"SYM-1","qty":10}]},{"name":"fund-Y","positions":[{"ticker":"SYM-1","qty":10}]},{"name":"fund-Z","positions":[{"ticker":"SYM-1","qty":20}]}]]

期望: [["fund-X","fund-Y"],["fund-Z"]]

fund-X 与 fund-Y 都持有 10 股 SYM-1,指纹相同;fund-Z 持有 20 股,数量不同视为不同指纹。两组按各自最小成员名排序:'fund-X' < 'fund-Z'。

最近提交

还没有提交记录。

编码区

实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。

加载编辑器...
计时0:00

默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。

Case 1 · example from statement

输入: [[{"name":"alpha-A","positions":[{"ticker":"TICK-A","qty":100},{"ticker":"TICK-B","qty":-50}]},{"name":"alpha-B","positions":[{"ticker":"TICK-B","qty":-50},{"ticker":"TICK-A","qty":100}]},{"name":"beta-1","positions":[{"ticker":"TICK-A","qty":60},{"ticker":"TICK-A","qty":40},{"ticker":"TICK-B","qty":-50}]},{"name":"gamma","positions":[{"ticker":"TICK-A","qty":100}]}]]

期望: [["alpha-A","alpha-B","beta-1"],["gamma"]]

alpha-A 与 alpha-B 仅上报顺序不同;beta-1 把 TICK-A 拆成 60+40 上报,累加后净仓 100,与前两者完全一致——三者归为一组。gamma 只持有 TICK-A 100 股,没有 TICK-B,自成一组。外层按组内字典序最小的名字排序:'alpha-A' < 'gamma'。

Case 2 · two equivalent and one distinct, all clean

输入: [[{"name":"fund-X","positions":[{"ticker":"SYM-1","qty":10}]},{"name":"fund-Y","positions":[{"ticker":"SYM-1","qty":10}]},{"name":"fund-Z","positions":[{"ticker":"SYM-1","qty":20}]}]]

期望: [["fund-X","fund-Y"],["fund-Z"]]

fund-X 与 fund-Y 都持有 10 股 SYM-1,指纹相同;fund-Z 持有 20 股,数量不同视为不同指纹。两组按各自最小成员名排序:'fund-X' < 'fund-Z'。