✨ feat(tsl-api): add parameter value domain lookup
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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 多个同分候选;必须补充 scope、table 或 field 范围
|
||||
status: no_match 数据字典无匹配,不等于 API not found
|
||||
status: no_match 表/字段/参数取值域均无匹配,不等于 API not found
|
||||
|
||||
参数取值域结果会标明值、名称、绑定 API、关联表和来源;动态目录只提供已知样例,
|
||||
完整名称必须使用结果中的运行时解析器重新确认。
|
||||
|
||||
退出码:
|
||||
0 查询完成;ok、ambiguous、no_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
|
||||
|
||||
@@ -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/module、page#anchor 来源标记;
|
||||
简单成员名可能返回多个 owner,qualified_name 可消歧;重载会全部返回
|
||||
简单成员名可能返回多个 owner,qualified_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:参数不合法。
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("表 ID:629", 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("绑定 API:getbktypelist.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("绑定 API:getBkByDate.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("绑定 API:getAbkbyDate.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("绑定 API:getBk.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("绑定 API:getBkByDate.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()
|
||||
Reference in New Issue
Block a user