refactor: 代码审查修复 - 日期过滤、性能优化、数据泄露防护
- 修复 data_loader.py 财务数据日期过滤,支持按范围加载 - 优化 MADClipper 使用窗口函数替代 join,提升性能 - 修复训练日期边界问题,添加1天间隔避免数据泄露 - 新增 .gitignore 规则忽略训练输出目录
This commit is contained in:
@@ -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",
|
||||
|
||||
587
src/data/api_wrappers/financial_data/api_financial_sync.py
Normal file
587
src/data/api_wrappers/financial_data/api_financial_sync.py
Normal 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}")
|
||||
139
src/data/api_wrappers/financial_data/api_income.py
Normal file
139
src/data/api_wrappers/financial_data/api_income.py
Normal 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)
|
||||
145
src/data/api_wrappers/financial_data/financial_api.md
Normal file
145
src/data/api_wrappers/financial_data/financial_api.md
Normal 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 | 母公司调整前报表 | 母公司报表发生变更前保留的原数据
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user