refactor: 代码审查修复 - 日期过滤、性能优化、数据泄露防护

- 修复 data_loader.py 财务数据日期过滤,支持按范围加载
- 优化 MADClipper 使用窗口函数替代 join,提升性能
- 修复训练日期边界问题,添加1天间隔避免数据泄露
- 新增 .gitignore 规则忽略训练输出目录
This commit is contained in:
2026-02-25 21:11:19 +08:00
parent 593ec99466
commit a9e4746239
24 changed files with 3597 additions and 56 deletions

View File

@@ -20,6 +20,7 @@ Example:
"""
from src.data.api_wrappers.api_daily import get_daily, sync_daily, preview_daily_sync, DailySync
from src.data.api_wrappers.financial_data.api_income import get_income, sync_income, IncomeSync
from src.data.api_wrappers.api_bak_basic import get_bak_basic, sync_bak_basic
from src.data.api_wrappers.api_namechange import get_namechange, sync_namechange
from src.data.api_wrappers.api_stock_basic import get_stock_basic, sync_all_stocks
@@ -37,6 +38,10 @@ __all__ = [
"sync_daily",
"preview_daily_sync",
"DailySync",
# Income statement
"get_income",
"sync_income",
"IncomeSync",
# Historical stock list
"get_bak_basic",
"sync_bak_basic",

View File

@@ -0,0 +1,587 @@
"""财务数据统一同步调度中心。
该模块作为财务数据同步的调度中心,统一管理各类型财务数据的同步流程。
支持全量同步和增量同步两种模式。
财务数据类型:
- income: 利润表 (已实现)
- balance: 资产负债表 (预留)
- cashflow: 现金流量表 (预留)
同步模式:
1. 全量同步 (force_full=True):
- 检查表是否存在,如不存在则建表+建索引
- 从默认开始日期 (20180101) 同步到当前季度
2. 增量同步 (force_full=False):
- 获取表中最新季度 (MAX(end_date))
- 计算当前季度(如果当前日期未到季末,则用前一季度)
- 如果最新季度 == 当前季度,不同步(避免消耗流量)
- 否则从最新季度+1 同步到当前季度
使用方式:
# 增量同步利润表数据(推荐)
from src.data.api_wrappers.financial_data.api_financial_sync import sync_financial
sync_financial()
# 全量同步利润表数据
from src.data.api_wrappers.financial_data.api_financial_sync import sync_financial
sync_financial(force_full=True)
# 预览同步
from src.data.api_wrappers.financial_data.api_financial_sync import preview_sync
preview = preview_sync()
"""
from typing import Optional, Dict, List
from datetime import datetime
import pandas as pd
from src.data.storage import Storage, ThreadSafeStorage
from src.data.utils import (
get_today_date,
get_quarters_in_range,
date_to_quarter,
DEFAULT_START_DATE,
)
from src.data.api_wrappers.financial_data.api_income import get_income
# =============================================================================
# 财务数据表结构定义
# =============================================================================
# 各财务数据表的表名和字段定义
FINANCIAL_TABLES = {
"income": {
"table_name": "financial_income",
"api_name": "income_vip",
"period_field": "end_date", # 用于存储最新季度的字段
"get_data_func": get_income,
},
# 预留:资产负债表
# "balance": {
# "table_name": "financial_balance",
# "api_name": "balance Sheet_vip",
# "period_field": "end_date",
# "get_data_func": get_balance,
# },
# 预留:现金流量表
# "cashflow": {
# "table_name": "financial_cashflow",
# "api_name": "cashflow_vip",
# "period_field": "end_date",
# "get_data_func": get_cashflow,
# },
}
# =============================================================================
# 财务数据同步核心类
# =============================================================================
class FinancialSync:
"""财务数据统一同步管理器。
支持全量同步和增量同步,自动检测数据状态并选择最优同步策略。
功能特性:
- 全量/增量同步自动切换
- 自动建表和索引(如不存在)
- 智能季度计算(当前季度未到季末时使用前一季度)
- 流量保护(最新季度==当前季度时不请求API
Example:
>>> sync = FinancialSync()
>>> sync.sync_all() # 增量同步所有财务数据
>>> sync.sync_all(force_full=True) # 全量同步
>>> sync.sync_income() # 只同步利润表
"""
def __init__(self):
"""初始化同步管理器"""
self.storage = Storage()
self.thread_storage = ThreadSafeStorage()
def _create_table_if_not_exists(self, table_name: str) -> None:
"""如果表不存在则创建表和索引。
Args:
table_name: 表名
"""
if self.storage.exists(table_name):
print(f"[FinancialSync] 表 {table_name} 已存在,跳过建表")
return
print(f"[FinancialSync] 表 {table_name} 不存在,创建表和索引...")
# 根据表名创建不同的表结构
if table_name == "financial_income":
self.storage._connection.execute(f"""
CREATE TABLE IF NOT EXISTS {table_name} (
ts_code VARCHAR(16) NOT NULL,
ann_date DATE,
f_ann_date DATE,
end_date DATE NOT NULL,
report_type INTEGER,
comp_type INTEGER,
basic_eps DOUBLE,
diluted_eps DOUBLE,
PRIMARY KEY (ts_code, end_date)
)
""")
# 创建索引
self.storage._connection.execute(f"""
CREATE INDEX IF NOT EXISTS idx_financial_ann
ON {table_name}(ts_code, ann_date)
""")
else:
# 默认表结构
self.storage._connection.execute(f"""
CREATE TABLE IF NOT EXISTS {table_name} (
ts_code VARCHAR(16) NOT NULL,
end_date DATE NOT NULL,
PRIMARY KEY (ts_code, end_date)
)
""")
print(f"[FinancialSync] 表 {table_name} 创建完成")
def _get_latest_quarter(
self, table_name: str, period_field: str = "end_date"
) -> Optional[str]:
"""获取表中最新季度。
Args:
table_name: 表名
period_field: 季度字段名
Returns:
最新季度字符串 (YYYYMMDD),如无数据返回 None
"""
try:
result = self.storage._connection.execute(f"""
SELECT MAX({period_field}) FROM {table_name}
""").fetchone()
if result and result[0]:
# 转换为字符串格式
latest = result[0]
if hasattr(latest, "strftime"):
return latest.strftime("%Y%m%d")
return str(latest)
return None
except Exception as e:
print(f"[FinancialSync] 获取最新季度失败: {e}")
return None
def _get_current_quarter(self) -> str:
"""获取当前季度(考虑是否到季末)。
如果当前日期未到季度最后一天,则返回前一季度。
这样可以避免获取尚无数据的未来季度。
Returns:
当前季度字符串 (YYYYMMDD)
"""
today = get_today_date()
current_quarter = date_to_quarter(today)
# 检查今天是否到了当前季度的最后一天
if today < current_quarter:
# 未到季末,返回前一季度
return self._get_prev_quarter(current_quarter)
return current_quarter
def _get_prev_quarter(self, quarter: str) -> str:
"""获取前一季度。
Args:
quarter: 季度字符串 (YYYYMMDD)
Returns:
前一季度字符串 (YYYYMMDD)
"""
year = int(quarter[:4])
month_day = quarter[4:]
if month_day == "0331":
# Q1 -> 去年 Q4
return f"{year - 1}1231"
elif month_day == "0630":
# Q2 -> Q1
return f"{year}0331"
elif month_day == "0930":
# Q3 -> Q2
return f"{year}0630"
else: # "1231"
# Q4 -> Q3
return f"{year}0930"
def _get_next_quarter(self, quarter: str) -> str:
"""获取下一季度。
Args:
quarter: 季度字符串 (YYYYMMDD)
Returns:
下一季度字符串 (YYYYMMDD)
"""
year = int(quarter[:4])
month_day = quarter[4:]
if month_day == "0331":
# Q1 -> Q2
return f"{year}0630"
elif month_day == "0630":
# Q2 -> Q3
return f"{year}0930"
elif month_day == "0930":
# Q3 -> Q4
return f"{year}1231"
else: # "1231"
# Q4 -> 明年 Q1
return f"{year + 1}0331"
def _check_incremental_needed(
self,
table_name: str,
period_field: str = "end_date",
) -> tuple[bool, Optional[str], Optional[str]]:
"""检查增量同步是否需要。
Args:
table_name: 表名
period_field: 季度字段名
Returns:
(是否需要同步, 起始季度, 目标季度)
- 如果不需要同步,返回 (False, None, None)
"""
# 获取表中最新季度
latest_quarter = self._get_latest_quarter(table_name, period_field)
if latest_quarter is None:
# 无本地数据,需要全量同步
print(f"[FinancialSync] 表 {table_name} 无数据,需要全量同步")
return (True, DEFAULT_START_DATE, self._get_current_quarter())
print(f"[FinancialSync] 表 {table_name} 最新季度: {latest_quarter}")
# 获取当前季度(考虑是否到季末)
current_quarter = self._get_current_quarter()
print(f"[FinancialSync] 当前季度: {current_quarter}")
# 比较:如果最新季度 >= 当前季度,不需要同步
if latest_quarter >= current_quarter:
print(
f"[FinancialSync] 最新季度 {latest_quarter} >= 当前季度 {current_quarter},跳过增量同步"
)
return (False, None, None)
# 需要增量同步:从最新季度+1 到 当前季度
start_quarter = self._get_next_quarter(latest_quarter)
print(f"[FinancialSync] 增量同步: {start_quarter} -> {current_quarter}")
return (True, start_quarter, current_quarter)
def _sync_single_table(
self,
table_config: Dict,
start_quarter: str,
end_quarter: str,
) -> int:
"""同步单个财务数据表。
Args:
table_config: 表配置字典
start_quarter: 起始季度
end_quarter: 目标季度
Returns:
同步的记录数
"""
table_name = table_config["table_name"]
get_data_func = table_config["get_data_func"]
# 获取需要同步的季度列表
quarters = get_quarters_in_range(start_quarter, end_quarter)
print(f"[FinancialSync] 计划同步 {len(quarters)} 个季度: {quarters}")
total_records = 0
# 对每个季度调用 API 获取数据
for period in quarters:
try:
df = get_data_func(period)
if df.empty:
print(f"[WARN] 季度 {period} 无数据")
continue
# 只保留合并报表 (report_type='1',注意是字符串)
if "report_type" in df.columns:
df = df[df["report_type"] == "1"]
if not df.empty:
self.thread_storage.queue_save(table_name, df)
print(f"[FinancialSync] 季度 {period} -> {len(df)} 条记录")
total_records += len(df)
except Exception as e:
print(f"[ERROR] 获取季度 {period} 数据失败: {e}")
# 刷新缓存到数据库
self.thread_storage.flush()
return total_records
def sync_income(
self,
force_full: bool = False,
) -> Dict:
"""同步利润表数据。
Args:
force_full: 若为 True强制全量同步
Returns:
同步结果字典
"""
table_config = FINANCIAL_TABLES["income"]
table_name = table_config["table_name"]
period_field = table_config["period_field"]
print("\n" + "=" * 60)
print(f"[FinancialSync] 开始同步利润表 (force_full={force_full})")
print("=" * 60)
# 1. 全量同步:建表
if force_full:
self._create_table_if_not_exists(table_name)
start_quarter = DEFAULT_START_DATE
end_quarter = self._get_current_quarter()
else:
# 2. 增量同步:检查是否需要
needed, start_quarter, end_quarter = self._check_incremental_needed(
table_name, period_field
)
if not needed:
return {
"status": "skipped",
"message": "数据已是最新",
"table": table_name,
}
# 检查表是否存在,不存在则创建
if not self.storage.exists(table_name):
self._create_table_if_not_exists(table_name)
# 3. 执行同步
print(f"[FinancialSync] 同步范围: {start_quarter} -> {end_quarter}")
total_records = self._sync_single_table(
table_config, start_quarter, end_quarter
)
result = {
"status": "success",
"table": table_name,
"start_quarter": start_quarter,
"end_quarter": end_quarter,
"records": total_records,
}
print(f"[FinancialSync] 利润表同步完成: {total_records} 条记录")
return result
def sync_all(
self,
force_full: bool = False,
) -> Dict[str, Dict]:
"""同步所有财务数据表。
Args:
force_full: 若为 True强制全量同步
Returns:
各表同步结果字典
"""
results = {}
print("\n" + "=" * 60)
print(f"[FinancialSync] 开始同步所有财务数据 (force_full={force_full})")
print("=" * 60)
# 同步各财务数据表
for data_type, table_config in FINANCIAL_TABLES.items():
try:
if data_type == "income":
result = self.sync_income(force_full=force_full)
else:
# 预留其他表的同步逻辑
print(f"[FinancialSync] {data_type} 暂未实现,跳过")
result = {"status": "not_implemented"}
results[data_type] = result
except Exception as e:
print(f"[ERROR] 同步 {data_type} 失败: {e}")
results[data_type] = {"status": "error", "error": str(e)}
# 打印汇总
print("\n" + "=" * 60)
print("[FinancialSync] 同步汇总")
print("=" * 60)
for data_type, result in results.items():
status = result.get("status", "unknown")
records = result.get("records", 0)
print(f" {data_type}: {status} ({records} records)")
print("=" * 60)
return results
# =============================================================================
# 便捷函数
# =============================================================================
def sync_financial(
data_type: str = "income",
force_full: bool = False,
) -> Dict:
"""同步财务数据(便捷函数)。
Args:
data_type: 财务数据类型 ('income', 'balance', 'cashflow')
force_full: 若为 True强制全量同步
Returns:
同步结果字典
Example:
>>> # 增量同步利润表
>>> sync_financial()
>>> # 全量同步
>>> sync_financial(force_full=True)
"""
syncer = FinancialSync()
if data_type == "income":
return syncer.sync_income(force_full=force_full)
else:
raise ValueError(f"不支持的财务数据类型: {data_type}")
def sync_all_financial(force_full: bool = False) -> Dict[str, Dict]:
"""同步所有财务数据(便捷函数)。
Args:
force_full: 若为 True强制全量同步
Returns:
各表同步结果字典
Example:
>>> # 增量同步所有财务数据
>>> sync_all_financial()
>>> # 全量同步
>>> sync_all_financial(force_full=True)
"""
syncer = FinancialSync()
return syncer.sync_all(force_full=force_full)
def preview_sync() -> Dict:
"""预览同步信息(不实际同步)。
Returns:
预览信息字典:
{
'income': {
'sync_needed': bool,
'latest_quarter': str,
'current_quarter': str,
'start_quarter': str,
'end_quarter': str,
},
...
}
"""
syncer = FinancialSync()
preview = {}
for data_type, table_config in FINANCIAL_TABLES.items():
if data_type != "income":
continue
table_name = table_config["table_name"]
period_field = table_config["period_field"]
# 获取最新季度
latest_quarter = syncer._get_latest_quarter(table_name, period_field)
current_quarter = syncer._get_current_quarter()
# 检查是否需要同步
needed, start_quarter, end_quarter = syncer._check_incremental_needed(
table_name, period_field
)
preview[data_type] = {
"sync_needed": needed,
"latest_quarter": latest_quarter,
"current_quarter": current_quarter,
"start_quarter": start_quarter,
"end_quarter": end_quarter,
}
return preview
# =============================================================================
# 主程序入口
# =============================================================================
if __name__ == "__main__":
import sys
print("=" * 60)
print("财务数据同步模块")
print("=" * 60)
print("\n使用方式:")
print(" # 预览同步信息")
print(
" from src.data.api_wrappers.financial_data.api_financial_sync import preview_sync"
)
print(" preview = preview_sync()")
print("")
print(" # 增量同步(推荐)")
print(
" from src.data.api_wrappers.financial_data.api_financial_sync import sync_financial"
)
print(" sync_financial()")
print("")
print(" # 全量同步")
print(" sync_financial(force_full=True)")
print("")
print(" # 同步所有财务数据")
print(
" from src.data.api_wrappers.financial_data.api_financial_sync import sync_all_financial"
)
print(" sync_all_financial()")
print("=" * 60)
# 默认执行增量同步
if len(sys.argv) > 1 and sys.argv[1] == "--full":
print("\n[Main] 执行全量同步...")
result = sync_all_financial(force_full=True)
else:
print("\n[Main] 执行增量同步...")
result = sync_financial()
print("\n[Main] 执行完成!")
print(f"结果: {result}")

View File

@@ -0,0 +1,139 @@
"""利润表数据接口 (VIP 版本)
使用 Tushare VIP 接口 (income_vip) 获取利润表数据。
按季度同步,一次请求获取一个季度的全部上市公司数据。
接口说明:
- income_vip: 获取某一季度全部上市公司利润表数据
- 需要 5000 积分才能调用
- period 参数为报告期(季度最后一天,如 20231231
"""
import pandas as pd
from typing import Optional, List
from tqdm import tqdm
from src.data.client import TushareClient
from src.data.storage import ThreadSafeStorage
from src.data.utils import get_today_date, get_quarters_in_range
def get_income(
period: str,
fields: Optional[str] = None,
) -> pd.DataFrame:
"""获取利润表数据 (VIP 接口)
从 Tushare 获取指定季度的全部上市公司利润表数据。
Args:
period: 报告期,季度最后一天日期 (如 '20231231', '20230930')
- 0331: 一季报
- 0630: 半年报
- 0930: 三季报
- 1231: 年报
fields: 指定返回字段,默认返回全部字段
Returns:
pd.DataFrame 包含利润表数据:
- ts_code: 股票代码
- ann_date: 公告日期
- end_date: 报告期
- basic_eps: 基本每股收益
- report_type: 报告类型 (1=合并报表)
Example:
>>> data = get_income('20231231')
>>> print(data[['ts_code', 'ann_date', 'basic_eps']].head())
"""
client = TushareClient()
# 默认字段返回全部字段利润表有100+字段)
if fields is None:
fields = "ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,end_type,basic_eps,diluted_eps,total_revenue,revenue,int_income,prem_earned,comm_income,n_commis_income,n_oth_income,n_oth_b_income,prem_income,out_prem,une_prem_reser,reins_income,n_sec_tb_income,n_sec_uw_income,n_asset_mg_income,oth_b_income,fv_value_chg_gain,invest_income,ass_invest_income,forex_gain,total_cogs,oper_cost,int_exp,comm_exp,biz_tax_surchg,sell_exp,admin_exp,fin_exp,assets_impair_loss,prem_refund,compens_payout,reser_insur_liab,div_payt,reins_exp,oper_exp,compens_payout_refu,insur_reser_refu,reins_cost_refund,other_bus_cost,operate_profit,non_oper_income,non_oper_exp,nca_disploss,total_profit,income_tax,n_income,n_income_attr_p,minority_gain,oth_compr_income,t_compr_income,compr_inc_attr_p,compr_inc_attr_m_s,ebit,ebitda,insurance_exp,undist_profit,distable_profit,rd_exp,fin_exp_int_exp,fin_exp_int_inc,transfer_surplus_rese,transfer_housing_imprest,transfer_oth,adj_lossgain,withdra_legal_surplus,withdra_legal_pubfund,withdra_biz_devfund,withdra_rese_fund,withdra_oth_ersu,workers_welfare,distr_profit_shrhder,prfshare_payable_dvd,comshare_payable_dvd,capit_comstock_div,net_after_nr_lp_correct,credit_impa_loss,net_expo_hedging_benefits,oth_impair_loss_assets,total_opcost,amodcost_fin_assets,oth_income,asset_disp_income,continued_net_profit,end_net_profit,update_flag"
params = {"fields": fields, "period": period}
return client.query("income_vip", **params)
# =============================================================================
# IncomeSync - 利润表数据批量同步类
# =============================================================================
class IncomeSync:
"""利润表数据批量同步管理器 (VIP 版本)
功能特性:
- 按季度同步,每次请求获取该季度全部上市公司数据
- 使用 income_vip 接口
- 只保留合并报表report_type=1
- 使用 ThreadSafeStorage 安全写入
Example:
>>> sync = IncomeSync()
>>> sync.sync(start_date='20200101', end_date='20231231')
"""
def __init__(self):
"""初始化同步管理器"""
self.storage = ThreadSafeStorage()
self.client = TushareClient()
def sync(
self,
start_date: str,
end_date: Optional[str] = None,
) -> None:
"""同步利润表数据
Args:
start_date: 开始日期 YYYYMMDD
end_date: 结束日期 YYYYMMDD默认为今天
"""
if end_date is None:
end_date = get_today_date()
# 获取日期范围内的所有季度
quarters = get_quarters_in_range(start_date, end_date)
print(f"[IncomeSync] 计划同步 {len(quarters)} 个季度: {quarters}")
# 对每个季度调用 income_vip 获取全部股票数据
for period in tqdm(quarters, desc="Syncing income by quarter"):
try:
df = get_income(period)
if df.empty:
print(f"[WARN] 季度 {period} 无数据")
continue
# 只保留合并报表 (report_type='1',注意是字符串)
if "report_type" in df.columns:
df = df[df["report_type"] == "1"]
if not df.empty:
self.storage.queue_save("financial_income", df)
print(f"[IncomeSync] 季度 {period} -> {len(df)} 条记录")
except Exception as e:
print(f"[ERROR] 获取季度 {period} 数据失败: {e}")
# 刷新缓存到数据库
self.storage.flush()
print(f"[IncomeSync] 同步完成,共处理 {len(quarters)} 个季度")
def sync_income(
start_date: str,
end_date: Optional[str] = None,
) -> None:
"""同步利润表数据(便捷函数)
Args:
start_date: 开始日期 YYYYMMDD
end_date: 结束日期 YYYYMMDD默认为今天
Example:
>>> sync_income('20200101')
>>> sync_income('20200101', '20231231')
"""
syncer = IncomeSync()
syncer.sync(start_date, end_date)

View File

@@ -0,0 +1,145 @@
利润表
接口income可以通过数据工具调试和查看数据。
描述:获取上市公司财务利润表数据
积分用户需要至少2000积分才可以调取具体请参阅积分获取办法
提示当前接口只能按单只股票获取其历史数据如果需要获取某一季度全部上市公司数据请使用income_vip接口参数一致需积攒5000积分。
输入参数
名称 类型 必选 描述
ts_code str Y 股票代码
ann_date str N 公告日期YYYYMMDD格式下同
f_ann_date str N 实际公告日期
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 报告期类型
basic_eps float Y 基本每股收益
diluted_eps float Y 稀释每股收益
total_revenue float Y 营业总收入
revenue float Y 营业收入
int_income float Y 利息收入
prem_earned float Y 已赚保费
comm_income float Y 手续费及佣金收入
n_commis_income float Y 手续费及佣金净收入
n_oth_income float Y 其他经营净收益
n_oth_b_income float Y 加:其他业务净收益
prem_income float Y 保险业务收入
out_prem float Y 减:分出保费
une_prem_reser float Y 提取未到期责任准备金
reins_income float Y 其中:分保费收入
n_sec_tb_income float Y 代理买卖证券业务净收入
n_sec_uw_income float Y 证券承销业务净收入
n_asset_mg_income float Y 受托客户资产管理业务净收入
oth_b_income float Y 其他业务收入
fv_value_chg_gain float Y 加:公允价值变动净收益
invest_income float Y 加:投资净收益
ass_invest_income float Y 其中:对联营企业和合营企业的投资收益
forex_gain float Y 加:汇兑净收益
total_cogs float Y 营业总成本
oper_cost float Y 减:营业成本
int_exp float Y 减:利息支出
comm_exp float Y 减:手续费及佣金支出
biz_tax_surchg float Y 减:营业税金及附加
sell_exp float Y 减:销售费用
admin_exp float Y 减:管理费用
fin_exp float Y 减:财务费用
assets_impair_loss float Y 减:资产减值损失
prem_refund float Y 退保金
compens_payout float Y 赔付总支出
reser_insur_liab float Y 提取保险责任准备金
div_payt float Y 保户红利支出
reins_exp float Y 分保费用
oper_exp float Y 营业支出
compens_payout_refu float Y 减:摊回赔付支出
insur_reser_refu float Y 减:摊回保险责任准备金
reins_cost_refund float Y 减:摊回分保费用
other_bus_cost float Y 其他业务成本
operate_profit float Y 营业利润
non_oper_income float Y 加:营业外收入
non_oper_exp float Y 减:营业外支出
nca_disploss float Y 其中:减:非流动资产处置净损失
total_profit float Y 利润总额
income_tax float Y 所得税费用
n_income float Y 净利润(含少数股东损益)
n_income_attr_p float Y 净利润(不含少数股东损益)
minority_gain float Y 少数股东损益
oth_compr_income float Y 其他综合收益
t_compr_income float Y 综合收益总额
compr_inc_attr_p float Y 归属于母公司(或股东)的综合收益总额
compr_inc_attr_m_s float Y 归属于少数股东的综合收益总额
ebit float Y 息税前利润
ebitda float Y 息税折旧摊销前利润
insurance_exp float Y 保险业务支出
undist_profit float Y 年初未分配利润
distable_profit float Y 可分配利润
rd_exp float Y 研发费用
fin_exp_int_exp float Y 财务费用:利息费用
fin_exp_int_inc float Y 财务费用:利息收入
transfer_surplus_rese float Y 盈余公积转入
transfer_housing_imprest float Y 住房周转金转入
transfer_oth float Y 其他转入
adj_lossgain float Y 调整以前年度损益
withdra_legal_surplus float Y 提取法定盈余公积
withdra_legal_pubfund float Y 提取法定公益金
withdra_biz_devfund float Y 提取企业发展基金
withdra_rese_fund float Y 提取储备基金
withdra_oth_ersu float Y 提取任意盈余公积金
workers_welfare float Y 职工奖金福利
distr_profit_shrhder float Y 可供股东分配的利润
prfshare_payable_dvd float Y 应付优先股股利
comshare_payable_dvd float Y 应付普通股股利
capit_comstock_div float Y 转作股本的普通股股利
net_after_nr_lp_correct float N 扣除非经常性损益后的净利润(更正前)
credit_impa_loss float N 信用减值损失
net_expo_hedging_benefits float N 净敞口套期收益
oth_impair_loss_assets float N 其他资产减值损失
total_opcost float N 营业总成本(二)
amodcost_fin_assets float N 以摊余成本计量的金融资产终止确认收益
oth_income float N 其他收益
asset_disp_income float N 资产处置收益
continued_net_profit float N 持续经营净利润
end_net_profit float N 终止经营净利润
update_flag str Y 更新标识
接口使用说明
pro = ts.pro_api()
df = pro.income(ts_code='600000.SH', start_date='20180101', end_date='20180730', fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,basic_eps,diluted_eps')
获取某一季度全部股票数据
df = pro.income_vip(period='20181231',fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,basic_eps,diluted_eps')
数据样例
ts_code ann_date f_ann_date end_date report_type comp_type basic_eps diluted_eps \
0 600000.SH 20180428 20180428 20180331 1 2 0.46 0.46
1 600000.SH 20180428 20180428 20180331 1 2 0.46 0.46
2 600000.SH 20180428 20180428 20171231 1 2 1.84 1.84
主要报表类型说明
代码 | 类型 | 说明
---- | ----- | ---- |
1 | 合并报表 | 上市公司最新报表(默认)
2 | 单季合并 | 单一季度的合并报表
3 | 调整单季合并表 | 调整后的单季合并报表(如果有)
4 | 调整合并报表 | 本年度公布上年同期的财务报表数据,报告期为上年度
5 | 调整前合并报表 | 数据发生变更,将原数据进行保留,即调整前的原数据
6 | 母公司报表 | 该公司母公司的财务报表数据
7 | 母公司单季表 | 母公司的单季度表
8 | 母公司调整单季表 | 母公司调整后的单季表
9 | 母公司调整表 | 该公司母公司的本年度公布上年同期的财务报表数据
10 | 母公司调整前报表 | 母公司调整之前的原始财务报表数据
11 | 母公司调整前合并报表 | 母公司调整之前合并报表原数据
12 | 母公司调整前报表 | 母公司报表发生变更前保留的原数据

View File

@@ -90,6 +90,113 @@ class Storage:
CREATE INDEX IF NOT EXISTS idx_daily_date_code ON daily(trade_date, ts_code)
""")
# Create financial_income table for income statement data
# 完整的利润表字段94列全部
self._connection.execute("""
CREATE TABLE IF NOT EXISTS financial_income (
ts_code VARCHAR(16) NOT NULL,
ann_date DATE,
f_ann_date DATE,
end_date DATE NOT NULL,
report_type INTEGER,
comp_type INTEGER,
end_type VARCHAR(10),
basic_eps DOUBLE,
diluted_eps DOUBLE,
total_revenue DOUBLE,
revenue DOUBLE,
int_income DOUBLE,
prem_earned DOUBLE,
comm_income DOUBLE,
n_commis_income DOUBLE,
n_oth_income DOUBLE,
n_oth_b_income DOUBLE,
prem_income DOUBLE,
out_prem DOUBLE,
une_prem_reser DOUBLE,
reins_income DOUBLE,
n_sec_tb_income DOUBLE,
n_sec_uw_income DOUBLE,
n_asset_mg_income DOUBLE,
oth_b_income DOUBLE,
fv_value_chg_gain DOUBLE,
invest_income DOUBLE,
ass_invest_income DOUBLE,
forex_gain DOUBLE,
total_cogs DOUBLE,
oper_cost DOUBLE,
int_exp DOUBLE,
comm_exp DOUBLE,
biz_tax_surchg DOUBLE,
sell_exp DOUBLE,
admin_exp DOUBLE,
fin_exp DOUBLE,
assets_impair_loss DOUBLE,
prem_refund DOUBLE,
compens_payout DOUBLE,
reser_insur_liab DOUBLE,
div_payt DOUBLE,
reins_exp DOUBLE,
oper_exp DOUBLE,
compens_payout_refu DOUBLE,
insur_reser_refu DOUBLE,
reins_cost_refund DOUBLE,
other_bus_cost DOUBLE,
operate_profit DOUBLE,
non_oper_income DOUBLE,
non_oper_exp DOUBLE,
nca_disploss DOUBLE,
total_profit DOUBLE,
income_tax DOUBLE,
n_income DOUBLE,
n_income_attr_p DOUBLE,
minority_gain DOUBLE,
oth_compr_income DOUBLE,
t_compr_income DOUBLE,
compr_inc_attr_p DOUBLE,
compr_inc_attr_m_s DOUBLE,
ebit DOUBLE,
ebitda DOUBLE,
insurance_exp DOUBLE,
undist_profit DOUBLE,
distable_profit DOUBLE,
rd_exp DOUBLE,
fin_exp_int_exp DOUBLE,
fin_exp_int_inc DOUBLE,
transfer_surplus_rese DOUBLE,
transfer_housing_imprest DOUBLE,
transfer_oth DOUBLE,
adj_lossgain DOUBLE,
withdra_legal_surplus DOUBLE,
withdra_legal_pubfund DOUBLE,
withdra_biz_devfund DOUBLE,
withdra_rese_fund DOUBLE,
withdra_oth_ersu DOUBLE,
workers_welfare DOUBLE,
distr_profit_shrhder DOUBLE,
prfshare_payable_dvd DOUBLE,
comshare_payable_dvd DOUBLE,
capit_comstock_div DOUBLE,
net_after_nr_lp_correct DOUBLE,
credit_impa_loss DOUBLE,
net_expo_hedging_benefits DOUBLE,
oth_impair_loss_assets DOUBLE,
total_opcost DOUBLE,
amodcost_fin_assets DOUBLE,
oth_income DOUBLE,
asset_disp_income DOUBLE,
continued_net_profit DOUBLE,
end_net_profit DOUBLE,
update_flag VARCHAR(1),
PRIMARY KEY (ts_code, end_date)
)
""")
# Create index for financial_income
self._connection.execute("""
CREATE INDEX IF NOT EXISTS idx_financial_ann ON financial_income(ts_code, ann_date)
""")
def save(self, name: str, data: pd.DataFrame, mode: str = "append") -> dict:
"""Save data to DuckDB.
@@ -104,13 +211,35 @@ class Storage:
if data.empty:
return {"status": "skipped", "rows": 0}
# Ensure date column is proper type
# 确保日期列是正确的类型 (YYYYMMDD -> date)
# trade_date: 日线数据日期
if "trade_date" in data.columns:
data = data.copy()
data["trade_date"] = pd.to_datetime(
data["trade_date"], format="%Y%m%d"
).dt.date
# ann_date: 公告日期
if "ann_date" in data.columns:
data = data.copy()
data["ann_date"] = pd.to_datetime(
data["ann_date"], format="%Y%m%d", errors="coerce"
).dt.date
# f_ann_date: 最终公告日期
if "f_ann_date" in data.columns:
data = data.copy()
data["f_ann_date"] = pd.to_datetime(
data["f_ann_date"], format="%Y%m%d", errors="coerce"
).dt.date
# end_date: 报告期/期末日期
if "end_date" in data.columns:
data = data.copy()
data["end_date"] = pd.to_datetime(
data["end_date"], format="%Y%m%d", errors="coerce"
).dt.date
# Register DataFrame as temporary view
self._connection.register("temp_data", data)

View File

@@ -4,7 +4,7 @@
"""
from datetime import datetime, timedelta
from typing import Optional
from typing import Optional, List
# 默认全量同步开始日期
@@ -73,3 +73,74 @@ def format_date(dt: datetime) -> str:
YYYYMMDD 格式的日期字符串
"""
return dt.strftime("%Y%m%d")
def is_quarter_end(date_str: str) -> bool:
"""判断是否为季度最后一天。
Args:
date_str: YYYYMMDD 格式的日期
Returns:
是否为季度最后一天
"""
month_day = date_str[4:]
return month_day in ("0331", "0630", "0930", "1231")
def date_to_quarter(date_str: str) -> str:
"""将日期转换为对应季度的最后一天。
Args:
date_str: YYYYMMDD 格式的日期
Returns:
季度最后一天,格式为 YYYYMMDD
例如: 20230115 -> 20230331
"""
year = date_str[:4]
month = int(date_str[4:6])
if month <= 3:
return year + "0331"
elif month <= 6:
return year + "0630"
elif month <= 9:
return year + "0930"
else:
return year + "1231"
def get_quarters_in_range(start_date: str, end_date: str) -> List[str]:
"""获取日期范围内的所有季度列表。
Args:
start_date: 开始日期 YYYYMMDD
end_date: 结束日期 YYYYMMDD
Returns:
季度列表,格式为 YYYYMMDD按时间倒序排列
例如: ['20231231', '20230930', '20230630', '20230331']
"""
quarters = []
# 将开始日期和结束日期都转换为季度
start_quarter = date_to_quarter(start_date)
end_quarter = date_to_quarter(end_date)
# 解析年份
start_year = int(start_date[:4])
end_year = int(end_date[:4])
# 遍历所有年份和季度
for year in range(end_year, start_year - 1, -1):
year_str = str(year)
# 季度顺序: Q4, Q3, Q2, Q1 (倒序)
for quarter in ["1231", "0930", "0630", "0331"]:
quarter_date = year_str + quarter
# 只包含在范围内的季度
if quarter_date >= start_quarter and quarter_date <= end_quarter:
quarters.append(quarter_date)
return quarters