From 0aec87281ea97bb47063eb151feac3f6dcea52f1 Mon Sep 17 00:00:00 2001
From: liaozhaorun <1300336796@qq.com>
Date: Sun, 8 Mar 2026 01:16:25 +0800
Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E8=B4=A2=E5=8A=A1?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=20API=20=E8=A7=84=E8=8C=83=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 更新 FINANCIAL_API_SPEC.md,添加首次同步优化策略
- 添加日期格式转换规范(YYYYMMDD → YYYY-MM-DD)
- 补充存储层 UPSERT 禁用说明和删除计数处理
- 扩充常见问题(Q7-Q9)
- 完善 financial_api.md,补充资产负债表接口完整文档和报表类型说明
Closes: 文档更新 v1.1
---
docs/api/FINANCIAL_API_SPEC.md | 270 ++++++++++++++++++++++++++++++++-
docs/api/financial_api.md | 213 ++++++++++++++++++++++++++
2 files changed, 476 insertions(+), 7 deletions(-)
diff --git a/docs/api/FINANCIAL_API_SPEC.md b/docs/api/FINANCIAL_API_SPEC.md
index df2463a..d08451a 100644
--- a/docs/api/FINANCIAL_API_SPEC.md
+++ b/docs/api/FINANCIAL_API_SPEC.md
@@ -305,7 +305,7 @@ def sync_full(self, dry_run: bool = False) -> List[Dict]:
### 单季度同步策略
-**规范**: 单季度同步采用"先删除后插入"策略。
+**规范**: 单季度同步采用"先删除后插入"策略,并优化首次同步场景。
**流程**:
@@ -319,24 +319,41 @@ def sync_quarter(self, period: str, dry_run: bool = False) -> Dict:
if self.TARGET_REPORT_TYPE and 'report_type' in remote_df.columns:
remote_df = remote_df[remote_df['report_type'] == self.TARGET_REPORT_TYPE]
- # 3. 对比找出差异股票
+ remote_total = len(remote_df)
+
+ # 3. 检查本地是否有该季度数据(首次同步优化)
+ local_counts = self.get_local_data_count_by_stock(period)
+ is_first_sync_for_period = len(local_counts) == 0
+
+ if is_first_sync_for_period:
+ # 首次同步:直接插入所有数据,跳过差异检测
+ print(f"[{self.__class__.__name__}] First sync for quarter {period}, inserting all data directly")
+ if not dry_run:
+ self.storage.queue_save(self.table_name, remote_df, use_upsert=False)
+ self.storage.flush()
+ return {...}
+
+ # 4. 非首次同步:对比找出差异股票
diff_df, stats_df = self.compare_and_find_differences(remote_df, period)
- # 4. 执行同步(先删除后插入)
+ # 5. 执行同步(先删除后插入)
if not dry_run and not diff_df.empty:
diff_stocks = list(diff_df['ts_code'].unique())
- # 4.1 删除差异股票的旧数据
+ # 5.1 删除差异股票的旧数据
self.delete_stock_quarter_data(period, diff_stocks)
- # 4.2 插入新数据
- self.storage.queue_save(self.table_name, diff_df)
+ # 5.2 插入新数据(必须使用 use_upsert=False)
+ self.storage.queue_save(self.table_name, diff_df, use_upsert=False)
self.storage.flush()
return {...}
```
-**重要**: 禁止使用 UPSERT(INSERT OR REPLACE),必须使用"先删除后插入"。
+**重要**:
+1. 禁止使用 UPSERT(INSERT OR REPLACE),必须使用"先删除后插入"
+2. **首次同步优化**:本地无数据时直接插入,不进行差异检测,提升性能
+3. **必须使用 `use_upsert=False`**:调用 `queue_save()` 时必须显式指定,避免触发 UPSERT 错误
---
@@ -436,6 +453,149 @@ def delete_stock_quarter_data(
return result.rowcount
```
+### 删除计数处理
+
+**注意**: DuckDB 的 DELETE 操作 `rowcount` 属性可能返回 `-1`(表示未知数量),需要特殊处理。
+
+**改进方案**:
+
+```python
+def delete_stock_quarter_data(self, period: str, ts_codes: Optional[List[str]] = None) -> int:
+ """删除指定季度和股票的数据。"""
+ storage = Storage()
+
+ try:
+ # 将 YYYYMMDD 转换为 YYYY-MM-DD 格式(DuckDB DATE 类型要求)
+ period_formatted = f"{period[:4]}-{period[4:6]}-{period[6:]}"
+
+ if ts_codes:
+ # 删除指定股票的数据
+ placeholders = ', '.join(['?' for _ in ts_codes])
+ query = f'''
+ DELETE FROM "{self.table_name}"
+ WHERE end_date = ? AND ts_code IN ({placeholders})
+ '''
+ storage._connection.execute(query, [period_formatted] + ts_codes)
+ # DuckDB rowcount 返回 -1,使用传入的股票数量作为估算
+ return len(ts_codes)
+ else:
+ # 删除整个季度的数据
+ query = f'DELETE FROM "{self.table_name}" WHERE end_date = ?'
+ storage._connection.execute(query, [period_formatted])
+ return -1 # 标记为未知
+ except Exception as e:
+ print(f"[{self.__class__.__name__}] Error deleting data: {e}")
+ return 0
+```
+
+**日志输出改进**:
+
+```python
+# 改进后的日志输出
+if not dry_run and not diff_df.empty:
+ deleted_stocks_count = len(diff_stocks)
+ self.delete_stock_quarter_data(period, diff_stocks)
+ deleted_count = len(diff_df)
+ print(f"[{self.__class__.__name__}] Deleted {deleted_stocks_count} stocks' old records (approx {deleted_count} rows)")
+```
+
+输出示例:
+```
+[IncomeQuarterSync] Deleted 100 stocks' old records (approx 500 rows)
+```
+
+---
+
+## 日期格式转换
+
+### DuckDB DATE 类型要求
+
+DuckDB 的 `DATE` 类型要求格式为 `YYYY-MM-DD`,而 Tushare API 返回的日期格式为 `YYYYMMDD`(字符串)。**必须**在 SQL 查询前进行转换。
+
+### 转换方法
+
+```python
+def _format_period_for_sql(self, period: str) -> str:
+ """将 YYYYMMDD 格式转换为 YYYY-MM-DD 格式。
+
+ Args:
+ period: YYYYMMDD 格式的日期字符串
+
+ Returns:
+ YYYY-MM-DD 格式的日期字符串
+ """
+ return f"{period[:4]}-{period[4:6]}-{period[6:]}"
+
+# 使用示例
+period = "20240331"
+period_sql = self._format_period_for_sql(period) # "2024-03-31"
+
+query = f'SELECT * FROM "{self.table_name}" WHERE end_date = ?'
+result = storage._connection.execute(query, [period_sql])
+```
+
+### 需要转换的位置
+
+以下方法中涉及 SQL 查询的 `period` 参数时**必须**进行转换:
+
+1. `get_local_data_count_by_stock()` - 查询本地数据计数
+2. `get_local_records_by_key()` - 按主键查询本地记录
+3. `delete_stock_quarter_data()` - 删除季度数据
+
+### 错误示例
+
+如果不进行转换,会报以下错误:
+
+```
+Conversion Error: invalid date field format: "20250331",
+expected format is (YYYY-MM-DD)
+```
+
+---
+
+## 存储层配置
+
+### 禁用 UPSERT
+
+财务数据表没有主键约束,**必须**在调用存储层方法时禁用 UPSERT。
+
+### ThreadSafeStorage 配置
+
+```python
+class ThreadSafeStorage:
+ """线程安全的 DuckDB 写入包装器。"""
+
+ def queue_save(self, name: str, data: pd.DataFrame, use_upsert: bool = True):
+ """将数据放入写入队列。
+
+ Args:
+ name: 表名
+ data: DataFrame 数据
+ use_upsert: 若为 True 使用 INSERT OR REPLACE,若为 False 使用普通 INSERT
+ """
+ if not data.empty:
+ self._pending_writes.append((name, data, use_upsert))
+```
+
+### 财务数据同步时的调用
+
+```python
+# 正确:禁用 UPSERT
+self.storage.queue_save(self.table_name, diff_df, use_upsert=False)
+
+# 错误:使用默认 UPSERT(会导致 Binder Error)
+self.storage.queue_save(self.table_name, diff_df) # 默认 use_upsert=True
+```
+
+### 错误信息
+
+如果错误地使用 UPSERT:
+
+```
+Binder Error: There are no UNIQUE/PRIMARY KEY constraints that refer
+to this table, specify ON CONFLICT columns manually
+```
+
---
## 表结构设计
@@ -880,6 +1040,101 @@ print(result)
- 差异股票列表
- 删除/插入记录数
+### Q7: 为什么要优化首次同步?
+
+**A**: 首次同步某个季度时,本地没有数据,不需要进行差异检测和删除操作。直接插入所有数据可以提升性能。
+
+**优化逻辑**:
+
+```python
+# 检查本地是否有该季度数据
+local_counts = self.get_local_data_count_by_stock(period)
+is_first_sync_for_period = len(local_counts) == 0
+
+if is_first_sync_for_period:
+ # 首次同步:直接插入,跳过差异检测
+ print(f"First sync for quarter {period}, inserting all data directly")
+ self.storage.queue_save(self.table_name, remote_df, use_upsert=False)
+ self.storage.flush()
+else:
+ # 非首次同步:进行差异检测
+ diff_df, stats_df = self.compare_and_find_differences(remote_df, period)
+ # ... 删除旧数据并插入新数据
+```
+
+**输出对比**:
+
+首次同步:
+```
+[IncomeQuarterSync] Syncing quarter 20240331...
+[IncomeQuarterSync] Fetched 5300 records from API
+[IncomeQuarterSync] First sync for quarter 20240331, inserting all data directly
+[IncomeQuarterSync] Inserted 5300 new records
+```
+
+非首次同步:
+```
+[IncomeQuarterSync] Syncing quarter 20240331...
+[IncomeQuarterSync] Fetched 5300 records from API
+[IncomeQuarterSync] Comparison result:
+ - Stocks with differences: 100
+ - Unchanged stocks: 5200
+[IncomeQuarterSync] Deleted 100 stocks' old records (approx 500 rows)
+[IncomeQuarterSync] Inserted 500 new records
+```
+
+### Q8: 为什么会报日期格式错误?
+
+**A**: DuckDB 的 `DATE` 类型要求格式为 `YYYY-MM-DD`,而系统中使用的日期格式为 `YYYYMMDD`(字符串)。在 SQL 查询前必须进行转换。
+
+**错误示例**:
+
+```python
+# 错误:直接传入 YYYYMMDD 格式
+query = 'SELECT * FROM table WHERE end_date = ?'
+result = storage.execute(query, ["20240331"])
+# 错误:Conversion Error: invalid date field format: "20240331"
+```
+
+**正确示例**:
+
+```python
+# 正确:转换为 YYYY-MM-DD 格式
+period_formatted = f"{period[:4]}-{period[4:6]}-{period[6:]}"
+query = 'SELECT * FROM table WHERE end_date = ?'
+result = storage.execute(query, [period_formatted])
+```
+
+**需要转换的方法**:
+- `get_local_data_count_by_stock()`
+- `get_local_records_by_key()`
+- `delete_stock_quarter_data()`
+
+### Q9: 为什么会报 UPSERT 错误?
+
+**A**: 财务数据表没有主键约束,不能使用 `INSERT OR REPLACE`(UPSERT)。必须使用普通 `INSERT`,并通过"先删除后插入"策略确保数据一致性。
+
+**错误信息**:
+```
+Binder Error: There are no UNIQUE/PRIMARY KEY constraints that refer
+to this table, specify ON CONFLICT columns manually
+```
+
+**正确做法**:
+
+```python
+# 1. 调用 storage.save() 时指定 use_upsert=False
+storage.save(table_name, data, use_upsert=False)
+
+# 2. 调用 queue_save() 时指定 use_upsert=False
+self.storage.queue_save(self.table_name, diff_df, use_upsert=False)
+
+# 3. 在删除旧数据后插入新数据
+self.delete_stock_quarter_data(period, diff_stocks)
+self.storage.queue_save(self.table_name, diff_df, use_upsert=False)
+self.storage.flush()
+```
+
---
## 附录
@@ -900,6 +1155,7 @@ print(result)
| 日期 | 版本 | 变更内容 |
|------|------|----------|
+| 2026-03-08 | v1.1 | 完善实际编码细节:
- 添加首次同步优化说明
- 添加日期格式转换规范
- 添加存储层 UPSERT 禁用说明
- 添加删除计数处理说明
- 扩充常见问题(Q7-Q9) |
| 2026-03-07 | v1.0 | 初始版本,规范财务数据 API 封装要求 |
---
diff --git a/docs/api/financial_api.md b/docs/api/financial_api.md
index 6e0d629..f7897be 100644
--- a/docs/api/financial_api.md
+++ b/docs/api/financial_api.md
@@ -129,6 +129,219 @@ df = pro.income_vip(period='20181231',fields='ts_code,ann_date,f_ann_date,end_da
2 600000.SH 20180428 20180428 20171231 1 2 1.84 1.84
主要报表类型说明
+代码 | 类型 | 说明
+---- | ----- | ---- |
+1 | 合并报表 | 上市公司最新报表(默认)
+2 | 单季合并 | 单一季度的合并报表
+3 | 调整单季合并表 | 调整后的单季合并报表(如果有)
+4 | 调整合并报表 | 本年度公布上年同期的财务报表数据,报告期为上年度
+5 | 调整前合并报表 | 数据发生变更,将原数据进行保留,即调整前的原数据
+6 | 母公司报表 | 该公司母公司的财务报表数据
+7 | 母公司单季表 | 母公司的单季度表
+8 | 母公司调整单季表 | 母公司调整后的单季表
+9 | 母公司调整表 | 该公司母公司的本年度公布上年同期的财务报表数据
+10 | 母公司调整前报表 | 母公司调整之前的原始财务报表数据
+11 | 母公司调整前合并报表 | 母公司调整之前合并报表原数据
+12 | 母公司调整前报表 | 母公司报表发生变更前保留的原数据
+
+
+资产负债表
+接口:balancesheet,可以通过数据工具调试和查看数据。
+描述:获取上市公司资产负债表
+积分:用户需要至少2000积分才可以调取,具体请参阅积分获取办法
+
+提示:当前接口只能按单只股票获取其历史数据,如果需要获取某一季度全部上市公司数据,请使用balancesheet_vip接口(参数一致),需积攒5000积分。
+
+输入参数
+
+名称 类型 必选 描述
+ts_code str Y 股票代码
+ann_date str N 公告日期(YYYYMMDD格式,下同)
+start_date str N 公告日开始日期
+end_date str N 公告日结束日期
+period str N 报告期(每个季度最后一天的日期,比如20171231表示年报,20170630半年报,20170930三季报)
+report_type str N 报告类型:见下方详细说明
+comp_type str N 公司类型:1一般工商业 2银行 3保险 4证券
+输出参数
+
+名称 类型 默认显示 描述
+ts_code str Y TS股票代码
+ann_date str Y 公告日期
+f_ann_date str Y 实际公告日期
+end_date str Y 报告期
+report_type str Y 报表类型
+comp_type str Y 公司类型(1一般工商业2银行3保险4证券)
+end_type str Y 报告期类型
+total_share float Y 期末总股本
+cap_rese float Y 资本公积金
+undistr_porfit float Y 未分配利润
+surplus_rese float Y 盈余公积金
+special_rese float Y 专项储备
+money_cap float Y 货币资金
+trad_asset float Y 交易性金融资产
+notes_receiv float Y 应收票据
+accounts_receiv float Y 应收账款
+oth_receiv float Y 其他应收款
+prepayment float Y 预付款项
+div_receiv float Y 应收股利
+int_receiv float Y 应收利息
+inventories float Y 存货
+amor_exp float Y 待摊费用
+nca_within_1y float Y 一年内到期的非流动资产
+sett_rsrv float Y 结算备付金
+loanto_oth_bank_fi float Y 拆出资金
+premium_receiv float Y 应收保费
+reinsur_receiv float Y 应收分保账款
+reinsur_res_receiv float Y 应收分保合同准备金
+pur_resale_fa float Y 买入返售金融资产
+oth_cur_assets float Y 其他流动资产
+total_cur_assets float Y 流动资产合计
+fa_avail_for_sale float Y 可供出售金融资产
+htm_invest float Y 持有至到期投资
+lt_eqt_invest float Y 长期股权投资
+invest_real_estate float Y 投资性房地产
+time_deposits float Y 定期存款
+oth_assets float Y 其他资产
+lt_rec float Y 长期应收款
+fix_assets float Y 固定资产
+cip float Y 在建工程
+const_materials float Y 工程物资
+fixed_assets_disp float Y 固定资产清理
+produc_bio_assets float Y 生产性生物资产
+oil_and_gas_assets float Y 油气资产
+intan_assets float Y 无形资产
+r_and_d float Y 研发支出
+goodwill float Y 商誉
+lt_amor_exp float Y 长期待摊费用
+defer_tax_assets float Y 递延所得税资产
+decr_in_disbur float Y 发放贷款及垫款
+oth_nca float Y 其他非流动资产
+total_nca float Y 非流动资产合计
+cash_reser_cb float Y 现金及存放中央银行款项
+depos_in_oth_bfi float Y 存放同业和其它金融机构款项
+prec_metals float Y 贵金属
+deriv_assets float Y 衍生金融资产
+rr_reins_une_prem float Y 应收分保未到期责任准备金
+rr_reins_outstd_cla float Y 应收分保未决赔款准备金
+rr_reins_lins_liab float Y 应收分保寿险责任准备金
+rr_reins_lthins_liab float Y 应收分保长期健康险责任准备金
+refund_depos float Y 存出保证金
+ph_pledge_loans float Y 保户质押贷款
+refund_cap_depos float Y 存出资本保证金
+indep_acct_assets float Y 独立账户资产
+client_depos float Y 其中:客户资金存款
+client_prov float Y 其中:客户备付金
+transac_seat_fee float Y 其中:交易席位费
+invest_as_receiv float Y 应收款项类投资
+total_assets float Y 资产总计
+lt_borr float Y 长期借款
+st_borr float Y 短期借款
+cb_borr float Y 向中央银行借款
+depos_ib_deposits float Y 吸收存款及同业存放
+loan_oth_bank float Y 拆入资金
+trading_fl float Y 交易性金融负债
+notes_payable float Y 应付票据
+acct_payable float Y 应付账款
+adv_receipts float Y 预收款项
+sold_for_repur_fa float Y 卖出回购金融资产款
+comm_payable float Y 应付手续费及佣金
+payroll_payable float Y 应付职工薪酬
+taxes_payable float Y 应交税费
+int_payable float Y 应付利息
+div_payable float Y 应付股利
+oth_payable float Y 其他应付款
+acc_exp float Y 预提费用
+deferred_inc float Y 递延收益
+st_bonds_payable float Y 应付短期债券
+payable_to_reinsurer float Y 应付分保账款
+rsrv_insur_cont float Y 保险合同准备金
+acting_trading_sec float Y 代理买卖证券款
+acting_uw_sec float Y 代理承销证券款
+non_cur_liab_due_1y float Y 一年内到期的非流动负债
+oth_cur_liab float Y 其他流动负债
+total_cur_liab float Y 流动负债合计
+bond_payable float Y 应付债券
+lt_payable float Y 长期应付款
+specific_payables float Y 专项应付款
+estimated_liab float Y 预计负债
+defer_tax_liab float Y 递延所得税负债
+defer_inc_non_cur_liab float Y 递延收益-非流动负债
+oth_ncl float Y 其他非流动负债
+total_ncl float Y 非流动负债合计
+depos_oth_bfi float Y 同业和其它金融机构存放款项
+deriv_liab float Y 衍生金融负债
+depos float Y 吸收存款
+agency_bus_liab float Y 代理业务负债
+oth_liab float Y 其他负债
+prem_receiv_adva float Y 预收保费
+depos_received float Y 存入保证金
+ph_invest float Y 保户储金及投资款
+reser_une_prem float Y 未到期责任准备金
+reser_outstd_claims float Y 未决赔款准备金
+reser_lins_liab float Y 寿险责任准备金
+reser_lthins_liab float Y 长期健康险责任准备金
+indept_acc_liab float Y 独立账户负债
+pledge_borr float Y 其中:质押借款
+indem_payable float Y 应付赔付款
+policy_div_payable float Y 应付保单红利
+total_liab float Y 负债合计
+treasury_share float Y 减:库存股
+ordin_risk_reser float Y 一般风险准备
+forex_differ float Y 外币报表折算差额
+invest_loss_unconf float Y 未确认的投资损失
+minority_int float Y 少数股东权益
+total_hldr_eqy_exc_min_int float Y 股东权益合计(不含少数股东权益)
+total_hldr_eqy_inc_min_int float Y 股东权益合计(含少数股东权益)
+total_liab_hldr_eqy float Y 负债及股东权益总计
+lt_payroll_payable float Y 长期应付职工薪酬
+oth_comp_income float Y 其他综合收益
+oth_eqt_tools float Y 其他权益工具
+oth_eqt_tools_p_shr float Y 其他权益工具(优先股)
+lending_funds float Y 融出资金
+acc_receivable float Y 应收款项
+st_fin_payable float Y 应付短期融资款
+payables float Y 应付款项
+hfs_assets float Y 持有待售的资产
+hfs_sales float Y 持有待售的负债
+cost_fin_assets float Y 以摊余成本计量的金融资产
+fair_value_fin_assets float Y 以公允价值计量且其变动计入其他综合收益的金融资产
+cip_total float Y 在建工程(合计)(元)
+oth_pay_total float Y 其他应付款(合计)(元)
+long_pay_total float Y 长期应付款(合计)(元)
+debt_invest float Y 债权投资(元)
+oth_debt_invest float Y 其他债权投资(元)
+oth_eq_invest float N 其他权益工具投资(元)
+oth_illiq_fin_assets float N 其他非流动金融资产(元)
+oth_eq_ppbond float N 其他权益工具:永续债(元)
+receiv_financing float N 应收款项融资
+use_right_assets float N 使用权资产
+lease_liab float N 租赁负债
+contract_assets float Y 合同资产
+contract_liab float Y 合同负债
+accounts_receiv_bill float Y 应收票据及应收账款
+accounts_pay float Y 应付票据及应付账款
+oth_rcv_total float Y 其他应收款(合计)(元)
+fix_assets_total float Y 固定资产(合计)(元)
+update_flag str Y 更新标识
+接口使用说明
+
+pro = ts.pro_api()
+
+df = pro.balancesheet(ts_code='600000.SH', start_date='20180101', end_date='20180730', fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,cap_rese')
+获取某一季度全部股票数据
+
+df2 = pro.balancesheet_vip(period='20181231',fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,cap_rese')
+数据样例
+
+ ts_code ann_date f_ann_date end_date report_type comp_type \
+0 600000.SH 20180830 20180830 20180630 1 2
+1 600000.SH 20180428 20180428 20180331 1 2
+
+ cap_rese
+0 8.176000e+10
+1 8.176000e+10
+主要报表类型说明
+
代码 | 类型 | 说明
---- | ----- | ---- |
1 | 合并报表 | 上市公司最新报表(默认)