Files
tushare/comprehensive_data_fetcher_5year.py
2025-10-19 07:46:53 +08:00

793 lines
37 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TuShare 5年历史数据全面获取器
获取所有股票的基本面、技术指标、公司信息、股东结构、基金、融资、公告、分红配股等数据
"""
import os
import sys
import time
import json
import logging
import pandas as pd
import tudata as ts
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import numpy as np
from concurrent.futures import ThreadPoolExecutor, as_completed
import sqlite3
import pymysql
from config_manager import get_config
class ComprehensiveDataFetcher:
"""全面数据获取器"""
def __init__(self, token: str = None):
self.config = get_config()
self.token = token or self.config.tushare.token
if not self.token:
raise ValueError("TuShare token is required")
ts.set_token(self.token)
self.pro = ts.pro_api()
# 设置日志
self.setup_logging()
# 数据库连接参数
self.db_config = {
'host': self.config.database.host,
'port': self.config.database.port,
'user': self.config.database.user,
'password': self.config.database.password,
'database': self.config.database.database,
'charset': self.config.database.charset
}
# 时间范围 (过去5年)
self.end_date = datetime.now().strftime('%Y%m%d')
self.start_date = (datetime.now() - timedelta(days=365*5)).strftime('%Y%m%d')
# 进度记录
self.progress_file = 'logs/comprehensive_5year_progress.json'
self.progress = self.load_progress()
# API限制配置
self.rate_limit = self.config.tushare.rate_limit # 每分钟请求数
self.batch_size = 50 # 批次大小
self.max_workers = 10 # 最大线程数
self.logger.info(f"🚀 初始化数据获取器 - 时间范围: {self.start_date} ~ {self.end_date}")
def setup_logging(self):
"""设置日志"""
os.makedirs('logs', exist_ok=True)
log_file = f'logs/comprehensive_5year_fetch_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def load_progress(self) -> Dict:
"""加载进度记录"""
if os.path.exists(self.progress_file):
try:
with open(self.progress_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {
'stocks': [],
'completed_categories': [],
'last_update': None,
'total_stocks': 0,
'processed_stocks': 0
}
def save_progress(self):
"""保存进度记录"""
self.progress['last_update'] = datetime.now().isoformat()
os.makedirs(os.path.dirname(self.progress_file), exist_ok=True)
with open(self.progress_file, 'w', encoding='utf-8') as f:
json.dump(self.progress, f, ensure_ascii=False, indent=2)
def get_stock_list(self) -> pd.DataFrame:
"""获取股票列表"""
self.logger.info("📋 获取股票列表...")
try:
# 获取所有股票基本信息
stocks = self.pro.stock_basic(
fields='ts_code,symbol,name,area,industry,market,list_date,is_hs'
)
# 过滤有效股票 (上市时间超过1年)
cutoff_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
stocks = stocks[stocks['list_date'] <= cutoff_date]
# 排除ST和退市股票
stocks = stocks[
(~stocks['name'].str.contains('ST', na=False)) &
(~stocks['name'].str.contains('退', na=False)) &
(~stocks['name'].str.contains(r'\*', na=False, regex=True))
]
self.logger.info(f"✅ 获取股票列表成功: {len(stocks)}只股票")
return stocks
except Exception as e:
self.logger.error(f"❌ 获取股票列表失败: {e}")
return pd.DataFrame()
def fetch_daily_data(self, ts_code: str) -> Optional[pd.DataFrame]:
"""获取日线数据"""
try:
time.sleep(0.1) # API限流
data = self.pro.daily(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date,
fields='ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount'
)
if not data.empty:
data['fetch_time'] = datetime.now()
self.logger.debug(f" 📊 日线数据: {len(data)}条记录")
return data
except Exception as e:
self.logger.error(f" ❌ 获取日线数据失败: {e}")
return None
def fetch_weekly_monthly_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取周线和月线数据"""
result = {}
try:
# 周线数据
time.sleep(0.1)
weekly = self.pro.weekly(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date,
fields='ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount'
)
if not weekly.empty:
weekly['fetch_time'] = datetime.now()
result['weekly'] = weekly
self.logger.debug(f" 📊 周线数据: {len(weekly)}条记录")
# 月线数据
time.sleep(0.1)
monthly = self.pro.monthly(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date,
fields='ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount'
)
if not monthly.empty:
monthly['fetch_time'] = datetime.now()
result['monthly'] = monthly
self.logger.debug(f" 📊 月线数据: {len(monthly)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取周/月线数据失败: {e}")
return result
def fetch_financial_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取财务数据"""
result = {}
try:
# 利润表
time.sleep(0.15)
income = self.pro.income(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231',
fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_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'
)
if not income.empty:
income['fetch_time'] = datetime.now()
result['income'] = income
self.logger.debug(f" 💰 利润表: {len(income)}条记录")
# 资产负债表
time.sleep(0.15)
balancesheet = self.pro.balancesheet(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231',
fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,total_share,cap_rese,undistr_porfit,surplus_rese,special_rese,money_cap,trad_asset,notes_receiv,accounts_receiv,oth_receiv,prepayment,div_receiv,int_receiv,inventories,amor_exp,nca_within_1y,sett_rsrv,loanto_oth_bank_fi,premium_receiv,reinsur_receiv,reinsur_res_receiv,pur_resale_fa,oth_cur_assets,total_cur_assets,fa_avail_for_sale,htm_invest,lt_eqt_invest,invest_real_estate,time_deposits,oth_assets,lt_rec,fix_assets,cip,const_materials,fixed_assets_disp,produc_bio_assets,oil_and_gas_assets,intan_assets,r_and_d,goodwill,lt_amor_exp,defer_tax_assets,decr_in_disbur,oth_nca,total_nca,cash_reser_cb,depos_in_oth_bfi,prec_metals,deriv_assets,rr_reinsur_une_prem,rr_reinsur_outstd_cla,rr_reinsur_lins_liab,rr_reinsur_lthins_liab,refund_depos,ph_pledge_loans,refund_cap_depos,indep_acct_assets,client_depos,client_prov,transac_seat_fee,invest_as_receiv,total_assets,lt_borr,st_borr,cb_borr,depos_ib_deposits,loan_oth_bank,trading_fl,notes_payable,acct_payable,adv_receipts,sold_for_repur_fa,comm_payable,payroll_payable,taxes_payable,int_payable,div_payable,oth_payable,acc_exp,deferred_inc,st_bonds_payable,payable_to_reinsurer,rsrv_insur_cont,acting_trading_sec,acting_uw_sec,non_cur_liab_due_1y,oth_cur_liab,total_cur_liab,bond_payable,lt_payable,specific_payables,estimated_liab,defer_tax_liab,defer_inc_non_cur_liab,oth_ncl,total_ncl,depos_oth_bfi,deriv_liab,depos,agency_bus_liab,oth_liab,prem_receiv_adva,depos_received,ph_invest,reser_une_prem,reser_outstd_claims,reser_lins_liab,reser_lthins_liab,indept_acc_liab,pledge_borr,indem_payable,policy_div_payable,total_liab,treasury_share,ordin_risk_reser,forex_differ,invest_loss_unconf,minority_int,total_hldr_eqy_exc_min_int,total_hldr_eqy_inc_min_int,total_liab_hldr_eqy,lt_payroll_payable,oth_comp_income,oth_eqt_tools,oth_eqt_tools_p_shr,lending_funds,acc_receivable,st_fin_payable,payables'
)
if not balancesheet.empty:
balancesheet['fetch_time'] = datetime.now()
result['balancesheet'] = balancesheet
self.logger.debug(f" 💰 资产负债表: {len(balancesheet)}条记录")
# 现金流量表
time.sleep(0.15)
cashflow = self.pro.cashflow(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231',
fields='ts_code,ann_date,f_ann_date,end_date,comp_type,report_type,net_profit,finan_exp,c_fr_sale_sg,recp_tax_rends,n_depos_incr_fi,n_incr_loans_cb,n_inc_borr_oth_fi,prem_fr_orig_contr,n_incr_insured_dep,n_reinsur_prem,n_incr_disp_tfa,ifc_cash_incr,n_incr_disp_faas,n_incr_loans_oth_bank,n_cap_incr_repur,c_fr_oth_operate_a,c_inf_fr_operate_a,c_paid_goods_s,c_paid_to_for_empl,c_paid_for_taxes,n_incr_clt_loan_adv,n_incr_dep_cbob,c_pay_claims_orig_inco,pay_handling_chrg,pay_comm_insur_plcy,oth_cash_pay_oper_act,st_cash_out_act,n_cashflow_act,oth_recp_ral_inv_act,c_disp_withdrwl_invest,c_recp_return_invest,n_recp_disp_fiolta,n_recp_disp_sobu,stot_inflows_inv_act,c_pay_acq_const_fiolta,c_paid_invest,n_disp_subs_oth_biz,oth_pay_ral_inv_act,n_incr_pledge_loan,stot_out_inv_act,n_cashflow_inv_act,c_recp_borrow,proc_issue_bonds,oth_cash_recp_ral_fnc_act,stot_cash_in_fnc_act,free_cashflow,c_prepay_amt_borr,c_pay_dist_dpcp_int_exp,incl_dvd_profit_paid_sc_ms,oth_cashpay_ral_fnc_act,stot_cashout_fnc_act,n_cash_flows_fnc_act,eff_fx_flu_cash,n_incr_cash_cash_equ,c_cash_equ_beg_period,c_cash_equ_end_period,c_recp_cap_contrib,incl_cash_rec_saims,uncon_invest_loss,prov_depr_assets,depr_fa_coga_dpba,amort_intang_assets,lt_amort_deferred_exp,decr_deferred_exp,incr_acc_exp,loss_disp_fiolta,loss_scr_fa,loss_fv_chg,invest_loss,decr_def_inc_tax_assets,incr_def_inc_tax_liab,decr_inventories,decr_oper_payable,incr_oper_payable,others,im_net_cashflow_oper_act,conv_debt_into_cap,conv_copbonds_due_within_1y,fa_fnc_leases,end_bal_cash,beg_bal_cash,end_bal_cash_equ,beg_bal_cash_equ,im_n_incr_cash_equ'
)
if not cashflow.empty:
cashflow['fetch_time'] = datetime.now()
result['cashflow'] = cashflow
self.logger.debug(f" 💰 现金流量表: {len(cashflow)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取财务数据失败: {e}")
return result
def fetch_financial_indicators(self, ts_code: str) -> Optional[pd.DataFrame]:
"""获取财务指标"""
try:
time.sleep(0.1)
data = self.pro.fina_indicator(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231',
fields='ts_code,ann_date,end_date,eps,dt_eps,total_revenue_ps,revenue_ps,capital_rese_ps,surplus_rese_ps,undist_profit_ps,extra_item,profit_dedt,gross_margin,current_ratio,quick_ratio,cash_ratio,invturn_days,arturn_days,inv_turn,ar_turn,ca_turn,fa_turn,assets_turn,op_income,valuechange_income,interst_income,daa,ebit,ebitda,fcff,fcfe,current_exint,noncurrent_exint,interestdebt,netdebt,tangible_asset,working_capital,networking_capital,invest_capital,retained_earnings,diluted2_eps,bps,ocfps,retainedps,cfps,ebit_ps,fcff_ps,fcfe_ps,netprofit_margin,grossprofit_margin,cogs_of_sales,expense_of_sales,profit_to_gr,saleexp_to_gr,adminexp_of_gr,finaexp_of_gr,impai_ttm,gc_of_gr,op_of_gr,ebit_of_gr,roe,roe_waa,roe_dt,roa,npta,roic,roe_yearly,roa2_yearly,roe_avg,opincome_of_ebt,investincome_of_ebt,n_op_profit_of_ebt,tax_to_ebt,dtprofit_to_profit,salescash_to_or,ocf_to_or,ocf_to_opincome,capitalized_to_da,debt_to_assets,assets_to_eqt,dp_assets_to_eqt,ca_to_assets,nca_to_assets,tbassets_to_totalassets,int_to_talcap,eqt_to_talcapital,currentdebt_to_debt,longdeb_to_debt,ocf_to_shortdebt,debt_to_eqt,eqt_to_debt,eqt_to_interestdebt,tangibleasset_to_debt,tangasset_to_intdebt,tangibleasset_to_netdebt,ocf_to_debt,ocf_to_interestdebt,ocf_to_netdebt,ebit_to_interest,longdebt_to_workingcapital,ebitda_to_debt,turn_days,roa_yearly,roa_dp,fixed_assets,profit_prefin_exp,non_op_profit,op_to_ebt,nop_to_ebt,ocf_to_profit,cash_to_liqdebt,cash_to_liqdebt_withinterest,op_to_liqdebt,op_to_debt,roic_yearly,total_fa_trun,profit_to_op,q_opincome,q_investincome,q_dtprofit,q_eps,q_netprofit_margin,q_gsprofit_margin,q_exp_to_sales,q_profit_to_gr,q_saleexp_to_gr,q_adminexp_to_gr,q_finaexp_to_gr,q_impair_to_gr_ttm,q_gc_to_gr,q_op_to_gr,q_roe,q_dt_roe,q_npta,q_opincome_to_ebt,q_investincome_to_ebt,q_dtprofit_to_profit,q_salescash_to_or,q_ocf_to_sales,q_ocf_to_or,basic_eps_yoy,dt_eps_yoy,cfps_yoy,op_yoy,ebt_yoy,netprofit_yoy,dt_netprofit_yoy,ocf_yoy,roe_yoy,bps_yoy,assets_yoy,eqt_yoy,tr_yoy,or_yoy,q_gr_yoy,q_gr_qoq,q_sales_yoy,q_sales_qoq,q_op_yoy,q_op_qoq,q_profit_yoy,q_profit_qoq,q_netprofit_yoy,q_netprofit_qoq,equity_yoy,rd_exp,equity_debt'
)
if not data.empty:
data['fetch_time'] = datetime.now()
self.logger.debug(f" 📊 财务指标: {len(data)}条记录")
return data
except Exception as e:
self.logger.error(f" ❌ 获取财务指标失败: {e}")
return None
def fetch_company_info(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取公司基本信息"""
result = {}
try:
# 公司基本信息
time.sleep(0.1)
company = self.pro.stock_company(
ts_code=ts_code,
fields='ts_code,exchange,chairman,manager,secretary,reg_capital,setup_date,province,city,introduction,website,email,office,employees,main_business,business_scope'
)
if not company.empty:
company['fetch_time'] = datetime.now()
result['company'] = company
self.logger.debug(f" 🏢 公司信息: {len(company)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取公司信息失败: {e}")
return result
def fetch_shareholder_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取股东数据"""
result = {}
try:
# 前十大股东
time.sleep(0.15)
top10_holders = self.pro.top10_holders(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231'
)
if not top10_holders.empty:
top10_holders['fetch_time'] = datetime.now()
result['top10_holders'] = top10_holders
self.logger.debug(f" 👥 前十大股东: {len(top10_holders)}条记录")
# 前十大流通股东
time.sleep(0.15)
top10_floatholders = self.pro.top10_floatholders(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231'
)
if not top10_floatholders.empty:
top10_floatholders['fetch_time'] = datetime.now()
result['top10_floatholders'] = top10_floatholders
self.logger.debug(f" 👥 前十大流通股东: {len(top10_floatholders)}条记录")
# 股东人数
time.sleep(0.1)
stk_holdernumber = self.pro.stk_holdernumber(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231'
)
if not stk_holdernumber.empty:
stk_holdernumber['fetch_time'] = datetime.now()
result['stk_holdernumber'] = stk_holdernumber
self.logger.debug(f" 👥 股东人数: {len(stk_holdernumber)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取股东数据失败: {e}")
return result
def fetch_fund_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取基金持股数据"""
result = {}
try:
# 基金持股
time.sleep(0.15)
fund_holdings = self.pro.fund_holdings(
ts_code=ts_code,
start_date=self.start_date[:4] + '0101',
end_date=self.end_date[:4] + '1231'
)
if not fund_holdings.empty:
fund_holdings['fetch_time'] = datetime.now()
result['fund_holdings'] = fund_holdings
self.logger.debug(f" 🏦 基金持股: {len(fund_holdings)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取基金数据失败: {e}")
return result
def fetch_financing_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取融资融券数据"""
result = {}
try:
# 融资融券交易汇总
time.sleep(0.1)
margin = self.pro.margin(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date
)
if not margin.empty:
margin['fetch_time'] = datetime.now()
result['margin'] = margin
self.logger.debug(f" 💳 融资融券: {len(margin)}条记录")
# 融资融券交易明细
time.sleep(0.1)
margin_detail = self.pro.margin_detail(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date
)
if not margin_detail.empty:
margin_detail['fetch_time'] = datetime.now()
result['margin_detail'] = margin_detail
self.logger.debug(f" 💳 融资融券明细: {len(margin_detail)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取融资数据失败: {e}")
return result
def fetch_dividend_data(self, ts_code: str) -> Dict[str, pd.DataFrame]:
"""获取分红配股数据"""
result = {}
try:
# 分红送股
time.sleep(0.1)
dividend = self.pro.dividend(
ts_code=ts_code,
fields='ts_code,end_date,ann_date,div_proc,stk_div,stk_bo_rate,stk_co_rate,cash_div,cash_div_tax,record_date,ex_date,pay_date,div_listdate,imp_ann_date'
)
if not dividend.empty:
dividend['fetch_time'] = datetime.now()
result['dividend'] = dividend
self.logger.debug(f" 💰 分红配股: {len(dividend)}条记录")
except Exception as e:
self.logger.error(f" ❌ 获取分红数据失败: {e}")
return result
def fetch_announcement_data(self, ts_code: str) -> Optional[pd.DataFrame]:
"""获取公告数据"""
try:
time.sleep(0.1)
data = self.pro.anns(
ts_code=ts_code,
start_date=self.start_date,
end_date=self.end_date,
fields='ts_code,ann_date,ann_type,title,content'
)
if not data.empty:
data['fetch_time'] = datetime.now()
self.logger.debug(f" 📢 公告: {len(data)}条记录")
return data
except Exception as e:
self.logger.error(f" ❌ 获取公告数据失败: {e}")
return None
def fetch_technical_indicators(self, daily_data: pd.DataFrame) -> pd.DataFrame:
"""计算技术指标"""
if daily_data.empty:
return daily_data
try:
# 确保数据按日期排序
daily_data = daily_data.sort_values('trade_date')
# 移动平均线
daily_data['ma5'] = daily_data['close'].rolling(window=5).mean()
daily_data['ma10'] = daily_data['close'].rolling(window=10).mean()
daily_data['ma20'] = daily_data['close'].rolling(window=20).mean()
daily_data['ma60'] = daily_data['close'].rolling(window=60).mean()
# RSI
delta = daily_data['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
daily_data['rsi'] = 100 - (100 / (1 + rs))
# MACD
ema12 = daily_data['close'].ewm(span=12).mean()
ema26 = daily_data['close'].ewm(span=26).mean()
daily_data['macd'] = ema12 - ema26
daily_data['macd_signal'] = daily_data['macd'].ewm(span=9).mean()
daily_data['macd_hist'] = daily_data['macd'] - daily_data['macd_signal']
# 布林带
daily_data['bb_middle'] = daily_data['close'].rolling(window=20).mean()
bb_std = daily_data['close'].rolling(window=20).std()
daily_data['bb_upper'] = daily_data['bb_middle'] + (bb_std * 2)
daily_data['bb_lower'] = daily_data['bb_middle'] - (bb_std * 2)
# KDJ
low_min = daily_data['low'].rolling(window=9).min()
high_max = daily_data['high'].rolling(window=9).max()
rsv = (daily_data['close'] - low_min) / (high_max - low_min) * 100
daily_data['kdj_k'] = rsv.ewm(com=2).mean()
daily_data['kdj_d'] = daily_data['kdj_k'].ewm(com=2).mean()
daily_data['kdj_j'] = 3 * daily_data['kdj_k'] - 2 * daily_data['kdj_d']
self.logger.debug(f" 📊 技术指标计算完成")
return daily_data
except Exception as e:
self.logger.error(f" ❌ 技术指标计算失败: {e}")
return daily_data
def save_to_database(self, data_dict: Dict, ts_code: str):
"""保存数据到数据库"""
try:
conn = pymysql.connect(**self.db_config)
for category, data in data_dict.items():
if data is None or (isinstance(data, pd.DataFrame) and data.empty):
continue
# 处理不同类型的数据
if isinstance(data, dict):
# 嵌套字典,如财务数据
for sub_category, sub_data in data.items():
if isinstance(sub_data, pd.DataFrame) and not sub_data.empty:
table_name = f"{category}_{sub_category}"
self._save_dataframe_to_mysql(conn, sub_data, table_name)
elif isinstance(data, pd.DataFrame):
# 直接是DataFrame
table_name = category
self._save_dataframe_to_mysql(conn, data, table_name)
conn.close()
self.logger.debug(f" 💾 数据保存成功")
except Exception as e:
self.logger.error(f" ❌ 数据保存失败: {e}")
def _save_dataframe_to_mysql(self, conn, df: pd.DataFrame, table_name: str):
"""保存DataFrame到MySQL"""
try:
# 转换数据类型
df = df.copy()
# 处理时间戳类型
for col in df.columns:
if df[col].dtype == 'object':
try:
df[col] = pd.to_datetime(df[col], errors='ignore')
except:
pass
# 使用to_sql保存数据
df.to_sql(
name=table_name,
con=conn,
if_exists='append',
index=False,
method='multi',
chunksize=1000
)
except Exception as e:
self.logger.debug(f" ⚠️ 表 {table_name} 保存警告: {e}")
def fetch_stock_comprehensive_data(self, ts_code: str, stock_name: str) -> bool:
"""获取单只股票的全面数据"""
self.logger.info(f"📈 处理股票: {stock_name}({ts_code})")
all_data = {}
try:
# 1. 日线数据
daily_data = self.fetch_daily_data(ts_code)
if daily_data is not None:
# 计算技术指标
daily_data = self.fetch_technical_indicators(daily_data)
all_data['daily'] = daily_data
# 2. 周线和月线数据
weekly_monthly = self.fetch_weekly_monthly_data(ts_code)
all_data.update(weekly_monthly)
# 3. 财务数据
financial_data = self.fetch_financial_data(ts_code)
all_data['financial'] = financial_data
# 4. 财务指标
financial_indicators = self.fetch_financial_indicators(ts_code)
if financial_indicators is not None:
all_data['financial_indicators'] = financial_indicators
# 5. 公司基本信息
company_info = self.fetch_company_info(ts_code)
all_data.update(company_info)
# 6. 股东数据
shareholder_data = self.fetch_shareholder_data(ts_code)
all_data.update(shareholder_data)
# 7. 基金持股
fund_data = self.fetch_fund_data(ts_code)
all_data.update(fund_data)
# 8. 融资融券
financing_data = self.fetch_financing_data(ts_code)
all_data.update(financing_data)
# 9. 分红配股
dividend_data = self.fetch_dividend_data(ts_code)
all_data.update(dividend_data)
# 10. 公告数据 (限制数量,避免过多数据)
announcement_data = self.fetch_announcement_data(ts_code)
if announcement_data is not None:
all_data['announcements'] = announcement_data
# 保存到数据库
self.save_to_database(all_data, ts_code)
self.logger.info(f"✅ {stock_name}({ts_code}) 数据获取完成")
return True
except Exception as e:
self.logger.error(f"❌ {stock_name}({ts_code}) 数据获取失败: {e}")
return False
def run_comprehensive_fetch(self):
"""运行全面数据获取"""
self.logger.info("🚀 开始5年历史数据全面获取...")
# 获取股票列表
stocks_df = self.get_stock_list()
if stocks_df.empty:
self.logger.error("❌ 无法获取股票列表,退出")
return
total_stocks = len(stocks_df)
self.progress['total_stocks'] = total_stocks
# 初始化进度
processed_count = 0
success_count = 0
failed_stocks = []
self.logger.info(f"📋 开始处理 {total_stocks} 只股票")
# 使用线程池并行处理
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
# 提交所有任务
future_to_stock = {}
for idx, row in stocks_df.iterrows():
ts_code = row['ts_code']
stock_name = row['name']
# 检查是否已处理
if ts_code in self.progress.get('completed_stocks', []):
processed_count += 1
continue
future = executor.submit(self.fetch_stock_comprehensive_data, ts_code, stock_name)
future_to_stock[future] = (ts_code, stock_name)
# 控制并发数
if len(future_to_stock) >= self.max_workers:
# 等待一批完成
for future in as_completed(list(future_to_stock.keys())[:5]):
ts_code, stock_name = future_to_stock[future]
try:
success = future.result()
processed_count += 1
if success:
success_count += 1
# 记录已完成的股票
if 'completed_stocks' not in self.progress:
self.progress['completed_stocks'] = []
self.progress['completed_stocks'].append(ts_code)
else:
failed_stocks.append((ts_code, stock_name))
# 更新进度
self.progress['processed_stocks'] = processed_count
# 定期保存进度
if processed_count % 50 == 0:
self.save_progress()
self.logger.info(f"📊 进度: {processed_count}/{total_stocks} ({processed_count/total_stocks*100:.1f}%)")
except Exception as e:
self.logger.error(f"❌ 股票 {stock_name}({ts_code}) 处理异常: {e}")
failed_stocks.append((ts_code, stock_name))
del future_to_stock[future]
# 处理剩余任务
for future in as_completed(future_to_stock.keys()):
ts_code, stock_name = future_to_stock[future]
try:
success = future.result()
processed_count += 1
if success:
success_count += 1
if 'completed_stocks' not in self.progress:
self.progress['completed_stocks'] = []
self.progress['completed_stocks'].append(ts_code)
else:
failed_stocks.append((ts_code, stock_name))
self.progress['processed_stocks'] = processed_count
except Exception as e:
self.logger.error(f"❌ 股票 {stock_name}({ts_code}) 处理异常: {e}")
failed_stocks.append((ts_code, stock_name))
# 保存最终进度
self.save_progress()
# 生成报告
self.generate_completion_report(total_stocks, success_count, failed_stocks)
def generate_completion_report(self, total_stocks: int, success_count: int, failed_stocks: List[Tuple[str, str]]):
"""生成完成报告"""
report = {
'completion_time': datetime.now().isoformat(),
'total_stocks': total_stocks,
'success_count': success_count,
'failed_count': len(failed_stocks),
'success_rate': success_count / total_stocks * 100 if total_stocks > 0 else 0,
'failed_stocks': failed_stocks,
'time_range': f"{self.start_date} ~ {self.end_date}",
'data_categories': [
'日线数据', '周线数据', '月线数据', '技术指标',
'财务数据(利润表/资产负债表/现金流量表)', '财务指标',
'公司基本信息', '股东结构', '基金持股',
'融资融券', '分红配股', '公告数据'
]
}
# 保存报告
report_file = f'logs/comprehensive_5year_report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
with open(report_file, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
# 打印摘要
self.logger.info("🎉 数据获取完成!")
self.logger.info(f"📊 总计股票: {total_stocks}")
self.logger.info(f"✅ 成功: {success_count} ({success_count/total_stocks*100:.1f}%)")
self.logger.info(f"❌ 失败: {len(failed_stocks)} ({len(failed_stocks)/total_stocks*100:.1f}%)")
self.logger.info(f"📄 详细报告: {report_file}")
if failed_stocks:
self.logger.info("❌ 失败股票列表:")
for ts_code, stock_name in failed_stocks[:10]: # 只显示前10个
self.logger.info(f" - {stock_name}({ts_code})")
if len(failed_stocks) > 10:
self.logger.info(f" ... 还有 {len(failed_stocks)-10} 只股票")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='TuShare 5年历史数据全面获取器')
parser.add_argument('--token', type=str, help='TuShare Pro API Token')
parser.add_argument('--test', action='store_true', help='测试模式 (只处理前5只股票)')
parser.add_argument('--incremental', action='store_true', help='增量更新模式')
parser.add_argument('--full', action='store_true', help='全量更新模式')
parser.add_argument('--deep-analysis', action='store_true', help='深度分析模式')
parser.add_argument('--start-date', type=str, help='开始日期 YYYYMMDD')
parser.add_argument('--end-date', type=str, help='结束日期 YYYYMMDD')
args = parser.parse_args()
try:
fetcher = ComprehensiveDataFetcher(token=args.token)
# 根据参数调整时间范围
if args.start_date:
fetcher.start_date = args.start_date
if args.end_date:
fetcher.end_date = args.end_date
if args.test:
# 测试模式
fetcher.logger.info("🧪 测试模式 - 只处理前5只股票")
stocks_df = fetcher.get_stock_list()
if not stocks_df.empty:
test_stocks = stocks_df.head(5)
for idx, row in test_stocks.iterrows():
ts_code = row['ts_code']
stock_name = row['name']
fetcher.fetch_stock_comprehensive_data(ts_code, stock_name)
fetcher.logger.info("🧪 测试模式完成")
elif args.incremental:
# 增量更新模式
fetcher.logger.info("📊 增量更新模式")
if not args.start_date:
# 默认获取昨天的数据
yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y%m%d')
fetcher.start_date = yesterday
fetcher.end_date = yesterday
fetcher.run_comprehensive_fetch()
elif args.full:
# 全量更新模式
fetcher.logger.info("🔄 全量更新模式")
fetcher.run_comprehensive_fetch()
elif args.deep_analysis:
# 深度分析模式
fetcher.logger.info("🧠 深度分析模式")
# 可以在这里添加额外的分析逻辑
fetcher.run_comprehensive_fetch()
else:
# 默认模式
fetcher.run_comprehensive_fetch()
except KeyboardInterrupt:
print("\n⚠️ 用户中断操作")
except Exception as e:
print(f"❌ 运行失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()