feat(tsl-api): add parameter value domain lookup

This commit is contained in:
csh
2026-08-22 14:15:54 +08:00
parent 476f999139
commit b101b13bba
17 changed files with 2551 additions and 400 deletions
+5 -7
View File
@@ -1,6 +1,6 @@
---
name: tsl-api-reference
description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员框架回调是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。"
description: "当编写、修改或审查 TSL 代码,需要确认 API、class 成员框架回调或 API 参数可用值是否存在及其精确签名、参数、返回值和示例时使用;也用于按中文业务说法查找天软数据字典中的表、数据源与字段。触发说法包括「argmax 怎么用」「有没有去重函数」「TStringList 有哪些方法」「TSBackTesting 要重写什么」「getbk 的板块名」「申万煤炭的分类代码」「销售现金比率在哪个表」。不用于 TSL 语法、控制流、语言对象模型,也不用于决定 TSL 如何运行。"
---
# TSL API 参考
@@ -16,13 +16,8 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class
[`workflows/api-lookup.md`](workflows/api-lookup.md)。
- 查询 class 摘要、实现子类、重写回调或处理生命周期:读取
[`workflows/class-and-framework.md`](workflows/class-and-framework.md)。
- 按中文业务含义查表、数据源字段:读取
- 按中文业务含义查表、数据源字段或 API 参数可用值:读取
[`workflows/dictionary-lookup.md`](workflows/dictionary-lookup.md)。
- 涉及行业分类、基金分类或分类属性代码(`SWHY` / `CAPCHY` / `CSRCHY` / `YHFL` /
`TSJJ02` / `TSJJ03`)的代码体系与取数路径:读取
[`workflows/classification-codes.md`](workflows/classification-codes.md)。
- 需要先取一批证券代码(板块成分、指数成分、指定日在市列表、沪深港通名单):读取
[`workflows/code-sequences.md`](workflows/code-sequences.md)。
具体参数、输出字段和退出码以所选脚本当前的 `--help` 为权威。
`<this-skill-dir>` 替换为包含本 `SKILL.md` 的目录。
@@ -34,6 +29,8 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class
- 不得凭记忆或参照相似语言推断 API 名称、签名、参数、返回值或回调字段。
- 同名结果不得默认取第一条;必须按 `scope``module`、owner 或完全限定名消歧。
- `lookup.py` 的 rc=1 表示 skill 数据缺失、损坏或正文不可达,不表示 API 不存在。
- 参数取值域标为 `runtime_catalog` 或“静态记录不完整”时,样例只用于定位 API;完整名称
必须按结果给出的运行时解析器核验,不能把样例当成完整清单。
## 🔴 CHECKPOINT · 🛑 STOP
@@ -45,6 +42,7 @@ description: "当编写、修改或审查 TSL 代码,需要确认 API、class
- `class_lookup.py` 返回 `status: not_found`
- 请求依赖生命周期、配置或回调,但 `profile_status=not_profiled`
- Framework Profile 的 `contract_status` 不是 `resolved`
- 参数取值需要运行时核验但当前环境无法核验,且代码会依赖该具体值。
rc=1 不进入上述“API 缺失”分支;应先修复或重新安装 skill 数据,再重新查询。
@@ -5390,6 +5390,7 @@ GetBenchmarkWeight module ts-multi-factor GetBenchmarkWeight(v_end_t) module/ts-
GetBenchmarkWeightandReturn module ts-factor-attribution GetBenchmarkWeightandReturn(v_end_t) module/ts-factor-attribution.md getbenchmarkweightandreturnv_end_t 查询 组合权重 资产配置 比较基准 业绩基准 表数据 二维表 基准权重表,数据表结构如下。注意,当FindexId 有值时,则不需要重载此方法 method instance public tsrm_FactorAttribution tsrm_FactorAttribution.GetBenchmarkWeightandReturn
GetBenchRiskForecasting module tsrm-exante-risk-attribution GetBenchRiskForecasting() module/tsrm-exante-risk-attribution.md getbenchriskforecasting 查询 预测 风险管理 风控 比较基准 业绩基准 获取基准风险预估值 method instance public tsrm_ExAnte_RiskAttribution tsrm_ExAnte_RiskAttribution.GetBenchRiskForecasting
GetBitsPerComponent module pdf-file GetBitsPerComponent() module/pdf-file.md getbitspercomponent 查询 图片 色彩 获取图像颜色分量值位数 method instance public pdfImage pdfImage.GetBitsPerComponent
getBk dotnet datawarehouse getBk(marketlist) dotnet/datawarehouse/market_sector.md getbk 数据仓库 市场 板块 股票 成分 分类属性代码 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 最新 获取 得到指定市场板块列表包含的最新成分。普通市场板块返回股票代码;行业或分类目录可能返回 function getBk
getBkAll dotnet financial getBkAll(bkname) dotnet/financial/sector-sector_constituents.md getbkall 金融 板块 板块成分 返回 获取 返回板块所有样本(包括退市) 功能特别说明: 不适用以下板块或目录下的板块: 不适用板块:A股,上证A股,深证A股,中小企业板,创业板,科创板,(可用getabkbydate获取指定日) B股,上证B股,深证B股, 全球指数(目前4个不存在退市) 期货主力,期货连续,期货指数线,期货品种代码(这4个板块不区分上市退市) 存款利率,贷款利率,Shibor,贷款基础利率 不适用以下目录下的板块:股票\概念板块(可用getbkbydate获取指定日) 股票\股票地域 股票\指数成份(可用getbkbydate获取指定日) 股票\融资融券(可用getbkbydate获取指定日) 股票\互通股(可用getbkbydate获取指定日) 股票\上市状态(已包含所有直接用getbk) 基金\基金托管人 指数\指数提供商 回购\上市状态(已包含所有直接用getbk) 基金\上市状态(已包含所有直接用getbk) 期货\上市状态(已包含所有直接用getbk) 期权\上市状态(已包含所有直接用getbk) 债券\上市状态(已包含所有直接用getbk) 指数\上市状态(已包含所有直接用getbk function getBkAll
getbkall_arr dotnet financial getbkall_arr() dotnet/financial/sector-sector_constituents-intermediate.md getbkall_arr 金融 板块 板块成分 中间函数 获取 查询 获取所有板块样本配置表,此表配置只能单独列出规则的板块 function getbkall_arr
getbkall_arr2 dotnet financial getbkall_arr2() dotnet/financial/sector-sector_constituents-intermediate.md getbkall_arr2 金融 板块 板块成分 中间函数 获取 查询 获取所有板块样本配置表,此表配置通用规则及板块名,开发用 function getbkall_arr2
@@ -5397,10 +5398,12 @@ getBkAllbyEndT dotnet financial getBkAllbyEndT(bkname, end_t) dotnet/financial/s
getBkBacktrackByDate dotnet financial getBkBacktrackByDate(index_id, end_t, ex_type) dotnet/financial/index-index_info.md getbkbacktrackbydate 金融 指数 指数情况 获取 查询 获取指数历史上指定日样本(可回溯)。当ExType=1时,如指数成分中取不到成分,则从权重表中获取。此函数可以简单回溯历史数据,当Endt早于指数开始日,则选取指数开始日成分股在endt在市交易的成分股列表 function getBkBacktrackByDate
getbkbybegtendt dotnet financial getbkbybegtendt(indexid, begt, endt) dotnet/financial/index-index_info.md getbkbybegtendt 金融 指数 指数情况 返回 获取 返回区间曾经是成分股的股票,区间范围内是指数成份股的股票 function getbkbybegtendt
getBkByBegTEndT_ii dotnet financial getBkByBegTEndT_ii(bk_name, beg_t, end_t) dotnet/financial/sector-basic_info.md getbkbybegtendt_ii 金融 板块 基本情况 返回 获取 返回区间范围内曾经在板块上市过的所有股票(含已暂停已退市)。目前只能使用以下板块:'A股','上证A股','科创板','深证A股','创业板','北证A股','B股','上证B股','深证B股' function getBkByBegTEndT_ii
getBkByDate dotnet financial getBkByDate(index_id, end_t, extype) dotnet/financial/index-index_info.md getbkbydate 金融 指数 指数情况 获取 查询 获取指数历史指定日的成份股列表。与GetBK不同,该函数需要的第一个参数是指数代码,获取该指数的指定日的成分股,比如沪深300定期调整,在不同时间获取的成分股将不同。 新增功能,当ExType=1时指数成分中取不到成分时,则从权重表中获取 function getBkByDate
getBkByDate dotnet financial getBkByDate(index_id, end_t, extype) dotnet/financial/index-index_info.md getbkbydate 金融 指数 指数情况 历史成份股 分类属性代码 下级分类 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 TSI000001 TSI000002 TSI000003 TSI000004 TSI000005 TSI000006 TSI000007 TSI000008 TSI000009 获取 查询 获取指数或分类属性在指定日的成份股列表。与getBk不同,该函数需要日期参数;普通指数使用 function getBkByDate
getBkByIndexName dotnet financial getBkByIndexName(index_name, end_t) dotnet/financial/index-index_info.md getbkbyindexname 金融 指数 指数情况 返回 获取 返回使用指数名称获取指定日指数成分,只支持申万行业、A股 function getBkByIndexName
getBkGoMarketByDate dotnet financial getBkGoMarketByDate(index_id, end_t, ex_type) dotnet/financial/index-index_info.md getbkgomarketbydate 金融 指数 指数情况 获取 查询 获取指数历史上指定日样本(已上市)。当ExType=1时指数成分中取不到成分时,则从权重表中获取。存在申万行业分类的成份股中,它的选入日期可能早于上市日,现剔除这部分标的 function getBkGoMarketByDate
getbkIndexIDs dotnet financial getbkIndexIDs() dotnet/financial/sector-basic_info-intermediate.md getbkindexids 金融 板块 基本情况 中间函数 获取 查询 AB 获取AB股板块及编码对照表 function getbkIndexIDs
getBkList dotnet datawarehouse getBkList(Type) dotnet/datawarehouse/market_sector.md getbklist 数据仓库 市场 板块 系统板块 行业分类 分类目录 列表 获取 得到所有的或指定系统板块类别下的板块名称列表。`Type` 是板块类别或具体板块目录名称; function getBkList
getBkList2 dotnet datawarehouse getBkList2([bktype]) dotnet/datawarehouse/market_sector.md getbklist2 数据仓库 市场 板块 系统板块 路径 列表 获取 得到所有的或指定系统板块类别下的板块名称及其所在路径。与 `getBkList` 不同,该函数会 function getBkList2
getBkNameToId dotnet financial getBkNameToId(index_name, end_t) dotnet/financial/index-index_info.md getbknametoid 金融 指数 指数情况 返回 获取 返回使用指数名称获取指定日指数成分 function getBkNameToId
getbktypelist dotnet financial getbktypelist(bktype) dotnet/financial/stock-basic_info-basic_info-intermediate.md getbktypelist 金融 股票 基本情况 中间函数 获取 查询 获取指定板块分类下的板块名称列表 function getbktypelist
getBkWeightByDate2 dotnet financial getBkWeightByDate2(index_id, benchmark_id, end_t) dotnet/financial/index-basic_information.md getbkweightbydate2 金融 指数 基本信息 返回 获取 SH000985 返回本模型从基准指数BenchmarkID的成分股权重数据中获取目标指数IndexID的成份股,并取其权重进行归一处理,计算目标指数IndexID的成分股权重。 使用时需注意: 1.基准指数BenchmarkID的选取比较重要,比如申万二级和三级行业,可以选择对应的申万一级行业指数作为估算的基准指数,经测算是较为接近的; 2.目标指数IndexID的成分股必须可获取; 3.基准指数BenchmarkID的成分股应包含目标指数IndexID的成分股; 4.如果BenchmarkID为nil,则取默认基准指数--SH000985 中证全指 function getBkWeightByDate2
@@ -5594,8 +5597,8 @@ getHttpContent builtin network getHttpContent(session[, from_pos]) builtin/netwo
getHttpCookies builtin network getHttpCookies(session) builtin/network.md gethttpcookies 网络 获取 查询 HTTP 请求 network 获取指定 HTTP 会话保存的服务器 Cookie function getHttpCookies
getHttpResponseHeader builtin network getHttpResponseHeader([session]) builtin/network.md gethttpresponseheader 网络 获取 查询 HTTP 请求 network 获取 HTTP 响应头;session 可省略 function getHttpResponseHeader
getHyDyFlDetails dotnet financial getHyDyFlDetails(type) dotnet/financial/stock-industry.md gethydyfldetails 金融 股票 行业 获取 查询 获取分行业/地域/分类的股票的详细情况 function getHyDyFlDetails
getHyDyFlList dotnet financial getHyDyFlList(type) dotnet/financial/stock-industry.md gethydyfllist 金融 股票 行业 获取 查询 获取行业/地域/分类的列表,用于替代GetBKList function getHyDyFlList
getHyDyFlStocks dotnet financial getHyDyFlStocks(type, name) dotnet/financial/stock-industry.md gethydyflstocks 金融 股票 行业 获取 查询 获取行业的股票。取代GetBK,比其更精确 function getHyDyFlStocks
getHyDyFlList dotnet financial getHyDyFlList(type) dotnet/financial/stock-industry.md gethydyfllist 金融 股票 行业 获取 查询 获取行业/地域/分类的列表,用于替代getBkList function getHyDyFlList
getHyDyFlStocks dotnet financial getHyDyFlStocks(type, name) dotnet/financial/stock-industry.md gethydyflstocks 金融 股票 行业 获取 查询 获取行业的股票。取代getBk,比其更精确 function getHyDyFlStocks
GetIC module tsf-factor-ext-indicator-base GetIC(begt, endt, ic_type) module/tsf-factor-ext-indicator-base.md geticbegtendtic_type 查询 信息系数 列表 集合 获得 IC 数据,需要用户重载,基类未实现,返回空数组,并提示报错:getIC 方法必须重写! method instance public tsf_factorExtindiCator_base tsf_factorExtindiCator_base.GetIC
GetImpactCostEstimatedDetail module impact-cost GetImpactCostEstimatedDetail() module/impact-cost.md getimpactcostestimateddetail 查询 预估 交易成本 获取冲击成本预测详情,包含"永久性冲击成本(BP)"、"暂时性冲击成本(BP"、 method instance public ts_ImpactCost ts_ImpactCost.GetImpactCostEstimatedDetail
GetImpactCostEstimatedValue module impact-cost GetImpactCostEstimatedValue() module/impact-cost.md getimpactcostestimatedvalue 查询 已实现冲击 冲击成本 BP 获取已实现冲击成本(BP),单位 BP method instance public ts_ImpactCost ts_ImpactCost.GetImpactCostEstimatedValue
@@ -5953,6 +5956,8 @@ GetUnicode module pdf-file GetUnicode(code) module/pdf-file.md getunicodecode
GetUnicodeWidth module pdf-file GetUnicodeWidth(code) module/pdf-file.md getunicodewidthcode 查询 尺寸 宽高 字型 获取当前字体下指定 Unicode 字符的宽度 method instance public pdfFont pdfFont.GetUnicodeWidth
getUnilateralValue dotnet financial getUnilateralValue(exp, endt) dotnet/financial/market-intermediate.md getunilateralvalue 金融 行情 中间函数 返回 获取 返回期货单边指标,与系统参数(证券代码)相关 function getUnilateralValue
getUrl builtin network getUrl(src_url, parse_url) builtin/network.md geturl 网络 解析 parse HTTP 请求 network HTML URL 解析在HTML中出现的URL,返回全路径URL串 function getUrl
getUserBkList dotnet datawarehouse getUserBkList([bktype]) dotnet/datawarehouse/market_sector.md getuserbklist 数据仓库 市场 板块 用户板块 列表 获取 得到所有的或指定板块类别下的用户板块名称。`bktype` 省略时返回全部用户板块;指定时可 function getUserBkList
getUserBkList2 dotnet datawarehouse getUserBkList2([bktype]) dotnet/datawarehouse/market_sector.md getuserbklist2 数据仓库 市场 板块 用户板块 路径 列表 获取 得到所有的或指定板块类别下用户板块的名称及其所在路径。与 `getUserBkList` 不同, function getUserBkList2
GetValuationDetail module bond-research GetValuationDetail() module/bond-research/convertible-bond-ccb.md getvaluationdetail 返回各估值路径的明细 method instance public cb_CompleteDecompose cb_CompleteDecompose.GetValuationDetail
GetValueByEndt module ts-persistence-of-fund GetValueByEndt(funds, endt) module/ts-persistence-of-fund.md getvaluebyendtfundsendt 查询 绩效 业绩表现 基金产品 获取指定日基金池基金业绩指标 method instance public ts_PersistenceOfFund ts_PersistenceOfFund.GetValueByEndt
getValuesByFieldValue dotnet base getValuesByFieldValue(t, f_name, field_value) dotnet/base/array.md getvaluesbyfieldvalue 返回 获取 返回指定字段值等于指定值的所有记录 function getValuesByFieldValue
@@ -7116,7 +7121,7 @@ marketAndTotalValueStructure dotnet financial_report marketAndTotalValueStructur
marketLastTradeDay dotnet financial marketLastTradeDay(end_t) dotnet/financial/market-n_day.md marketlasttradeday 金融 行情 N 日 返回 获取 返回指定日前一个市场交易日 function marketLastTradeDay
marketlasttradeday2 dotnet financial marketlasttradeday2() dotnet/financial/market-current_day.md marketlasttradeday2 金融 行情 当日 返回 获取 SH000001 HKHSI001 HSG000001 HSG000002 返回市场最近交易日 系统参数:股票,日期获取市场指定日前最近交易日。与系统参数(股票,日期)相关。 其中各市场代表指数为: A股市场:SH000001; 港股市场:HKHSI001 港股通:HSG000001,包括港股通(沪)?和港股通(深)两个通道 陆股通:HSG000002,包括沪股通和深股通两个通道 南北向开始日:2014-11-17 如果返回nil,则指定日通道未开通 function marketlasttradeday2
marketlasttradedayn dotnet financial marketlasttradedayn(n) dotnet/financial/market-n_day.md marketlasttradedayn 金融 行情 N 日 返回 获取 返回A股市场、港股市场、沪深港通板块获取市场指定日前N个交易日。系统参数:市场指数代码 function marketlasttradedayn
marketList dotnet financial marketList() dotnet/financial/sector-basic_info-miscellaneous.md marketlist 金融 板块 基本情况 其它 返回 获取 返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数GetBKList,使用方法参见该函数的函数说明 function marketList
marketList dotnet financial marketList() dotnet/financial/sector-basic_info-miscellaneous.md marketlist 金融 板块 基本情况 其它 返回 获取 返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数getBkList,使用方法参见该函数的函数说明 function marketList
marketNextTradeDay dotnet financial marketNextTradeDay(end_t) dotnet/financial/market-n_day.md marketnexttradeday 金融 行情 N 日 返回 获取 返回市场后一个交易日 function marketNextTradeDay
marketnexttradedayn dotnet financial marketnexttradedayn(n) dotnet/financial/market-n_day.md marketnexttradedayn 金融 行情 N 日 返回 获取 返回A股市场、港股市场、沪深港通板块获取市场指定日后N个交易日。系统参数:市场指数代码 function marketnexttradedayn
marketPRInitTradeAmountEndT dotnet financial marketPRInitTradeAmountEndT(market_type, end_t) dotnet/financial/stock-stock_pledge_repo_transaction_summary-specified_date.md marketprinittradeamountendt 金融 股票 质押回购 交易汇总 初始交易金额 市场 指定日 返回指定日指定市场的初始交易金额 function marketPRInitTradeAmountEndT
Can't render this file because it is too large.
@@ -0,0 +1,461 @@
{
"version": 1,
"domains": [
{
"id": "market_board",
"label": "市场和板块名称",
"mode": "runtime_catalog",
"complete": false,
"bindings": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBk",
"parameter": "marketlist",
"role": "current_components"
}
],
"resolvers": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBkList2",
"parameter": "bktype",
"catalog": "system",
"source": "net_function:28964"
},
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getUserBkList2",
"parameter": "bktype",
"catalog": "user",
"source": "net_function:28963"
}
],
"values": [],
"related_tables": [],
"sources": ["net_function:28960", "faq:37996"],
"as_of": "2026-08-20"
},
{
"id": "classification_code",
"label": "分类属性代码",
"mode": "versioned_catalog",
"complete": false,
"bindings": [
{
"scope": "dotnet",
"module": "financial",
"api": "getBkByDate",
"parameter": "index_id",
"role": "historical_components"
}
],
"resolvers": [],
"related_tables": [
{
"id": "138",
"name": "股票.股票行业分类信息",
"scope": "stock",
"page": "references/data_dictionary/股票__股票行业分类信息.md"
},
{
"id": "355",
"name": "基金.基金分类信息",
"scope": "fund",
"page": "references/data_dictionary/基金__基金分类信息.md"
}
],
"values": [
{
"value": "SWHY",
"label": "申万行业",
"aliases": ["申万行业分类"],
"level": 0,
"related_table": "138",
"sources": ["faq:31492", "faq:31494"],
"relations": [
{
"kind": "runtime_parameter",
"domain": "classification_catalog_selector",
"value": "申万行业",
"verification": {
"status": "documented",
"scope": "dotnet",
"module": "datawarehouse",
"resolver": "getBkList2",
"parameter": "bktype",
"argument": "申万行业"
},
"sources": [
"faq:31494",
"net_function:28962",
"net_function:28964"
]
}
]
},
{
"value": "SWHY740000",
"label": "申万煤炭",
"aliases": ["申万煤炭一级行业"],
"parent": "SWHY",
"parent_label": "申万行业",
"level": 1,
"valid_from": "20211213",
"related_table": "138",
"as_of": "2023-10-13",
"sources": ["faq:31494", "faq:35791"],
"relations": [
{
"kind": "runtime_parameter",
"domain": "market_board",
"value": "申万煤炭",
"verification": {
"status": "runtime_required",
"scope": "dotnet",
"module": "datawarehouse",
"resolver": "getBkList2",
"parameter": "bktype",
"argument": "申万行业"
},
"sources": ["faq:31494", "net_function:28964"]
}
]
},
{
"value": "CAPCHY",
"label": "中上协行业",
"aliases": ["中国上市公司协会行业"],
"level": 0,
"related_table": "138",
"sources": ["faq:31492"]
},
{
"value": "CSRCHY",
"label": "证监会行业",
"aliases": [],
"level": 0,
"note": "2024-02-19 起不再维护",
"related_table": "138",
"sources": ["faq:31492"]
},
{
"value": "YHFL",
"label": "银河基金分类",
"aliases": [],
"level": 0,
"related_table": "355",
"sources": ["faq:31492"]
},
{
"value": "TSJJ02",
"label": "投资风格",
"aliases": ["基金投资风格"],
"level": 0,
"related_table": "355",
"sources": ["faq:31492"]
},
{
"value": "TSJJ03",
"label": "净值增长率计算方法",
"aliases": [],
"level": 0,
"related_table": "355",
"sources": ["faq:31492"]
}
],
"sources": ["faq:31492"],
"as_of": "2026-08-20"
},
{
"id": "classification_catalog_selector",
"label": "分类属性目录选择器",
"mode": "catalog",
"complete": false,
"bindings": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBk",
"parameter": "marketlist",
"role": "catalog_selector"
}
],
"resolvers": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBkList2",
"parameter": "bktype",
"catalog": "system",
"source": "net_function:28964"
}
],
"related_tables": [
{
"id": "138",
"name": "股票.股票行业分类信息",
"scope": "stock",
"page": "references/data_dictionary/股票__股票行业分类信息.md"
},
{
"id": "355",
"name": "基金.基金分类信息",
"scope": "fund",
"page": "references/data_dictionary/基金__基金分类信息.md"
}
],
"values": [
{
"value": "属性代码",
"label": "全部股票与基金分类属性代码",
"aliases": ["分类属性代码目录"],
"sources": ["faq:31492"]
}
],
"sources": ["faq:31492"],
"as_of": "2026-08-20"
},
{
"id": "classification_child_code",
"label": "下级分类属性代码模式",
"mode": "pattern",
"complete": true,
"bindings": [
{
"scope": "dotnet",
"module": "financial",
"api": "getBkByDate",
"parameter": "index_id",
"role": "child_codes"
}
],
"resolvers": [],
"related_tables": [
{
"id": "138",
"name": "股票.股票行业分类信息",
"scope": "stock",
"page": "references/data_dictionary/股票__股票行业分类信息.md"
},
{
"id": "355",
"name": "基金.基金分类信息",
"scope": "fund",
"page": "references/data_dictionary/基金__基金分类信息.md"
}
],
"values": [
{
"value": "<分类属性代码>.N",
"label": "指定分类节点的下级分类属性代码",
"aliases": [".N", "下级分类代码"],
"sources": ["faq:31492"]
}
],
"sources": ["faq:31492"],
"as_of": "2026-08-20"
},
{
"id": "market_board_type",
"label": "板块类别",
"mode": "enum",
"complete": true,
"bindings": [
{
"scope": "dotnet",
"module": "financial",
"api": "getbktypelist",
"parameter": "bktype",
"role": "category"
},
{
"scope": "dotnet",
"module": "financial",
"api": "stocksbklist",
"parameter": "bktype",
"role": "category"
}
],
"resolvers": [],
"related_tables": [],
"values": [
{
"value": "-1",
"label": "自定义板块序列",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "0",
"label": "概念板块",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "1",
"label": "中证行业",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "2",
"label": "中证证监会行业",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "3",
"label": "股票地域",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "4",
"label": "指数成份",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "5",
"label": "申万行业(一级)",
"aliases": ["申万一级行业"],
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "6",
"label": "申万二级行业",
"sources": ["net_function:22421", "net_function:22416"]
},
{
"value": "7",
"label": "申万三级行业",
"sources": ["net_function:22421", "net_function:22416"]
}
],
"sources": ["net_function:22421", "net_function:22416"],
"as_of": "2026-08-21"
},
{
"id": "historical_market_board_code",
"label": "常用股票市场板块历史代码",
"mode": "enum",
"complete": true,
"bindings": [
{
"scope": "dotnet",
"module": "financial",
"api": "getBkByDate",
"parameter": "index_id",
"role": "historical_market_components"
}
],
"resolvers": [],
"related_tables": [],
"values": [
{
"value": "TSI000001",
"label": "A股板块",
"aliases": ["A股历史成份股"],
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000002",
"label": "上证A股板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000003",
"label": "科创板板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000004",
"label": "深证A股板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000005",
"label": "创业板板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000006",
"label": "北证A股板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000007",
"label": "B股板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000008",
"label": "上证B股板块",
"sources": ["faq:34194", "faq:35788"]
},
{
"value": "TSI000009",
"label": "深证B股板块",
"sources": ["faq:34194", "faq:35788"]
}
],
"sources": ["faq:34194", "faq:35788"],
"as_of": "2023-10-19"
},
{
"id": "historical_a_share_board_name",
"label": "按名称查询的历史 A 股板块",
"mode": "enum",
"complete": true,
"bindings": [
{
"scope": "dotnet",
"module": "financial",
"api": "getAbkbyDate",
"parameter": "bk_name",
"role": "historical_named_components"
}
],
"resolvers": [],
"related_tables": [],
"values": [
{
"value": "A股",
"label": "全部 A 股",
"note": "包括上证A股、科创板、深证A股、创业板和北证A股,不含新三板",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "上证A股",
"label": "上证 A 股",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "深证A股",
"label": "深证 A 股",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "北证A股",
"label": "北证 A 股",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "创业板",
"label": "创业板",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "科创板",
"label": "科创板",
"sources": ["net_function:24767", "faq:35788"]
},
{
"value": "中小企业板",
"label": "中小企业板(历史兼容)",
"note": "仅用于历史兼容;该板块已于 2021-04-06 并入主板",
"sources": ["net_function:24767", "faq:35788"]
}
],
"sources": ["net_function:24767", "faq:35788"],
"as_of": "2025-08-13"
}
]
}
@@ -0,0 +1,160 @@
# 数据仓库 / 市场和板块函数
## `getBk(marketlist)`
声明:function
得到指定市场板块列表包含的最新成分。普通市场板块返回股票代码;行业或分类目录可能返回
行业名称、板块名称或分类属性代码,不是指数代码。板块或行业名称必须与板块管理中的名称
一致;需要指定历史日期时使用 `getBkByDate`
<!-- tags: 数据仓库 市场 板块 股票 成分 分类属性代码 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 最新 获取 -->
| 参数 | 类型 | 说明 |
| ------------ | ------ | ---------------------------------------- |
| `marketlist` | string | 市场板块列表,多个市场板块之间用分号分隔 |
返回:array of string
### 示例
范例01:获取多个市场板块的当前成分
```tsl
stocks := getBk('上证A股;创业板');
return stocks;
```
范例02:获取申万行业分类属性代码目录
```tsl
codes := getBk("申万行业");
return select * from infotable 138 of codes end;
```
`getBk("申万行业")` 返回的分类属性代码可作为股票行业分类信息表(表 138)的访问代码。
具体板块名称应先从板块管理或 `getBkList` 返回结果确认
范例03:获取股票和基金的全部分类属性代码
```tsl
codes := getBk('属性代码');
stock_classes := select * from infotable 138 of codes end;
fund_classes := select * from infotable 355 of codes end;
return stock_classes union fund_classes;
```
## `getUserBkList([bktype])`
声明:function
得到所有的或指定板块类别下的用户板块名称。`bktype` 省略时返回全部用户板块;指定时可
使用地域或行业目录,也可以用反斜杠分隔多级目录
<!-- tags: 数据仓库 市场 板块 用户板块 列表 获取 -->
| 参数 | 类型 | 说明 |
| -------- | ------ | -------------------------------------------------------- |
| `bktype` | string | 可选。板块类别或目录路径,例如 `地域``行业` 或多级目录 |
返回:array of string
### 示例
范例01:获取全部用户板块名称
```tsl
my_sectors := getUserBkList();
return my_sectors;
```
范例02:获取用户板块的指定子目录
```tsl
my_sectors := getUserBkList("申万行业\\申万金融业");
return my_sectors;
```
## `getBkList(Type)`
声明:function
得到所有的或指定系统板块类别下的板块名称列表。`Type` 是板块类别或具体板块目录名称;
官方定义同时说明省略该参数时返回全部板块
<!-- tags: 数据仓库 市场 板块 系统板块 行业分类 分类目录 列表 获取 -->
| 参数 | 类型 | 说明 |
| ------ | ------ | ----------------------------------- |
| `Type` | string | 板块类别或目录名称,例如 `申万行业` |
返回:array of string
### 示例
范例01:获取申万行业列表
```tsl
bk_list := getBkList('申万行业');
return bk_list;
```
范例02:获取申万行业的子目录列表
```tsl
bk_list2 := getBkList('申万行业\\申万采掘');
return bk_list2;
```
## `getUserBkList2([bktype])`
声明:function
得到所有的或指定板块类别下用户板块的名称及其所在路径。与 `getUserBkList` 不同,
该函数会包含指定目录及其子目录下的用户板块
<!-- tags: 数据仓库 市场 板块 用户板块 路径 列表 获取 -->
| 参数 | 类型 | 说明 |
| -------- | ------ | ---------------------------------------------- |
| `bktype` | string | 可选。板块类别或目录路径,多级目录用反斜杠分隔 |
返回:table_array
### 示例
范例01:获取申万行业目录下的用户板块及所属组别
```tsl
my_sectors := getUserBkList2("申万行业");
return my_sectors;
```
## `getBkList2([bktype])`
声明:function
得到所有的或指定系统板块类别下的板块名称及其所在路径。与 `getBkList` 不同,该函数会
包含指定目录及其子目录下的系统板块
<!-- tags: 数据仓库 市场 板块 系统板块 路径 列表 获取 -->
| 参数 | 类型 | 说明 |
| -------- | ------ | ------------------------------------- |
| `bktype` | string | 可选。板块类别或目录名称,例如 `行业` |
返回:table_array
### 示例
范例01:获取全部系统板块及其路径
```tsl
return getBkList2();
```
范例02:获取系统申万行业板块及其路径
```tsl
return getBkList2('申万行业');
```
@@ -67,13 +67,15 @@ return GetBKBacktrackByDate("SWHY740000", 20140805T);
声明:function
获取指数历史指定日的成份股列表。与GetBK不同,该函数需要的第一个参数是指数代码,获取该指数的指定日的成分股,比如沪深300定期调整,在不同时间获取的成分股将不同。 新增功能,当ExType=1时指数成分中取不到成分时,则从权重表中获取
获取指数或分类属性在指定日的成份股列表。与getBk不同,该函数需要日期参数;普通指数使用
指数代码,行业或基金分类使用分类属性代码。新增功能,当ExType=1时指数成分中取不到成分时,
则从权重表中获取
<!-- tags: 金融 指数 指数情况 获取 查询 -->
<!-- tags: 金融 指数 指数情况 历史成份股 分类属性代码 下级分类 SWHY CAPCHY CSRCHY YHFL TSJJ02 TSJJ03 TSI000001 TSI000002 TSI000003 TSI000004 TSI000005 TSI000006 TSI000007 TSI000008 TSI000009 获取 查询 -->
| 参数 | 类型 | 说明 |
| ---------- | -------- | ------------------------------------------------------------------------------------- |
| `index_id` | string | 指数代码 |
| `index_id` | string | 指数代码、分类属性代码或 TSI 市场板块代码 |
| `end_t` | datetime | 截止日 |
| `extype` | int | 整型,是否扩展,默认不扩展;0:不扩展;1:扩展取数(指数成分表无数据时,从权重表中获取) |
@@ -95,6 +97,37 @@ Return GetBkByDate('SH000300', inttodate(20120908));
return GetBKByDate("SZ399102", 20201201T, 1); // 取权重中的成份股替代
```
范例03:取指定日申万分类的下级属性代码
```tsl
return GetBKByDate("SWHY.N", 20210423T, 0);
```
范例04:取指定日申万分类的成份股
```tsl
return GetBKByDate("SWHY740000", 20220101T, 0);
```
范例05:取指定日 A 股市场板块的成份股
```tsl
return GetBKByDate("TSI000001", 20230810T, 0);
```
范例06:取指定日投资风格分类下的基金
```tsl
return GetBKByDate("TSJJ0201", 20220101T, 0);
```
`index_id` 使用分类属性代码时,代码末尾的 `.N` 表示返回该分类节点在指定日的下级
分类属性代码;不带 `.N` 时返回该分类节点的成份券。常用根分类包括股票的 `SWHY`
`CAPCHY``CSRCHY`,以及基金的 `YHFL``TSJJ02``TSJJ03`。分类目录信息和分类归属
信息分别见股票表 138、139 及基金表 355、356;不能把分类属性代码当作个券代码传给归属表。
级数为 0 的根代码本身没有成份券;要先用根代码加 `.N` 取得一级分类代码。`CSRCHY`
自 2024-02-19 起不再维护,此后使用 `CAPCHY`
## `getBkByIndexName(index_name, end_t)`
声明:function
@@ -100,7 +100,7 @@ return isValueBetweeMaxAndMin(t, 'close', 0, 5);
声明:function
返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数GetBKList,使用方法参见该函数的函数说明
返回市场常用列表,仅包括深证A股、深证B股、深证基金、上证A股、上证B股、上证基金。如果需要获得其他目录下的板块列表,请使用函数getBkList,使用方法参见该函数的函数说明
<!-- tags: 金融 板块 基本情况 其它 返回 获取 -->
@@ -146,7 +146,7 @@ Return GetHyDyFlDetails('行业');
声明:function
获取行业/地域/分类的列表,用于替代GetBKList
获取行业/地域/分类的列表,用于替代getBkList
<!-- tags: 金融 股票 行业 获取 查询 -->
@@ -168,7 +168,7 @@ Return GetHyDyFlList('行业');
声明:function
获取行业的股票。取代GetBK,比其更精确
获取行业的股票。取代getBk,比其更精确
<!-- tags: 金融 股票 行业 获取 查询 -->
@@ -6,16 +6,29 @@ from __future__ import annotations
import argparse
import csv
import json
import re
import sys
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Mapping, Sequence
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from value_domains import ( # noqa: E402
RelatedTable,
ValueDomainCatalog,
ValueMatch,
domains_path_for_index,
)
SKILL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_TSV = SKILL_ROOT / "data" / "dictionary_index.tsv"
DEFAULT_LEXICON = SKILL_ROOT / "data" / "dictionary_lexicon.json"
DEFAULT_VALUE_DOMAINS = SKILL_ROOT / "data" / "value_domains.json"
REQUIRED_COLUMNS = (
"kind",
"scope",
@@ -36,20 +49,24 @@ REQUIRED_COLUMNS = (
)
HELP_EPILOG = """\
按中文业务含义查询表数据源或字段
按中文业务含义查询表数据源字段或参数取值域
dictionary_lookup.py --query "股票现金流指标销售现金比率"
dictionary_lookup.py --query "市值" --scope fund
dictionary_lookup.py --query "买一量" --table TradeTable
dictionary_lookup.py --query "9900700" --field 销售现金比率
dictionary_lookup.py --query "申万煤炭"
输出第一行是 in-band 状态
status: ok 最高分候选唯一
status: ambiguous 多个同分候选必须补充 scopetable field 范围
status: no_match 数据字典无匹配不等于 API not found
status: no_match /字段/参数取值域均无匹配不等于 API not found
参数取值域结果会标明值名称绑定 API关联表和来源动态目录只提供已知样例
完整名称必须使用结果中的运行时解析器重新确认
退出码
0 查询完成okambiguousno_match 均由 status 表达
1 dictionary_index.tsv 或词表缺失损坏格式错误
1 dictionary_index.tsv词表或 value_domains.json 缺失损坏格式错误
2 参数不合法
"""
@@ -73,10 +90,47 @@ class QueryItem:
score: int
@dataclass(frozen=True)
class ValueDomainItem:
kind: str
domain_id: str
domain_label: str
value: str
label: str
parent: str
parent_label: str
level: int | None
valid_from: str
valid_to: str
note: str
related_tables: tuple[str, ...]
bindings: tuple[str, ...]
mode: str
complete: bool
as_of: str
sources: tuple[str, ...]
resolver_text: str
@dataclass(frozen=True)
class ValueDomainSummaryItem:
kind: str
domain_id: str
label: str
related_tables: tuple[str, ...]
bindings: tuple[str, ...]
mode: str
complete: bool
as_of: str
sources: tuple[str, ...]
resolvers: tuple[str, ...]
candidates: tuple[str, ...]
@dataclass(frozen=True)
class QueryResult:
status: str
items: tuple[QueryItem, ...]
items: tuple[QueryItem | ValueDomainItem | ValueDomainSummaryItem, ...]
@dataclass(frozen=True)
@@ -229,6 +283,205 @@ def _item(scored: _ScoredRow) -> QueryItem:
)
def _value_item(
catalog: ValueDomainCatalog,
match: ValueMatch,
related_tables: Sequence[RelatedTable] | None = None,
) -> ValueDomainItem:
value = match.value
hints = catalog.api_hints((match,))
bindings = tuple(dict.fromkeys(hint.target for hint in hints))
verification = []
for hint in hints:
if not hint.verification:
continue
if hint.verification.status == "runtime_required":
call = hint.verification.resolver
if hint.verification.argument:
call += f"('{hint.verification.argument}')"
verification.append(f"{call}(运行时)")
elif hint.verification.status == "documented":
verification.append("文档已确认")
resolver_text = "; ".join(dict.fromkeys(verification))
table = match.domain.related_table(value.related_table)
selected_tables = tuple(related_tables) if related_tables is not None else ()
if not selected_tables:
selected_tables = (table,) if table else match.domain.related_tables
rendered_tables = tuple(
f"{related.table_id} {related.name}"
for related in selected_tables
if related is not None
)
return ValueDomainItem(
kind="parameter_value",
domain_id=match.domain.domain_id,
domain_label=match.domain.label,
value=value.value,
label=value.label,
parent=value.parent,
parent_label=value.parent_label,
level=value.level,
valid_from=value.valid_from,
valid_to=value.valid_to,
note=value.note,
related_tables=rendered_tables,
bindings=bindings,
mode=match.domain.mode,
complete=match.domain.complete,
as_of=value.as_of or match.domain.as_of,
sources=value.sources,
resolver_text=resolver_text,
)
def _domain_summary_items(
catalog: ValueDomainCatalog,
query: str,
*,
scope: str | None,
table: str | None,
limit: int,
) -> tuple[ValueDomainSummaryItem, ...]:
scope_key = _compact(scope or "")
table_key = _compact(table or "")
items = []
for domain in catalog.exact_domains(query):
related_tables = domain.related_tables
if scope_key:
related_tables = tuple(
related
for related in related_tables
if _compact(related.scope) == scope_key
)
if table_key:
related_tables = tuple(
related
for related in related_tables
if table_key in _compact(related.name)
or table_key in _compact(related.table_id)
)
if (scope_key or table_key) and not related_tables:
continue
bindings = tuple(
f"{binding.api}.{binding.parameter}" for binding in domain.bindings
)
resolvers = []
for resolver in domain.resolvers:
catalog_label = {
"system": "系统",
"user": "用户",
}.get(resolver.catalog, resolver.catalog)
resolvers.append(
f"{resolver.api}({resolver.parameter}){catalog_label}目录)"
)
candidates = tuple(
value if not label or label == value else f"{value}{label}"
for value, label in catalog.recorded_candidates(domain)
)
items.append(
ValueDomainSummaryItem(
kind="parameter_domain",
domain_id=domain.domain_id,
label=domain.label,
related_tables=tuple(
f"{related.table_id} {related.name}" for related in related_tables
),
bindings=bindings,
mode=domain.mode,
complete=domain.complete,
as_of=domain.as_of,
sources=domain.sources,
resolvers=tuple(resolvers),
candidates=candidates,
)
)
return tuple(items[:limit])
def _value_matches(
catalog: ValueDomainCatalog,
query: str,
*,
scope: str | None,
table: str | None,
field: str | None,
limit: int,
strong_only: bool,
) -> tuple[ValueDomainItem, ...]:
if field:
return ()
query_terms = tuple(query.split())
matches = catalog.search(query_terms) if len(query_terms) > 1 else ()
if not matches:
matches = catalog.search(query)
if re.fullmatch(r"\s*-?\d{1,2}\s*", query):
matches = tuple(
match
for match in matches
if not re.fullmatch(r"-?\d{1,2}", match.value.value)
)
if strong_only:
query_key = _compact(query)
intent_markers = ("代码", "分类", "属性", "取值", "枚举")
matches = tuple(
match
for match in matches
if match.exact
or (
len(_compact(match.value.value)) >= 4
and any(
character.isascii() and character.isalpha()
for character in match.value.value
)
and any(character.isdigit() for character in match.value.value)
and _compact(match.value.value) in query_key
)
or (
match.domain.mode == "versioned_catalog"
and _compact(match.value.label) in query_key
and any(marker in query for marker in intent_markers)
)
or (
match.domain.mode == "enum"
and _compact(match.domain.label) in query_key
and _compact(match.value.value) in query_key
)
)
filtered = []
scope_key = _compact(scope or "")
table_key = _compact(table or "")
for match in matches:
related_tables = match.domain.related_tables
if match.value.related_table:
related = match.domain.related_table(match.value.related_table)
related_tables = (related,) if related else ()
if scope_key:
related_tables = tuple(
related
for related in related_tables
if _compact(related.scope) == scope_key
)
if table_key:
related_tables = tuple(
related
for related in related_tables
if table_key in _compact(related.name)
or table_key in _compact(related.table_id)
)
if (scope_key or table_key) and not related_tables:
continue
filtered.append((match, related_tables))
matches_with_tables = tuple(filtered)
if not matches_with_tables:
return ()
top_score = matches_with_tables[0][0].score
top = [item for item in matches_with_tables if item[0].score == top_score]
return tuple(
_value_item(catalog, match, related_tables)
for match, related_tables in top[:limit]
)
def query_index(
rows: Iterable[Mapping[str, str]],
query: str,
@@ -243,6 +496,7 @@ def query_index(
raise ValueError("query must not be empty")
if limit < 1:
raise ValueError("limit must be at least 1")
rows = list(rows)
variants = _query_variants(query, lexicon or {})
scope_key = _compact(scope or "")
scored = []
@@ -294,6 +548,26 @@ def query_index(
return QueryResult(status, items)
def _direct_dictionary_items(result: QueryResult, query: str) -> tuple[QueryItem, ...]:
query_key = _compact(query)
if not query_key:
return ()
direct = []
for item in result.items:
if not isinstance(item, QueryItem):
continue
names = (_compact(item.table_name), _compact(item.field_name))
identifiers = (_compact(item.table_id), _compact(item.field_id))
access_code = _compact(item.access_code)
if (
any(name and (query_key == name or name in query_key) for name in names)
or any(identifier and query_key == identifier for identifier in identifiers)
or (len(query_key) >= 3 and query_key in access_code)
):
direct.append(item)
return tuple(direct)
def _render_item(index: int, item: QueryItem) -> list[str]:
return [
f"[{index}]",
@@ -314,14 +588,66 @@ def _render_item(index: int, item: QueryItem) -> list[str]:
]
def _render_value_item(index: int, item: ValueDomainItem) -> list[str]:
validity = ""
if item.valid_from or item.valid_to:
validity = f"{item.valid_from or '不限'}{item.valid_to or ''}"
parent = item.parent or ""
if item.parent_label:
parent = f"{parent}{item.parent_label}"
return [
f"[{index}]",
"类型:参数取值",
f"参数域:{item.domain_id}{item.domain_label}",
f"取值:{item.value}",
f"名称:{item.label}",
f"父级:{parent}",
f"层级:{item.level if item.level is not None else ''}",
f"有效期:{validity}",
f"备注:{item.note}",
f"关联表:{'; '.join(item.related_tables)}",
f"绑定 API{'; '.join(item.bindings)}",
f"模式:{item.mode}",
f"完整性:{'完整' if item.complete else '静态记录不完整'}",
f"快照日期:{item.as_of}",
f"核验:{item.resolver_text}",
f"来源:{', '.join(item.sources)}",
]
def _render_domain_summary_item(
index: int, item: ValueDomainSummaryItem
) -> list[str]:
return [
f"[{index}]",
"类型:参数取值域",
f"参数域:{item.domain_id}",
f"名称:{item.label}",
f"关联表:{'; '.join(item.related_tables)}",
f"绑定 API{'; '.join(item.bindings)}",
f"模式:{item.mode}",
f"完整性:{'完整' if item.complete else '静态记录不完整'}",
f"快照日期:{item.as_of}",
f"运行时解析器:{'; '.join(item.resolvers)}",
f"已记录值或候选:{'; '.join(item.candidates)}",
f"来源:{', '.join(item.sources)}",
]
def render_result(result: QueryResult) -> str:
lines = [f"status: {result.status}"]
if result.status == "no_match":
lines.append("未找到匹配的数据字典条目")
elif result.status == "ambiguous":
lines.append("存在多个同分候选,请补充表或数据源范围")
lines.append("存在多个同分候选,请补充 scope、表、数据源或字段范围")
for index, item in enumerate(result.items, start=1):
lines.extend(["", *_render_item(index, item)])
if isinstance(item, ValueDomainSummaryItem):
renderer = _render_domain_summary_item
elif isinstance(item, ValueDomainItem):
renderer = _render_value_item
else:
renderer = _render_item
lines.extend(["", *renderer(index, item)])
return "\n".join(lines) + "\n"
@@ -337,7 +663,7 @@ def _positive_int(value: str) -> int:
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="查询独立的天软表、数据源字段字典索引",
description="查询天软表、数据源字段及 API 参数取值域",
epilog=HELP_EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False,
@@ -351,15 +677,31 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.add_argument(
"--lexicon", type=Path, default=DEFAULT_LEXICON, help="受控同义词词表路径"
)
parser.add_argument(
"--value-domains",
type=Path,
help="显式指定 value_domains.json;默认使用所选索引同目录的数据,"
"自定义索引旁没有该文件时不加载参数取值域",
)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
domain_path = (
args.value_domains
if args.value_domains
else domains_path_for_index(
args.tsv,
default_index=DEFAULT_TSV,
default_domains=DEFAULT_VALUE_DOMAINS,
)
)
try:
rows = load_rows(args.tsv)
lexicon = load_lexicon(args.lexicon)
result = query_index(
catalog = ValueDomainCatalog.load(domain_path) if domain_path else None
dictionary_result = query_index(
rows,
args.query,
scope=args.scope,
@@ -368,6 +710,59 @@ def main(argv: Sequence[str] | None = None) -> int:
limit=args.limit,
lexicon=lexicon,
)
domain_items = (
_domain_summary_items(
catalog,
args.query,
scope=args.scope,
table=args.table,
limit=args.limit,
)
if catalog and not args.field
else ()
)
value_items = (
_value_matches(
catalog,
args.query,
scope=args.scope,
table=args.table,
field=args.field,
limit=args.limit,
strong_only=True,
)
if catalog
else ()
)
if domain_items:
result = QueryResult(
"ambiguous" if len(domain_items) > 1 else "ok",
domain_items,
)
elif value_items:
dictionary_items = _direct_dictionary_items(dictionary_result, args.query)
combined = (*value_items, *dictionary_items)[: args.limit]
result = QueryResult(
"ambiguous" if len(value_items) > 1 or dictionary_items else "ok",
combined,
)
else:
result = dictionary_result
if result.status == "no_match" and catalog:
fallback_items = _value_matches(
catalog,
args.query,
scope=args.scope,
table=args.table,
field=args.field,
limit=args.limit,
strong_only=False,
)
if fallback_items:
result = QueryResult(
"ambiguous" if len(fallback_items) > 1 else "ok",
fallback_items,
)
except (OSError, ValueError, json.JSONDecodeError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
+138 -4
View File
@@ -6,6 +6,16 @@ import sys
import unicodedata
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from value_domains import ( # noqa: E402
ApiHint,
ValueDomainCatalog,
domains_path_for_index,
)
TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$")
PLAIN_H2_RE = re.compile(r"^##(?!#)\s+")
BARE_ENTRY_RE = re.compile(r"^(#{3,4})(?!#)\s+`(.+?)`\s*$")
@@ -26,6 +36,7 @@ CLASS_MEMBER_LABELS = {
UNIT_DIRECT_LABELS = {"function", "var", "const", "class"}
SKILL_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_TSV = SKILL_ROOT / "data" / "function_index.tsv"
DEFAULT_VALUE_DOMAINS = SKILL_ROOT / "data" / "value_domains.json"
DEFAULT_CODEGEN_ROOT = SKILL_ROOT / "references" / "codegen"
REQUIRED_COLUMNS = (
"name",
@@ -77,13 +88,14 @@ HELP_EPILOG = """\
不存在的 scope 会列出当前索引实际提供的值并返回 2
结果
--kw 输出候选摘要表选定后仍须运行 --name
--kw 输出候选摘要表参数取值域可补充召回相关 API选定后仍须运行 --name
--name 输出完整条目正文和 scope/modulepage#anchor 来源标记;
简单成员名可能返回多个 ownerqualified_name 可消歧重载会全部返回
简单成员名可能返回多个 ownerqualified_name 可消歧重载会全部返回
若该 API 有参数取值域同时显示动态目录解析器和已核实值边界
退出码
0 查询完成--name 无匹配和 --kw 零候选也通过正文提示表达
1 function_index.tsv 缺失格式错误或候选指向的条目正文找不到
1 function_index.tsv/value_domains.json 缺失格式错误或候选指向的条目正文找不到
2 参数不合法缺少动作空值--limit 小于 1使用短选项或缩写
"""
@@ -366,6 +378,91 @@ def format_row(row):
)
def row_identity(row):
return (
normalize(row.get("qualified_name", "") or row.get("name", "")),
normalize(row.get("signature", "")),
normalize(row.get("scope", "")),
row.get("page", ""),
row.get("anchor", ""),
)
def signature_parameters(signature):
match = re.search(r"\((.*)\)", signature)
if not match:
return set()
parameters = set()
for raw_parameter in match.group(1).split(","):
parameter = raw_parameter.strip().strip("[]").strip()
parameter = re.sub(r"^(?:const|out|var)\s+", "", parameter, flags=re.I)
parameter = parameter.split("=", maxsplit=1)[0].strip()
parameter = parameter.split(":", maxsplit=1)[0].strip()
if parameter:
parameters.add(normalize(parameter))
return parameters
def rows_for_hint(rows, hint):
api = normalize(hint.api)
parameter = normalize(hint.parameter)
return [
row
for row in rows
if api
in {
normalize(row.get("name", "")),
normalize(row.get("qualified_name", "")),
}
and normalize(row.get("scope", "")) == normalize(hint.scope)
and normalize(row.get("module", "")) == normalize(hint.module)
and parameter in signature_parameters(row.get("signature", ""))
]
def merge_keyword_matches(rows, normal_matches, hints):
merged = []
annotations = {}
seen = set()
for hint in hints:
for row in rows_for_hint(rows, hint):
key = row_identity(row)
annotations.setdefault(key, []).append(hint)
if key not in seen:
seen.add(key)
merged.append(row)
for row in normal_matches:
key = row_identity(row)
if key not in seen:
seen.add(key)
merged.append(row)
return merged, annotations
def format_api_hint(hint: ApiHint) -> str:
parts = [f" 参数取值域:{hint.target}", hint.summary()]
verification = hint.verification_text()
if verification:
parts.append(verification)
if hint.sources:
parts.append(f"来源:{', '.join(hint.sources)}")
return " · ".join(parts)
def top_value_domain_matches(matches, *, score_window=50):
matches = tuple(matches)
if not matches:
return ()
minimum_score = matches[0].score - score_window
return tuple(match for match in matches if match.score >= minimum_score)
def value_domain_keyword_matches(catalog, terms):
if len(terms) == 1 and re.fullmatch(r"\s*-?\d{1,2}\s*", terms[0]):
return ()
return top_value_domain_matches(catalog.search(terms))
def codegen_root_for_tsv(tsv_path):
if tsv_path.parent.name == "data":
candidate = tsv_path.parent.parent / "references" / "codegen"
@@ -423,6 +520,12 @@ def main(argv=None):
help="显式指定 function_index.tsv;默认只使用本 skill 内置的 "
"data/function_index.tsv,不从工作目录回退查找旧索引",
)
parser.add_argument(
"--value-domains",
metavar="PATH",
help="显式指定 value_domains.json;默认使用所选索引同目录的数据,"
"自定义索引旁没有该文件时不加载参数取值域",
)
parser.add_argument(
"--scope",
type=non_empty,
@@ -451,11 +554,25 @@ def main(argv=None):
)
return 1
domain_path = (
Path(args.value_domains)
if args.value_domains
else domains_path_for_index(
tsv_path,
default_index=DEFAULT_TSV,
default_domains=DEFAULT_VALUE_DOMAINS,
)
)
try:
rows = load_rows(tsv_path)
except (OSError, UnicodeError, ValueError) as error:
print(f"ERROR: failed to load {tsv_path}: {error}", file=sys.stderr)
return 1
try:
catalog = ValueDomainCatalog.load(domain_path) if domain_path else None
except (OSError, UnicodeError, ValueError) as error:
print(f"ERROR: failed to load {domain_path}: {error}", file=sys.stderr)
return 1
if not rows:
print(
f"ERROR: {tsv_path} has no entries; reinstall tsl-api-reference "
@@ -499,17 +616,34 @@ def main(argv=None):
entries.append((row, body))
for row, body in entries:
print(body)
if catalog:
domain_text = catalog.describe_api(
row.get("qualified_name", "") or row.get("name", ""),
scope=row.get("scope", ""),
module=row.get("module", ""),
parameters=signature_parameters(row.get("signature", "")),
)
if domain_text:
print(f"\n{domain_text}", end="")
print(
f"\n<!-- {row['scope']}/{row['module']} · "
f"{row['page']}#{row['anchor']} -->\n"
)
return 0
matches = search_keyword(rows, args.kw)
normal_matches = search_keyword(rows, args.kw)
hints = (
catalog.api_hints(value_domain_keyword_matches(catalog, args.kw))
if catalog
else ()
)
matches, annotations = merge_keyword_matches(rows, normal_matches, hints)
print(f"# {len(matches)} matches for: {' '.join(args.kw)}")
print("qualified_name\tdeclaration\tsignature\tpage#anchor\tsummary")
for row in matches[: args.limit]:
print(format_row(row))
for hint in annotations.get(row_identity(row), ()):
print(format_api_hint(hint))
if len(matches) > args.limit:
print(f"... {len(matches) - args.limit} more; refine keywords or raise --limit")
if not matches:
@@ -0,0 +1,737 @@
#!/usr/bin/env python3
"""Load and query bundled TSL API parameter value domains."""
from __future__ import annotations
import json
import re
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Sequence
DOMAIN_MODES = {
"catalog",
"enum",
"pattern",
"runtime_catalog",
"versioned_catalog",
}
DOMAIN_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
def _fold(value: str) -> str:
normalized = unicodedata.normalize("NFKC", value).casefold()
return "".join(
character
for character in normalized
if character in {"_", "."}
or unicodedata.category(character)[0] in {"L", "N"}
)
def _fold_value(value: str) -> str:
"""Normalize a domain value without conflating negative and positive numbers."""
normalized = unicodedata.normalize("NFKC", value).casefold()
return "".join(
character
for character in normalized
if character in {"-", "_", "."}
or unicodedata.category(character)[0] in {"L", "N"}
)
def _required_string(value: object, where: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{where} must be a non-empty string")
return value.strip()
def _optional_string(value: object, where: str) -> str:
if value is None:
return ""
if not isinstance(value, str):
raise ValueError(f"{where} must be a string")
return value.strip()
def _string_list(value: object, where: str, *, required: bool = False) -> tuple[str, ...]:
if not isinstance(value, list):
raise ValueError(f"{where} must be a list")
items = tuple(_required_string(item, f"{where}[]") for item in value)
if required and not items:
raise ValueError(f"{where} must not be empty")
if len({_fold(item) for item in items}) != len(items):
raise ValueError(f"{where} contains duplicate values")
return items
def _mapping(value: object, where: str) -> dict:
if not isinstance(value, dict):
raise ValueError(f"{where} must be an object")
return value
def _list(value: object, where: str) -> list:
if not isinstance(value, list):
raise ValueError(f"{where} must be a list")
return value
def _reject_unknown(value: dict, allowed: set[str], where: str) -> None:
unknown = sorted(set(value) - allowed)
if unknown:
raise ValueError(f"{where} contains unknown fields: {', '.join(unknown)}")
@dataclass(frozen=True)
class Binding:
scope: str
module: str
api: str
parameter: str
role: str
@dataclass(frozen=True)
class Resolver:
scope: str
module: str
api: str
parameter: str
catalog: str
source: str
@dataclass(frozen=True)
class RelatedTable:
table_id: str
name: str
scope: str
page: str
@dataclass(frozen=True)
class Verification:
status: str
scope: str
module: str
resolver: str
parameter: str
argument: str
@dataclass(frozen=True)
class Relation:
kind: str
domain_id: str
value: str
verification: Verification
sources: tuple[str, ...]
@dataclass(frozen=True)
class DomainValue:
value: str
label: str
aliases: tuple[str, ...]
parent: str
parent_label: str
level: int | None
valid_from: str
valid_to: str
note: str
related_table: str
as_of: str
sources: tuple[str, ...]
relations: tuple[Relation, ...]
@dataclass(frozen=True)
class ValueDomain:
domain_id: str
label: str
mode: str
complete: bool
bindings: tuple[Binding, ...]
resolvers: tuple[Resolver, ...]
related_tables: tuple[RelatedTable, ...]
values: tuple[DomainValue, ...]
sources: tuple[str, ...]
as_of: str
def related_table(self, table_id: str) -> RelatedTable | None:
return next(
(table for table in self.related_tables if table.table_id == table_id),
None,
)
@dataclass(frozen=True)
class ValueMatch:
domain: ValueDomain
value: DomainValue
score: int
exact: bool
@dataclass(frozen=True)
class ApiHint:
scope: str
module: str
api: str
parameter: str
role: str
domain: ValueDomain
value: str
label: str
verification: Verification | None
sources: tuple[str, ...]
@property
def target(self) -> str:
return f"{self.api}.{self.parameter}"
def summary(self) -> str:
if self.role == "historical_components":
text = f"历史分类代码:{self.value}"
if self.label and self.label != self.value:
text += f"{self.label}"
return text
if self.role == "historical_market_components":
text = f"历史市场板块代码:{self.value}"
if self.label and self.label != self.value:
text += f"{self.label}"
return text
if self.role == "historical_named_components":
return f"历史市场板块名:{self.value}"
if self.role == "current_components":
prefix = (
"当前板块候选"
if self.verification
and self.verification.status == "runtime_required"
else "当前板块名"
)
return f"{prefix}{self.value}"
if self.role == "catalog_selector":
return f"目录选择器:{self.value}"
if self.role == "child_codes":
return f"下级分类代码模式:{self.value}"
text = f"参数值:{self.value}"
if self.label and self.label != self.value:
text += f"{self.label}"
return text
def verification_text(self) -> str:
if not self.verification:
return ""
call = self.verification.resolver
if self.verification.argument:
call += f"('{self.verification.argument}')"
if self.verification.status == "runtime_required":
return f"运行时核验:{call}"
if self.verification.status == "documented":
return "文档已确认"
return self.verification.status
class ValueDomainCatalog:
def __init__(self, domains: Sequence[ValueDomain]):
self.domains = tuple(domains)
self._by_id = {domain.domain_id: domain for domain in self.domains}
@classmethod
def load(cls, path: Path) -> "ValueDomainCatalog":
return cls(_parse_document(json.loads(path.read_text(encoding="utf-8"))))
def search(self, terms: str | Iterable[str]) -> tuple[ValueMatch, ...]:
raw_terms = [terms] if isinstance(terms, str) else list(terms)
normalized_terms = tuple(
_fold_value(term) for term in raw_terms if _fold_value(term)
)
if not normalized_terms:
return ()
matches = []
for domain in self.domains:
for value in domain.values:
scored = _score_value(domain, value, normalized_terms)
if scored:
score, exact = scored
matches.append(ValueMatch(domain, value, score, exact))
matches.sort(
key=lambda match: (
-match.score,
_fold(match.domain.domain_id),
_fold(match.value.value),
)
)
return tuple(matches)
def exact_domains(self, query: str) -> tuple[ValueDomain, ...]:
query_key = _fold_value(query)
if not query_key:
return ()
return tuple(
domain
for domain in self.domains
if query_key
in {
_fold_value(domain.domain_id),
_fold_value(domain.label),
}
)
def recorded_candidates(
self, domain: ValueDomain
) -> tuple[tuple[str, str], ...]:
candidates = [(value.value, value.label) for value in domain.values]
candidates.extend(
(relation.value, value.label)
for source_domain in self.domains
for value in source_domain.values
for relation in value.relations
if relation.domain_id == domain.domain_id
)
unique = []
seen = set()
for value, label in candidates:
key = _fold_value(value)
if key in seen:
continue
seen.add(key)
unique.append((value, label))
return tuple(unique)
def api_hints(self, matches: Iterable[ValueMatch]) -> tuple[ApiHint, ...]:
hints = []
seen = set()
for match in matches:
for binding in match.domain.bindings:
hint = ApiHint(
scope=binding.scope,
module=binding.module,
api=binding.api,
parameter=binding.parameter,
role=binding.role,
domain=match.domain,
value=match.value.value,
label=match.value.label,
verification=None,
sources=match.value.sources,
)
key = _hint_key(hint)
if key not in seen:
seen.add(key)
hints.append(hint)
for relation in match.value.relations:
target = self._by_id[relation.domain_id]
for binding in target.bindings:
hint = ApiHint(
scope=binding.scope,
module=binding.module,
api=binding.api,
parameter=binding.parameter,
role=binding.role,
domain=target,
value=relation.value,
label=match.value.label,
verification=relation.verification,
sources=relation.sources,
)
key = _hint_key(hint)
if key not in seen:
seen.add(key)
hints.append(hint)
hints.sort(
key=lambda hint: (
_fold(hint.api),
_fold(hint.parameter),
hint.summary(),
)
)
return tuple(hints)
def describe_api(
self,
api: str,
*,
scope: str = "",
module: str = "",
parameters: Iterable[str] | None = None,
) -> str:
api_key = _fold(api)
scope_key = _fold(scope)
module_key = _fold(module)
parameter_keys = (
{_fold(parameter) for parameter in parameters}
if parameters is not None
else None
)
bound = [
(domain, binding)
for domain in self.domains
for binding in domain.bindings
if _fold(binding.api) == api_key
and (not scope_key or _fold(binding.scope) == scope_key)
and (not module_key or _fold(binding.module) == module_key)
and (
parameter_keys is None
or _fold(binding.parameter) in parameter_keys
)
]
if not bound:
return ""
lines = ["### 参数取值域", ""]
for domain, binding in bound:
completeness = "完整" if domain.complete else "静态记录不完整"
lines.append(
f"- `{binding.parameter}` -> `{domain.domain_id}`{domain.label}"
f"`{domain.mode}`{completeness}"
)
lines.append(f" - 快照日期:`{domain.as_of}`")
sources = ", ".join(f"`{source}`" for source in domain.sources)
lines.append(f" - 来源:{sources}")
for resolver in domain.resolvers:
catalog = {"system": "系统", "user": "用户"}.get(
resolver.catalog, resolver.catalog
)
lines.append(
f" - 运行时解析器:`{resolver.api}`{catalog}目录;"
f"来源 `{resolver.source}`"
)
recorded_count = len(self.recorded_candidates(domain))
if domain.complete and domain.mode in {"enum", "pattern"}:
for value in domain.values:
label = f"{value.label}" if value.label != value.value else ""
note = f"{value.note}" if value.note else ""
lines.append(f" - `{value.value}`{label}{note}")
else:
lines.append(f" - 已记录值或候选:{recorded_count} 条(此处不展开)")
lines.append(
" - 按值查询:`dictionary_lookup.py --query "
"\"<域 ID、名称、代码或模式>\"`"
)
return "\n".join(lines) + "\n"
def domains_path_for_index(
index_path: Path, *, default_index: Path, default_domains: Path
) -> Path | None:
if index_path.parent.name == "data":
sibling = index_path.parent / default_domains.name
if sibling.is_file():
return sibling
try:
is_default = index_path.resolve() == default_index.resolve()
except OSError:
is_default = index_path == default_index
return default_domains if is_default else None
def _parse_document(document: object) -> tuple[ValueDomain, ...]:
root = _mapping(document, "value domain document")
_reject_unknown(root, {"version", "domains"}, "value domain document")
if root.get("version") != 1:
raise ValueError("value domain document version must be 1")
domain_items = _list(root.get("domains"), "domains")
if not domain_items:
raise ValueError("domains must not be empty")
domains = tuple(
_parse_domain(item, f"domains[{index}]")
for index, item in enumerate(domain_items)
)
ids = [domain.domain_id for domain in domains]
if len(set(ids)) != len(ids):
raise ValueError("domains contains duplicate ids")
known = set(ids)
by_id = {domain.domain_id: domain for domain in domains}
for domain in domains:
table_ids = {table.table_id for table in domain.related_tables}
for value in domain.values:
if value.related_table and value.related_table not in table_ids:
raise ValueError(
f"domain {domain.domain_id!r} value {value.value!r} references "
f"unknown related table {value.related_table!r}"
)
for relation in value.relations:
if relation.domain_id not in known:
raise ValueError(
f"domain {domain.domain_id!r} value {value.value!r} references "
f"unknown domain {relation.domain_id!r}"
)
target = by_id[relation.domain_id]
verification_key = (
_fold(relation.verification.scope),
_fold(relation.verification.module),
_fold(relation.verification.resolver),
_fold(relation.verification.parameter),
)
resolver_keys = {
(
_fold(resolver.scope),
_fold(resolver.module),
_fold(resolver.api),
_fold(resolver.parameter),
)
for resolver in target.resolvers
}
if verification_key not in resolver_keys:
raise ValueError(
f"domain {domain.domain_id!r} value {value.value!r} "
f"references a resolver not owned by {relation.domain_id!r}"
)
return domains
def _parse_domain(value: object, where: str) -> ValueDomain:
item = _mapping(value, where)
_reject_unknown(
item,
{
"id",
"label",
"mode",
"complete",
"bindings",
"resolvers",
"related_tables",
"values",
"sources",
"as_of",
},
where,
)
domain_id = _required_string(item.get("id"), f"{where}.id")
if not DOMAIN_ID_RE.fullmatch(domain_id):
raise ValueError(f"{where}.id must use lowercase snake_case")
mode = _required_string(item.get("mode"), f"{where}.mode")
if mode not in DOMAIN_MODES:
raise ValueError(f"{where}.mode must be one of {', '.join(sorted(DOMAIN_MODES))}")
complete = item.get("complete")
if not isinstance(complete, bool):
raise ValueError(f"{where}.complete must be a boolean")
if mode == "enum" and not complete:
raise ValueError(f"{where}.complete must be true for mode enum")
if mode == "runtime_catalog" and complete:
raise ValueError(f"{where}.complete must be false for mode runtime_catalog")
bindings = tuple(
_parse_binding(binding, f"{where}.bindings[{index}]")
for index, binding in enumerate(_list(item.get("bindings"), f"{where}.bindings"))
)
if not bindings:
raise ValueError(f"{where}.bindings must not be empty")
resolvers = tuple(
_parse_resolver(resolver, f"{where}.resolvers[{index}]")
for index, resolver in enumerate(_list(item.get("resolvers"), f"{where}.resolvers"))
)
if mode == "runtime_catalog" and not resolvers:
raise ValueError(f"{where}.resolvers must not be empty for mode runtime_catalog")
tables = tuple(
_parse_table(table, f"{where}.related_tables[{index}]")
for index, table in enumerate(
_list(item.get("related_tables"), f"{where}.related_tables")
)
)
values = tuple(
_parse_value(domain_value, f"{where}.values[{index}]")
for index, domain_value in enumerate(_list(item.get("values"), f"{where}.values"))
)
value_keys = [_fold_value(domain_value.value) for domain_value in values]
if len(set(value_keys)) != len(value_keys):
raise ValueError(f"{where}.values contains duplicate values")
if mode != "runtime_catalog" and not values:
raise ValueError(f"{where}.values must not be empty for mode {mode}")
return ValueDomain(
domain_id=domain_id,
label=_required_string(item.get("label"), f"{where}.label"),
mode=mode,
complete=complete,
bindings=bindings,
resolvers=resolvers,
related_tables=tables,
values=values,
sources=_string_list(item.get("sources"), f"{where}.sources", required=True),
as_of=_required_string(item.get("as_of"), f"{where}.as_of"),
)
def _parse_binding(value: object, where: str) -> Binding:
item = _mapping(value, where)
_reject_unknown(item, {"scope", "module", "api", "parameter", "role"}, where)
return Binding(
scope=_required_string(item.get("scope"), f"{where}.scope"),
module=_required_string(item.get("module"), f"{where}.module"),
api=_required_string(item.get("api"), f"{where}.api"),
parameter=_required_string(item.get("parameter"), f"{where}.parameter"),
role=_required_string(item.get("role"), f"{where}.role"),
)
def _parse_resolver(value: object, where: str) -> Resolver:
item = _mapping(value, where)
_reject_unknown(
item,
{"scope", "module", "api", "parameter", "catalog", "source"},
where,
)
return Resolver(
scope=_required_string(item.get("scope"), f"{where}.scope"),
module=_required_string(item.get("module"), f"{where}.module"),
api=_required_string(item.get("api"), f"{where}.api"),
parameter=_required_string(item.get("parameter"), f"{where}.parameter"),
catalog=_required_string(item.get("catalog"), f"{where}.catalog"),
source=_required_string(item.get("source"), f"{where}.source"),
)
def _parse_table(value: object, where: str) -> RelatedTable:
item = _mapping(value, where)
_reject_unknown(item, {"id", "name", "scope", "page"}, where)
return RelatedTable(
table_id=_required_string(item.get("id"), f"{where}.id"),
name=_required_string(item.get("name"), f"{where}.name"),
scope=_required_string(item.get("scope"), f"{where}.scope"),
page=_required_string(item.get("page"), f"{where}.page"),
)
def _parse_value(value: object, where: str) -> DomainValue:
item = _mapping(value, where)
_reject_unknown(
item,
{
"value",
"label",
"aliases",
"parent",
"parent_label",
"level",
"valid_from",
"valid_to",
"note",
"related_table",
"as_of",
"sources",
"relations",
},
where,
)
level = item.get("level")
if level is not None and (not isinstance(level, int) or level < 0):
raise ValueError(f"{where}.level must be a non-negative integer")
return DomainValue(
value=_required_string(item.get("value"), f"{where}.value"),
label=_required_string(item.get("label"), f"{where}.label"),
aliases=_string_list(item.get("aliases", []), f"{where}.aliases"),
parent=_optional_string(item.get("parent"), f"{where}.parent"),
parent_label=_optional_string(item.get("parent_label"), f"{where}.parent_label"),
level=level,
valid_from=_optional_string(item.get("valid_from"), f"{where}.valid_from"),
valid_to=_optional_string(item.get("valid_to"), f"{where}.valid_to"),
note=_optional_string(item.get("note"), f"{where}.note"),
related_table=_optional_string(
item.get("related_table"), f"{where}.related_table"
),
as_of=_optional_string(item.get("as_of"), f"{where}.as_of"),
sources=_string_list(item.get("sources"), f"{where}.sources", required=True),
relations=tuple(
_parse_relation(relation, f"{where}.relations[{index}]")
for index, relation in enumerate(
_list(item.get("relations", []), f"{where}.relations")
)
),
)
def _parse_relation(value: object, where: str) -> Relation:
item = _mapping(value, where)
_reject_unknown(item, {"kind", "domain", "value", "verification", "sources"}, where)
verification_item = _mapping(item.get("verification"), f"{where}.verification")
_reject_unknown(
verification_item,
{"status", "scope", "module", "resolver", "parameter", "argument"},
f"{where}.verification",
)
status = _required_string(
verification_item.get("status"), f"{where}.verification.status"
)
if status not in {"documented", "runtime_required"}:
raise ValueError(
f"{where}.verification.status must be documented or runtime_required"
)
return Relation(
kind=_required_string(item.get("kind"), f"{where}.kind"),
domain_id=_required_string(item.get("domain"), f"{where}.domain"),
value=_required_string(item.get("value"), f"{where}.value"),
verification=Verification(
status=status,
scope=_required_string(
verification_item.get("scope"), f"{where}.verification.scope"
),
module=_required_string(
verification_item.get("module"), f"{where}.verification.module"
),
resolver=_required_string(
verification_item.get("resolver"), f"{where}.verification.resolver"
),
parameter=_required_string(
verification_item.get("parameter"),
f"{where}.verification.parameter",
),
argument=_optional_string(
verification_item.get("argument"), f"{where}.verification.argument"
),
),
sources=_string_list(item.get("sources"), f"{where}.sources", required=True),
)
def _score_value(
domain: ValueDomain, value: DomainValue, terms: Sequence[str]
) -> tuple[int, bool] | None:
fields = (
(value.value, 600),
(value.label, 560),
*((alias, 520) for alias in value.aliases),
(value.parent, 260),
(value.parent_label, 300),
(domain.label, 180),
(domain.domain_id, 160),
)
folded_fields = tuple(
(_fold_value(text), weight) for text, weight in fields if _fold_value(text)
)
scores = []
exact = False
for term in terms:
best = 0
for field, weight in folded_fields:
if term == field:
best = max(best, weight + 500)
exact = exact or len(terms) == 1
elif len(term) < 2:
continue
elif term in field:
best = max(best, weight)
elif len(field) >= 2 and field in term:
best = max(best, weight - 80)
if not best:
return None
scores.append(best)
same_field_bonus = 300 if any(all(term in field for term in terms) for field, _ in folded_fields) else 0
return sum(scores) + same_field_bonus, exact
def _hint_key(hint: ApiHint) -> tuple[str, ...]:
return (
_fold(hint.scope),
_fold(hint.module),
_fold(hint.api),
_fold(hint.parameter),
_fold(hint.value),
hint.role,
hint.verification.status if hint.verification else "",
)
@@ -1,6 +1,6 @@
# API 检索
用于普通函数、具体成员“有没有某种行为的 API”这类请求。
用于普通函数、具体成员“有没有某种行为的 API”或“某个 API 的参数可以填什么”这类请求。
先查看脚本当前契约:
@@ -25,8 +25,9 @@ python <this-skill-dir>/scripts/lookup.py --name arrDropDuplicate
```
`--kw` 使用字面 AND 语义,搜索名称、签名、tags、summary、scope、module、
`qualified_name`、owner、kind、binding 和 visibility,并只输出候选摘要。选定候选后,
必须再用 `--name` 取正文;不得把 summary 当作签名。
`qualified_name`、owner、kind、binding 和 visibility;如果关键词命中参数取值域,结果会
在对应 API 候选下附上参数名、值/名称、绑定关系和来源。它仍只输出候选摘要,选定候选后
必须再用 `--name` 取正文;不得把 summary 或参数域样例当作签名。
已知范围时用当前索引实际提供的 scope 过滤,例如:
@@ -55,4 +56,7 @@ binding 和 visibility。使用自定义 `--tsv` 时,只有索引实际包含
- rc=1:索引缺失、格式错误、为空,或候选指向的正文不可达;这是数据/部署错误。
- rc=2:参数不合法;按 stderr 和 `--help` 修正后重跑。
`--name` 成功输出完整条目正文和来源标记;最终报告按 `SKILL.md` 的输出约定组织。
`--name` 成功输出完整条目正文和来源标记;若 API 绑定参数取值域,还会显示域模式、
完整性、快照和系统/用户运行时解析器。完整的封闭枚举或值模式会直接列出;不完整的版本
目录和运行时目录只显示已记录数量,用 `dictionary_lookup.py --query` 按域 ID、名称、代码
或模式取回。最终报告按 `SKILL.md` 的输出约定组织。
@@ -1,167 +0,0 @@
# 分类属性代码(行业与基金分类)
天软把「个券归属会随时间变更」的分类维护成**分类属性**,用一套属性代码
(如 `SWHY210000`)标识。分类目录本身和个券归属分别放在不同的字典表里,
取数入口也不同。本页收拢这类分类的代码体系与取数路径。
本页是手写整理页,不由字典生成器产出;`references/data_dictionary/` 下的页面
是生成产物,不要把这里的内容写回那些页面。
## 目前维护的分类属性
| 资产 | 所属属性代码 | 分类名称 | 级数 | 备注 |
| ---- | ------------ | ------------------ | ---- | --------------------------- |
| 股票 | `SWHY` | 申万行业 | 三级 | |
| 股票 | `CAPCHY` | 中上协行业 | 二级 | 2024-02-19 起接替证监会行业 |
| 股票 | `CSRCHY` | 证监会行业 | 二级 | **2024-02-19 起不再维护** |
| 基金 | `YHFL` | 银河基金分类 | 二级 | 数据起始 2021-01-04 |
| 基金 | `TSJJ02` | 投资风格 | 二级 | |
| 基金 | `TSJJ03` | 净值增长率计算方法 | 二级 | |
属性代码的构成是「所属属性代码 + 层级编号」,例如 `SWHY``SWHY210000`(申万采掘,
一级)→ 其下二级、三级继续细分;`CSRCHY``CSRCHYA`(农、林、牧、渔业)→
`CSRCHYA01`(农业)。
## 两类表:分类目录 vs 个券归属
分类目录(有哪些分类、上下级关系、启用与剔除日期)和个券归属(某只券在某段时间属于
哪个分类)是两张不同的表,访问代码不同:
| 表 ID | 表名 | 访问代码 | 用途 |
| ----- | --------------------- | ------------------------------------- | ------------------ |
| 138 | 股票.股票行业分类信息 | **属性代码**,如 `SWHY``SWHY110000` | 股票分类目录 |
| 355 | 基金.基金分类信息 | **属性代码**,如 `YHFL``YHFL010000` | 基金分类目录 |
| 356 | 基金.基金分类 | **基金代码**,如 `OF000316` | 单只基金的分类归属 |
138 与 355 的字段结构一致:`属性代码``属性名称``级数``上级属性代码`
`上级属性名称``入选日期``剔除日期``最新标识``所属属性代码`
传错访问代码是这里最常见的错误:把基金代码传给 355、或把属性代码传给 356,都不会报错,
只会返回空表。字段清单与取数示例见字典页,用
`dictionary_lookup.py --query 股票行业分类信息` / `--query 基金分类信息` 取回。
## 取分类目录
按属性代码取某个分类节点及其记录:
```tsl
// 申万行业分类目录(全部层级)
return select * from infotable 138 of "SWHY" end;
```
`getbk(板块名)` 可以一次取回某个分类下的全部属性代码,再喂给 `infotable`
```tsl
// 申万行业的全部分类属性代码,按级数分组展示
codes := getbk("申万行业");
t := select * from infotable 138 of codes end;
return select ['级数'], select * from thisgroup end as '分类列表'
from t group by ['级数'] order by ['级数'] end;
```
`getbk("属性代码")` 是跨资产的总入口,同时覆盖股票与基金两侧:
```tsl
codes := getbk("属性代码");
t1 := select "股票" as '资产', * from infotable 138 of codes end;
t2 := select "基金" as '资产', * from infotable 355 of codes end;
return t1 union t2;
```
`getbk` 取的是**最新**成分,且入参是板块名称而非代码;签名与板块名约束见
`lookup.py --name getbk`
## 沿层级向下走
分类属性代码后缀 `.N` 表示「该属性下的所有下级分类属性代码」:
```tsl
// 指定日所有申万一级行业代码
return getbkbydate("SWHY.N", 20210423T);
// 指定一级行业下的二级行业代码
return getbkbydate("SWHY210000" $ ".N", 20210423T);
```
三级同理,用二级代码加 `.N`。也有直接返回下级列表的函数,签名用
`lookup.py --name stockIndustryLowerArr` / `--name fundYHIndustryArr` 取回。
## 取分类成分
不带 `.N` 时,`getBkByDate` 把属性代码当分类节点,返回该分类在指定日的成分券:
```tsl
return getbkbydate("SWHY740000", 20220101T); // 申万煤炭成分股
return getbkbydate("CAPCHYD", 20240219T); // 中上协:电力、热力、燃气及水
return getbkbydate("YHFL030000", 20220101T); // 银河债券型成分基金
return getbkbydate("TSJJ0201", 20220101T); // 投资风格:股票型
```
`getBkByDate` 的字典条目只描述了指数代码入参,分类属性代码入参同样成立;两种入参
共用一个函数。签名与 `extype` 参数见 `lookup.py --name getBkByDate`
## 行业分类代码 ≠ 行业指数代码
同一个申万行业有两套代码,用途不同,混用会取到空结果:
| 用途 | 代码形态 | 示例 |
| -------------------- | ------------------------ | ----------------------------- |
| 行业**分类**属性代码 | `SWHY` + 层级编号 | `SWHY210000` 申万采掘 |
| 行业**指数**代码 | `SWHY00000N`N 为级数) | `SWHY000001` 申万一级行业指数 |
取行业指数代码序列走指数侧:
```tsl
// 指定日所有申万一级行业指数代码
return getbkbydate("SWHY000001", 20190201T);
// 最新的行业指数序列也可按板块名取
return getbk("申万一级行业指数");
```
二级、三级分别对应 `SWHY000002` / `SWHY000003`,板块名分别是
`申万二级行业指数` / `申万三级行业指数`。代码转名称用
`lookup.py --name stockName`
## 反查:个券在指定日属于哪个分类
申万各级有成对的名称/编码查询函数,与系统参数「证券」「日期」相关:
```tsl
setSysParam(pn_stock(), "SZ000002");
setSysParam(pn_date(), 20200101T);
return array("一级名称": stockSWIndustryName1(),
"一级编码": stockSWIndustryID1(),
"二级名称": stockSWIndustryName2(),
"二级编码": stockSWIndustryID2(),
"三级名称": stockSWIndustryName3(),
"三级编码": stockSWIndustryID3());
```
批量反查配合 `spec(...)` 逐券切换系统参数:
```tsl
endt := 20240103T;
stocks := getbkbydate("SH000016", endt);
setSysParam(pn_date(), endt);
return select thisRow as "sid",
spec(stockSWIndustryName1(), thisRow) as "一级行业名称",
spec(stockSWIndustryID1(), thisRow) as "一级行业编码"
from stocks end;
```
这六个函数无参数,签名逐个用 `lookup.py --name stockSWIndustryName1` 等确认。
按板块分类批量反查另有 `stocksbklist`,其 `bktype` 是整数板块类别,取值不在本页
事实范围内,用前先确认。
## 其它取数代码
少量非个券维度的数据也用代码位取:`HG000001` 宏观数据表格,`HG000001``HG000004`
分别对应港股通(沪)、沪股通、港股通(深)、深股通方向。其余特别数据(如国债收益率
曲线)按字典页或专题文档确认,不在本页枚举。
## 事实边界
本页只给代码体系与取数路径。函数签名、参数类型、返回形态一律以
`lookup.py --name` 的条目正文为准,本页示例不作为签名依据。具体分类下有哪些属性代码
会随上游调整,不在本页硬编码;用上面的 `getbk` + `infotable` 组合在运行时取回当前列表。
@@ -1,194 +0,0 @@
# 证券代码序列取数
「先拿到一批证券代码,再逐个取指标」是天软里最常见的起手动作。代码序列有三条互不
等价的取数路径,选错路径会静默拿到错误的样本集:
| 路径 | 函数 | 时点语义 | 入参 |
| ----------------- | -------------- | ------------------ | -------------------------- |
| 板块最新成分 | `getbk` | **最新**,不带日期 | 板块中文名 |
| 板块历史成分 | `getAbkbyDate` | 指定日 | 板块中文名(仅限部分板块) |
| 指数/分类历史成分 | `getBkByDate` | 指定日 | 指数代码或分类属性代码 |
`getbk` 没有日期入参,拿到的永远是当前成分;做历史回测时用它会引入前视偏差。
签名与各自的适用范围用 `lookup.py --name getbk` / `--name getAbkbyDate` /
`--name getBkByDate` 取回。
本页是手写整理页,不由字典生成器产出。
## 板块名是复合的
多个板块名用英文分号拼接,一次传入:
```tsl
return getbk("深证A股;上证A股"); // 只要主板
return getbk("A股;暂停上市;终止上市"); // 含退市与暂停上市
return getbk("港股;终止上市港股"); // 全部港股
return getbk("上市指数;退市指数"); // 全部指数
return getbk("上证基金;深证基金"); // 在市基金交易代码
```
`A股` 本身就是复合板块,等于 `深证A股;上证A股;创业板;科创板;北证A股`。需要单独一个
板块时直接写该板块名,不要在 `A股` 结果上二次过滤。
板块划分会随上游调整:港股在 2026-06-25 之前全部在「港股」板块内,之后拆出
「终止上市港股」。依赖板块名的代码在跨越这类调整时要复核。
具体有哪些板块名不在本页事实范围内;`getbk` 要求名称与板块管理中的名称完全一致,
先在客户端板块管理里确认。
## 指定日的市场板块成分
`getAbkbyDate(bk_name, endt)` 按板块中文名取指定日成分,并剔除暂停上市:
```tsl
return getAbkbyDate("A股", 20200210T);
return getAbkbyDate("深证A股;上证A股;北证A股", 20200210T);
```
它只支持市场板块(`A股``上证A股``深证A股``北证A股``创业板``科创板`
`中小企业板`),其它板块名不支持。
另一条等价路径是用天软自编的市场板块**指数**代码走 `getBkByDate`
| 代码 | 板块 | 代码 | 板块 |
| ----------- | ------- | ----------- | ------- |
| `TSI000001` | A股 | `TSI000006` | 北证A股 |
| `TSI000002` | 上证A股 | `TSI000007` | B股 |
| `TSI000003` | 科创板 | `TSI000008` | 上证B股 |
| `TSI000004` | 深证A股 | `TSI000009` | 深证B股 |
| `TSI000005` | 创业板 | | |
```tsl
return getbkbydate("TSI000001", 20230810T); // A股板块 20230810 成分
```
## 指数成分必须用指数代码
指数成分来源于指数成份变更表(`infotable 752` 指数.指数成份),与板块来源不同,
只能用指数代码取:
```tsl
return getbkbydate("SH000300", 20210908T); // 沪深300 指定日成分
```
板块名传给 `getBkByDate` 不会报错,只会返回空表。字段清单用
`dictionary_lookup.py --query 指数成份` 取回。
## 行业分类与行业指数是两套代码
`getbkbydate("SWHY740000", ...)` 取的是**行业分类**成分(来源:个股所属行业),
`getbkbydate("SW801150", ...)` 取的是**行业指数**成分(来源:指数成份表)。两者
样本可能不同。先用 `SWHY00000N` 拿到该级全部行业指数代码,再逐个取成分:
```tsl
idx := getbkbydate("SWHY000001", 20190201T); // 所有申万一级行业指数代码
return getbkbydate(idx[0], 20190201T); // 其中一个的成分股
```
分类属性代码体系见 [`classification-codes.md`](classification-codes.md)。
## 沪深港通
沪深港通合资格名单也记在 `infotable 752`,用四个方向虚拟代码取:
| 代码 | 方向 | 代码 | 方向 |
| ---------- | ------------ | ---------- | ------------ |
| `HG000001` | 港股通(沪) | `HG000003` | 港股通(深) |
| `HG000002` | 沪股通 | `HG000004` | 深股通 |
```tsl
// 指定日北向(A股)合资格名单
return getbkbydate("HG000002", 20240701T) union2 getbkbydate("HG000004", 20240701T);
// 指定日南向(港股)合资格名单
return getbkbydate("HG000001", 20240701T) union2 getbkbydate("HG000003", 20240701T);
// 曾经进过港股通的全部港股代码
return sselect distinct ["证券代码"]
from infotable 752 of array("HG000001", "HG000003") end;
```
## 基金:在市与运作状态是两回事
板块决定「是否上市」,运作状态要另外筛:
```tsl
return getbk("上市基金"); // 当前在市
return getbk("已退市基金"); // 已退市
return getbkall("普通股票型"); // 某分类全部,含已停止运作
return fundOperateFundsbyBkname("上市基金;已退市基金", 20200924T); // 指定日仍在运作
return fundsOperateFunds(getbkall("股票型"), 20200101T); // 对给定列表筛运作中
```
`getBkAll` 返回板块全部样本含退市,但对一批板块不适用(A股类、指数成份类、概念板块
等),限制清单见 `lookup.py --name getBkAll` 的条目正文。
按基金分类取指定日成分优先走分类属性代码:
```tsl
return getbkbydate("TSJJ0201", 20220101T); // 投资风格:股票型
```
一级市场代码与二级市场(交易)代码不同:`getbk("上证基金;深证基金")` 取的是交易代码,
两者可通过基金基本信息表里的交易代码字段互转。
## 债券
```tsl
return getbk("深交所债券;上交所债券;银行间债券"); // 当前在市全部债券
return getbk("可转债"); // 当前在市可转债
return getbk("可转债;退市可转债"); // 含退市
return getbkall("可转债"); // 同上,走 getBkAll
return bondConvertibleBonds2(); // 专用函数:所有可转债
return cb_getTradeMarkByDate(20210813T); // 指定日有交易的转债
```
券种板块名(`国债``企业债` 等)走同一套 `getbk` 路径。两个专用函数的签名见
`lookup.py --name bondConvertibleBonds2` / `--name cb_getTradeMarkByDate`
## 期货
期货有「合约」和「品种」两层,代码序列要分清取哪一层:
```tsl
return getbk("上市期货"); // 当前在市合约
return getbk("上市期货;退市期货"); // 含退市合约
return getbk("期货品种代码"); // 品种代码(CU、IF 这类)
return getbk("期货品种代码;退市期货品种代码");
return getbk("期货主力"); // 主力线;期货连续/次主力/指数线同理
return getFuturesBkbydate("上市期货;退市期货", 20251218T); // 指定日在市合约
return getFuturesBkbydate("郑州商品交易所;中国金融期货交易所", 20240508T);
```
`getFuturesBkbydate` 的板块名可以是交易所名、期货类别(`股指期货``商品期货`
`农产品期货`)或品种名,签名见 `lookup.py --name getFuturesBkbydate`
品种代码与主力/次主力/指数线/连续代码的对照关系在 `infotable 708`
(期货.期货品种代码对照表),任一形态的代码都能查到其余形态;访问代码与字段用
`dictionary_lookup.py --query 期货品种` 取回。按品种取全部合约要先把品种代码换成
品种名称再走 `getBkAll`
## 期权
```tsl
return getbk("上市期权"); // 当前在市合约
return getbk("ETF期权"); // 按标的类型
return getbk("华泰柏瑞沪深300ETF期权"); // 按标的名称
```
## 其它代码位
`HG000001` 也用作宏观数据表格的取数代码位(如国内生产总值 816 表、居民消费价格
822 表)。其余特别数据(国债收益率曲线等)按字典页确认。
## 事实边界
本页只给取数路径与代码位体系。函数签名、参数类型、返回形态一律以
`lookup.py --name` 的条目正文为准。板块名清单、分类代码清单会随上游调整,不在本页
硬编码,用运行时取回。
FAQ 中出现但当前 API 索引查不到的名称(`GetETFListByDate``GetFuturesID`)不作为
可用 API;需要时先用 `lookup.py --kw` 确认存在再使用,查不到就按 SKILL.md 的
CHECKPOINT 停止。上表中 `getbk` 系列、`getAbkbyDate``getBkByDate``getBkAll`
`getFuturesBkbydate``fundOperateFundsbyBkname``fundsOperateFunds`
`bondConvertibleBonds2``cb_getTradeMarkByDate` 均已在索引中核对存在。
@@ -1,7 +1,7 @@
# 数据字典查询
用于按中文业务含义查询天软数据字典中的表、数据源字段。它们不是可调用 API
不进入 `function_index.tsv`
用于按中文业务含义查询天软数据字典中的表、数据源字段或 API 参数取值域。表、数据源
和字段不是可调用 API不进入 `function_index.tsv`;参数取值域是独立的随 skill 分发数据
先查看脚本当前契约:
@@ -17,17 +17,24 @@ python <this-skill-dir>/scripts/dictionary_lookup.py \
python <this-skill-dir>/scripts/dictionary_lookup.py --query "市值" --scope fund
python <this-skill-dir>/scripts/dictionary_lookup.py --query "买一量" --table TradeTable
python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field 销售现金比率
python <this-skill-dir>/scripts/dictionary_lookup.py --query "申万煤炭"
```
结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问代码、
对应 API 和字典页面。
表/字段结果包含表或数据源名称、表 ID、字段名称、字段 ID、类型、单位、提取方式、访问
代码、对应 API 和字典页面。参数取值域结果包含原始值、名称、父级/层级、有效期、绑定
API 参数、关联表、模式和来源;`runtime_catalog` 还会给出系统或用户目录解析器。
## 状态判读
- `status: ok`:最高分候选唯一。
- `status: ambiguous`:存在多个同分候选;列出候选并请用户补充 scope、表或数据源范围,
不能默认选第一条。
- `status: no_match`数据字典无匹配,不能改述为 API 的 `not found`
- `status: no_match`表、字段和参数取值域均无匹配,不能改述为 API 的 `not found`
参数取值域的“静态记录不完整”表示只记录了已核实样例。需要使用未列出的板块名、用户板块
或其他动态名称时,先调用结果给出的运行时解析器确认;不能凭名称相似性推断可用值。
用精确的参数域 ID 或域名称查询时,结果返回该域的绑定 API、完整性、已记录候选和运行时
解析器;用具体名称或代码查询时,结果返回对应取值及其父级、有效期和关联表。
标识符匹配保留下划线;同义词只来自 `dictionary_lexicon.json` 的受控映射。
`MarketTable``TradeTable` 是 TS-SQL 数据源,不是函数。
@@ -35,5 +42,5 @@ python <this-skill-dir>/scripts/dictionary_lookup.py --query "9900700" --field
## 退出码
- rc=0:查询完成,实际结果由 `status` 判定,包括 `ambiguous``no_match`
- rc=1:字典索引词表缺失、损坏、格式错误。
- rc=1:字典索引词表或参数取值域数据缺失、损坏、格式错误。
- rc=2:参数不合法。
+2
View File
@@ -382,6 +382,8 @@ no_backup = true
/ "tsl-api-reference"
)
self.assertTrue((installed / "scripts" / "class_lookup.py").is_file())
self.assertTrue((installed / "scripts" / "value_domains.py").is_file())
self.assertTrue((installed / "data" / "value_domains.json").is_file())
self.assertTrue((installed / "workflows" / "api-lookup.md").is_file())
self.assertTrue(
(installed / "workflows" / "class-and-framework.md").is_file()
+34
View File
@@ -73,11 +73,45 @@ python tools/tsl-codegen/scripts/build_dictionary.py \
生成完成后,用 `dictionary_lookup.py --help` 中的真实示例做查询检查。
## 维护参数取值域
`skills/tsl-api-reference/data/value_domains.json` 是 API 参数取值域的检索数据,供
`lookup.py``dictionary_lookup.py` 共同读取。它不改变 `function_index.tsv` 的 API 元数据
职责,也不向 `dictionary_index.tsv` 增加 table/source/field 之外的 kind。
以下内容进入参数取值域:
- 由外部目录维护、需要跨 API 查询的版本化代码,如分类属性代码;
- 多个 API 参数共享的封闭枚举;只属于单个 API 的枚举仍留在该 API 页面;
- 已有正式文档但不构成封闭枚举的特殊字面值,使用 `catalog`
- 运行时才完整的系统或用户目录,如市场板块名称;
- 多个 API 共享的特殊选择器或值模式,如分类节点的 `.N` 后缀。
单个 API 自带的封闭枚举仍写在 codegen 录入数据的 `params[].values` 中,由生成器渲染为
参数取值段,不在两个位置重复维护。
维护规则:
1. FAQ 或帮助页逐条人工核对,不用脚本从语料批量筛选后直接入库。
2. 每个值记录原样值、名称、来源和适用的 API 参数;版本化目录按需记录父级、层级、
有效期及关联表。
3. `runtime_catalog` 必须为 `complete: false`,并提供系统/用户运行时解析器;静态值只能
作为已核实样例,不能宣称是完整目录。
4. 分类名称映射为当前板块名但本地证据不足时,关系标记为 `runtime_required`,由
`getBkList2``getUserBkList2` 核验后再用于代码。
5. 修改后运行参数域 CLI 回归和安装测试:
```bash
python -m unittest tools.tsl-codegen.tests.test_value_domains
python -m unittest test.test_playbook
```
## 完成检查
```bash
python skills/tsl-api-reference/scripts/class_lookup.py --check
python tools/tsl-codegen/scripts/build_index.py \
--skill-dir skills/tsl-api-reference --check
python -m unittest tools.tsl-codegen.tests.test_value_domains
python test/integration/check_doc_links.py
```
@@ -0,0 +1,542 @@
import csv
import json
import re
import subprocess
import sys
import tempfile
import unittest
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SKILL_ROOT = ROOT / "skills" / "tsl-api-reference"
LOOKUP = SKILL_ROOT / "scripts" / "lookup.py"
DICTIONARY_LOOKUP = SKILL_ROOT / "scripts" / "dictionary_lookup.py"
class ValueDomainCliTest(unittest.TestCase):
def run_cli(self, script, *args):
return subprocess.run(
[sys.executable, str(script), *args],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
def test_dictionary_query_resolves_classification_by_label_and_code(self):
for query in ("申万煤炭", "SWHY740000"):
with self.subTest(query=query):
result = self.run_cli(DICTIONARY_LOOKUP, "--query", query)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("status: ok", result.stdout)
self.assertIn("类型:参数取值", result.stdout)
self.assertIn(
"参数域:classification_code(分类属性代码)",
result.stdout,
)
self.assertIn("取值:SWHY740000", result.stdout)
self.assertIn("名称:申万煤炭", result.stdout)
self.assertIn("父级:SWHY(申万行业)", result.stdout)
self.assertIn("层级:1", result.stdout)
self.assertIn("关联表:138 股票.股票行业分类信息", result.stdout)
self.assertIn("快照日期:2023-10-13", result.stdout)
self.assertIn("来源:faq:31494", result.stdout)
def test_dictionary_query_uses_strong_values_without_hijacking_table_names(self):
for query in ("SWHY740000 是什么", "申万煤炭分类代码"):
with self.subTest(query=query):
result = self.run_cli(DICTIONARY_LOOKUP, "--query", query)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("类型:参数取值", result.stdout)
self.assertIn("取值:SWHY740000", result.stdout)
table = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万行业配置")
self.assertEqual(0, table.returncode, table.stderr)
self.assertIn("类型:table", table.stdout)
self.assertIn("表 ID629", table.stdout)
self.assertNotIn("类型:参数取值", table.stdout)
def test_pattern_punctuation_is_significant(self):
pattern = self.run_cli(DICTIONARY_LOOKUP, "--query", ".N")
single_letter = self.run_cli(DICTIONARY_LOOKUP, "--query", "N")
self.assertEqual(0, pattern.returncode, pattern.stderr)
self.assertIn("类型:参数取值", pattern.stdout)
self.assertIn("取值:<分类属性代码>.N", pattern.stdout)
self.assertEqual(0, single_letter.returncode, single_letter.stderr)
self.assertNotIn("类型:参数取值", single_letter.stdout)
def test_shared_board_type_enum_maps_multiple_api_parameters(self):
label = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万三级行业")
numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "板块类别 7")
bare_numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "7")
api = self.run_cli(LOOKUP, "--name", "getbktypelist")
bare_lookup = self.run_cli(LOOKUP, "--kw", "7")
self.assertEqual(0, label.returncode, label.stderr)
self.assertIn("status: ambiguous", label.stdout)
self.assertIn("取值:7", label.stdout)
self.assertIn("名称:申万三级行业", label.stdout)
self.assertIn("绑定 APIgetbktypelist.bktype; stocksbklist.bktype", label.stdout)
self.assertIn("字段:申万三级行业", label.stdout)
self.assertEqual(0, numeric.returncode, numeric.stderr)
self.assertIn("名称:申万三级行业", numeric.stdout)
self.assertEqual(0, bare_numeric.returncode, bare_numeric.stderr)
self.assertNotIn("类型:参数取值", bare_numeric.stdout)
self.assertEqual(0, api.returncode, api.stderr)
self.assertIn("market_board_type", api.stdout)
self.assertIn("完整", api.stdout)
self.assertIn("`7` — 申万三级行业", api.stdout)
self.assertEqual(0, bare_lookup.returncode, bare_lookup.stderr)
self.assertNotIn("参数取值域:getbktypelist.bktype", bare_lookup.stdout)
def test_historical_market_board_codes_use_the_shared_domain(self):
dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "TSI000001")
lookup = self.run_cli(LOOKUP, "--kw", "TSI000001")
self.assertEqual(0, dictionary.returncode, dictionary.stderr)
self.assertIn("取值:TSI000001", dictionary.stdout)
self.assertIn("名称:A股板块", dictionary.stdout)
self.assertIn("绑定 APIgetBkByDate.index_id", dictionary.stdout)
self.assertEqual(0, lookup.returncode, lookup.stderr)
self.assertIn("历史市场板块代码:TSI000001(A股板块)", lookup.stdout)
self.assertNotIn("参数取值域:getbktypelist.bktype", lookup.stdout)
def test_exact_code_value_ignores_incidental_dictionary_id_substrings(self):
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "SWHY740000")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("status: ok", result.stdout)
self.assertIn("取值:SWHY740000", result.stdout)
self.assertNotIn("字段:截止日", result.stdout)
def test_named_historical_board_domain_is_complete_and_api_specific(self):
dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "中小企业板")
lookup = self.run_cli(LOOKUP, "--kw", "中小企业板")
exact = self.run_cli(LOOKUP, "--name", "getAbkbyDate")
self.assertEqual(0, dictionary.returncode, dictionary.stderr)
self.assertIn("绑定 APIgetAbkbyDate.bk_name", dictionary.stdout)
self.assertIn("2021-04-06 并入主板", dictionary.stdout)
self.assertEqual(0, lookup.returncode, lookup.stderr)
self.assertIn("历史市场板块名:中小企业板", lookup.stdout)
self.assertEqual(0, exact.returncode, exact.stderr)
self.assertIn("historical_a_share_board_name", exact.stdout)
self.assertIn("`中小企业板` — 中小企业板(历史兼容)", exact.stdout)
def test_scope_filters_multi_table_value_domains(self):
stock = self.run_cli(
DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "stock"
)
fund = self.run_cli(
DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "fund"
)
self.assertEqual(0, stock.returncode, stock.stderr)
self.assertIn("关联表:138 股票.股票行业分类信息", stock.stdout)
self.assertNotIn("355 基金.基金分类信息", stock.stdout)
self.assertEqual(0, fund.returncode, fund.stderr)
self.assertIn("关联表:355 基金.基金分类信息", fund.stdout)
self.assertNotIn("138 股票.股票行业分类信息", fund.stdout)
def test_keyword_query_maps_value_to_current_and_historical_apis(self):
result = self.run_cli(LOOKUP, "--kw", "申万", "煤炭")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("getBk\tfunction\tgetBk(marketlist)", result.stdout)
self.assertIn(
"getBkByDate\tfunction\tgetBkByDate(index_id, end_t, extype)",
result.stdout,
)
self.assertIn("getBk.marketlist", result.stdout)
self.assertIn("当前板块候选:申万煤炭", result.stdout)
self.assertIn("运行时核验", result.stdout)
self.assertIn("getBkByDate.index_id", result.stdout)
self.assertIn("历史分类代码:SWHY740000", result.stdout)
def test_keyword_annotations_only_include_the_top_value_group(self):
exact_code = self.run_cli(LOOKUP, "--kw", "SWHY740000")
domain_query = self.run_cli(LOOKUP, "--kw", "属性", "代码")
self.assertEqual(0, exact_code.returncode, exact_code.stderr)
self.assertIn("历史分类代码:SWHY740000", exact_code.stdout)
self.assertNotIn("历史分类代码:SWHY(申万行业)", exact_code.stdout)
self.assertEqual(0, domain_query.returncode, domain_query.stderr)
self.assertIn("目录选择器:属性代码", domain_query.stdout)
self.assertIn("下级分类代码模式:<分类属性代码>.N", domain_query.stdout)
self.assertNotIn("历史分类代码:CAPCHY", domain_query.stdout)
def test_exact_getbk_query_explains_runtime_catalog_boundary(self):
result = self.run_cli(LOOKUP, "--name", "getBk")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("参数取值域", result.stdout)
self.assertIn("marketlist", result.stdout)
self.assertIn("runtime_catalog", result.stdout)
self.assertIn("静态记录不完整", result.stdout)
self.assertIn("getBkList2", result.stdout)
self.assertIn("getUserBkList2", result.stdout)
self.assertIn("按值查询", result.stdout)
self.assertNotIn("`申万煤炭`", result.stdout)
def test_runtime_domain_query_returns_resolvers_not_unrelated_market_codes(self):
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "market_board")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("status: ok", result.stdout)
self.assertIn("类型:参数取值域", result.stdout)
self.assertIn("参数域:market_board", result.stdout)
self.assertIn("绑定 APIgetBk.marketlist", result.stdout)
self.assertIn("getBkList2(bktype)(系统目录)", result.stdout)
self.assertIn("getUserBkList2(bktype)(用户目录)", result.stdout)
self.assertIn("申万煤炭", result.stdout)
self.assertNotIn("TSI000001", result.stdout)
def test_domain_label_query_returns_one_domain_summary(self):
result = self.run_cli(DICTIONARY_LOOKUP, "--query", "分类属性代码")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("status: ok", result.stdout)
self.assertIn("参数域:classification_code", result.stdout)
self.assertIn("绑定 APIgetBkByDate.index_id", result.stdout)
self.assertIn("SWHY740000(申万煤炭)", result.stdout)
self.assertNotIn("类型:field", result.stdout)
def test_exact_api_query_does_not_expand_versioned_catalog(self):
result = self.run_cli(LOOKUP, "--name", "getBkByDate")
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("分类属性代码", result.stdout)
self.assertIn("已记录值或候选:7 条", result.stdout)
self.assertIn("dictionary_lookup.py --query", result.stdout)
parameter_domain = result.stdout.split("### 参数取值域", maxsplit=1)[1]
self.assertNotIn("`SWHY740000`", parameter_domain)
def test_invalid_value_domain_data_is_a_deployment_error(self):
with tempfile.TemporaryDirectory() as temp_dir:
invalid = Path(temp_dir) / "value_domains.json"
invalid.write_text('{"version": 1, "domains": []}\n', encoding="utf-8")
for script, action in (
(LOOKUP, ("--kw", "申万")),
(DICTIONARY_LOOKUP, ("--query", "申万")),
):
with self.subTest(script=script.name):
result = self.run_cli(
script,
*action,
"--value-domains",
str(invalid),
)
self.assertEqual(1, result.returncode)
self.assertEqual("", result.stdout)
self.assertIn("domains must not be empty", result.stderr)
self.assertNotIn("Traceback", result.stderr)
def test_parameter_domain_binding_does_not_cross_scope_or_module(self):
with tempfile.TemporaryDirectory() as temp_dir:
data_dir = Path(temp_dir) / "data"
data_dir.mkdir()
tsv = data_dir / "function_index.tsv"
tsv.write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
"dotnet/market.md\tgetbk\t\t系统板块\n"
"getBk\tdotnet\tdatawarehouse\tgetBk()\t"
"dotnet/market.md\tgetbk-empty\t\t无参数重载\n"
"getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t"
"getbk\t\t项目函数\n",
encoding="utf-8",
)
domains = data_dir / "value_domains.json"
domains.write_text(
json.dumps(
{
"version": 1,
"domains": [
{
"id": "market_board",
"label": "市场板块",
"mode": "runtime_catalog",
"complete": False,
"bindings": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBk",
"parameter": "marketlist",
"role": "current_components",
}
],
"resolvers": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBkList2",
"parameter": "bktype",
"catalog": "system",
"source": "net_function:28964",
}
],
"related_tables": [],
"values": [
{
"value": "申万煤炭",
"label": "申万煤炭",
"sources": ["faq:31494"],
}
],
"sources": ["faq:31494"],
"as_of": "2026-08-20",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
result = self.run_cli(
LOOKUP,
"--kw",
"申万煤炭",
"--tsv",
str(tsv),
"--value-domains",
str(domains),
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("dotnet/market.md#getbk", result.stdout)
self.assertNotIn("dotnet/market.md#getbk-empty", result.stdout)
self.assertNotIn("project/demo.md#getbk", result.stdout)
def test_exact_query_attaches_domain_only_to_overload_with_bound_parameter(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
data_dir = root / "data"
codegen = root / "references" / "codegen" / "dotnet"
data_dir.mkdir()
codegen.mkdir(parents=True)
tsv = data_dir / "function_index.tsv"
tsv.write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
"dotnet/market.md\tgetbk\t\t系统板块\n"
"getBk\tdotnet\tdatawarehouse\tgetBk()\t"
"dotnet/market.md\tgetbk-1\t\t无参数重载\n",
encoding="utf-8",
)
(codegen / "market.md").write_text(
"# Dotnet\n\n"
"## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n\n"
"## `getBk()`\n\n声明:function\n\n无参数重载\n",
encoding="utf-8",
)
domains = data_dir / "value_domains.json"
domains.write_text(
json.dumps(
{
"version": 1,
"domains": [
{
"id": "market_board",
"label": "市场板块",
"mode": "runtime_catalog",
"complete": False,
"bindings": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBk",
"parameter": "marketlist",
"role": "current_components",
}
],
"resolvers": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBkList2",
"parameter": "bktype",
"catalog": "system",
"source": "net_function:28964",
}
],
"related_tables": [],
"values": [],
"sources": ["net_function:28960"],
"as_of": "2026-08-20",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
result = self.run_cli(
LOOKUP,
"--name",
"getBk",
"--tsv",
str(tsv),
"--value-domains",
str(domains),
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("## `getBk(marketlist)`", result.stdout)
self.assertIn("## `getBk()`", result.stdout)
self.assertEqual(1, result.stdout.count("### 参数取值域"))
def test_exact_query_does_not_attach_domain_to_same_name_in_other_scope(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
data_dir = root / "data"
codegen = root / "references" / "codegen"
(codegen / "dotnet").mkdir(parents=True)
(codegen / "project").mkdir(parents=True)
data_dir.mkdir()
tsv = data_dir / "function_index.tsv"
tsv.write_text(
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
"getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t"
"dotnet/market.md\tgetbk\t\t系统板块\n"
"getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t"
"getbk\t\t项目函数\n",
encoding="utf-8",
)
(codegen / "dotnet" / "market.md").write_text(
"# Dotnet\n\n## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n",
encoding="utf-8",
)
(codegen / "project" / "demo.md").write_text(
"# Project\n\n## `getBk(name)`\n\n声明:function\n\n项目函数\n",
encoding="utf-8",
)
domains = data_dir / "value_domains.json"
domains.write_text(
json.dumps(
{
"version": 1,
"domains": [
{
"id": "market_board",
"label": "市场板块",
"mode": "runtime_catalog",
"complete": False,
"bindings": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBk",
"parameter": "marketlist",
"role": "current_components",
}
],
"resolvers": [
{
"scope": "dotnet",
"module": "datawarehouse",
"api": "getBkList2",
"parameter": "bktype",
"catalog": "system",
"source": "net_function:28964",
}
],
"related_tables": [],
"values": [],
"sources": ["net_function:28960"],
"as_of": "2026-08-20",
}
],
},
ensure_ascii=False,
),
encoding="utf-8",
)
result = self.run_cli(
LOOKUP,
"--scope",
"project",
"--name",
"getBk",
"--tsv",
str(tsv),
"--value-domains",
str(domains),
)
self.assertEqual(0, result.returncode, result.stderr)
self.assertIn("项目函数", result.stdout)
self.assertNotIn("参数取值域", result.stdout)
def test_bundled_value_domains_reference_existing_apis_parameters_and_tables(self):
document = json.loads(
(SKILL_ROOT / "data" / "value_domains.json").read_text(encoding="utf-8")
)
with (SKILL_ROOT / "data" / "function_index.tsv").open(
encoding="utf-8", newline=""
) as handle:
rows = list(csv.DictReader(handle, delimiter="\t"))
with (SKILL_ROOT / "data" / "dictionary_index.tsv").open(
encoding="utf-8", newline=""
) as handle:
dictionary_rows = list(csv.DictReader(handle, delimiter="\t"))
apis = defaultdict(set)
for row in rows:
match = re.search(r"\((.*)\)", row["signature"])
if not match:
continue
key = (
row["scope"].casefold(),
row["module"].casefold(),
row["name"].casefold(),
)
for parameter in match.group(1).split(","):
parameter = parameter.strip().strip("[]")
if parameter:
apis[key].add(parameter.casefold())
table_pages = {
row["table_id"]: row["page"]
for row in dictionary_rows
if row["kind"] in {"table", "source"} and row["table_id"]
}
for domain in document["domains"]:
for binding in domain["bindings"]:
self.assert_api_parameter_exists(apis, binding)
for resolver in domain["resolvers"]:
self.assert_api_parameter_exists(apis, resolver)
for table in domain["related_tables"]:
self.assertEqual(table["page"], table_pages.get(table["id"]))
self.assertTrue((SKILL_ROOT / table["page"]).is_file())
for value in domain["values"]:
for relation in value.get("relations", []):
self.assert_api_parameter_exists(apis, relation["verification"])
def assert_api_parameter_exists(self, apis, reference):
key = (
reference["scope"].casefold(),
reference["module"].casefold(),
(reference.get("api") or reference.get("resolver")).casefold(),
)
parameter = reference.get("parameter")
self.assertIn(key, apis)
if parameter:
self.assertIn(parameter.casefold(), apis[key])
if __name__ == "__main__":
unittest.main()