commit;
This commit is contained in:
parent
a0de689b3b
commit
34f1cfd6e6
69
src/app.py
69
src/app.py
|
@ -46,6 +46,7 @@ from src.scripts.stock_daily_data_collector import collect_stock_daily_data
|
|||
from valuation_analysis.financial_analysis import FinancialAnalyzer
|
||||
from src.valuation_analysis.stock_price_collector import StockPriceCollector
|
||||
from src.quantitative_analysis.batch_stock_price_collector import fetch_and_store_stock_data, get_stock_realtime_info_from_redis
|
||||
from src.quantitative_analysis.momentum_analysis import MomentumAnalyzer
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(
|
||||
|
@ -1960,6 +1961,7 @@ def industry_analysis():
|
|||
"type": "line",
|
||||
"data": valuation_data['avg_values'],
|
||||
"markLine": {
|
||||
"symbol": "none",
|
||||
"data": [
|
||||
{"name": "历史最小值", "yAxis": percentiles['min'], "lineStyle": {"color": "#28a745", "type": "dashed"}},
|
||||
{"name": "历史最大值", "yAxis": percentiles['max'], "lineStyle": {"color": "#dc3545", "type": "dashed"}},
|
||||
|
@ -2323,6 +2325,20 @@ def get_stock_price_range():
|
|||
"message": "缺少必要参数: stock_code"
|
||||
}), 400
|
||||
|
||||
# 兼容处理股票代码 (SZ002009, 002009.SZ, 002009)
|
||||
stock_code = stock_code.strip().upper()
|
||||
if '.' in stock_code: # 处理 002009.SZ 格式
|
||||
parts = stock_code.split('.')
|
||||
if len(parts) == 2:
|
||||
stock_code = f"{parts[1]}{parts[0]}"
|
||||
elif stock_code.isdigit(): # 处理 002009 格式
|
||||
if stock_code.startswith(('60', '68')):
|
||||
stock_code = f"SH{stock_code}"
|
||||
elif stock_code.startswith(('00', '30', '20')):
|
||||
stock_code = f"SZ{stock_code}"
|
||||
elif stock_code.startswith(('8', '43', '87')):
|
||||
stock_code = f"BJ{stock_code}"
|
||||
|
||||
# 计算一年前的日期作为默认起始日期
|
||||
default_start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
||||
start_date = request.args.get('start_date', default_start_date)
|
||||
|
@ -2923,6 +2939,59 @@ def filter_industry_crowding():
|
|||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/api/quantitative/momentum_by_plate', methods=['GET'])
|
||||
def get_momentum_by_plate():
|
||||
"""
|
||||
根据行业或概念板块名称,批量获取所有成分股的动量数据
|
||||
---
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
type: string
|
||||
required: true
|
||||
description: 行业或概念的名称.
|
||||
- name: type
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: 板块类型, 'industry' (默认) 或 'concept'.
|
||||
responses:
|
||||
200:
|
||||
description: A list of momentum indicators for stocks in the plate.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
"""
|
||||
try:
|
||||
plate_name = request.args.get('name')
|
||||
plate_type = request.args.get('type', 'industry')
|
||||
|
||||
if not plate_name:
|
||||
return jsonify({'success': False, 'message': '必须提供板块名称(name参数)'}), 400
|
||||
|
||||
is_concept = plate_type.lower() == 'concept'
|
||||
|
||||
analyzer = MomentumAnalyzer()
|
||||
result = analyzer.analyze_momentum_by_name(plate_name, is_concept=is_concept)
|
||||
|
||||
if result.get('success'):
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify(result), 404
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量获取板块动量数据接口异常: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
@app.route('/scheduler/batch_stock_price/collection', methods=['GET'])
|
||||
def run_batch_stock_price_collection():
|
||||
"""批量采集A股行情并保存到数据库"""
|
||||
|
|
|
@ -57,7 +57,7 @@ def fetch_and_store_stock_data(page_size=90):
|
|||
'page': 1,
|
||||
'size': page_size,
|
||||
'order': 'desc',
|
||||
'order_by': 'percent',
|
||||
'order_by': 'dividend_yield',
|
||||
'market': 'CN',
|
||||
'type': stock_type
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
from src.valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||
except ImportError:
|
||||
# 兼容在不同环境下执行
|
||||
from valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger("momentum_analysis")
|
||||
|
||||
class MomentumAnalyzer:
|
||||
"""动量分析器类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化动量分析器"""
|
||||
self.industry_analyzer = IndustryAnalyzer()
|
||||
self.momentum_api_url = "http://192.168.18.42:5000/api/dify/getStockMomentumIndex"
|
||||
logger.info("动量分析器初始化完成")
|
||||
|
||||
def get_stocks_by_name(self, name: str, is_concept: bool = False) -> List[str]:
|
||||
"""
|
||||
根据行业或概念名称获取股票列表。
|
||||
返回的股票代码格式为 '600036.SH'
|
||||
"""
|
||||
if is_concept:
|
||||
# 调用获取概念成份股的方法
|
||||
raw_codes = self.industry_analyzer.get_concept_stocks(name)
|
||||
else:
|
||||
# 调用获取行业成份股的方法
|
||||
raw_codes = self.industry_analyzer.get_industry_stocks(name)
|
||||
|
||||
# 统一将 'SH600036' 格式转换为 '600036.SH' 格式
|
||||
stock_list = []
|
||||
if raw_codes:
|
||||
for code in raw_codes:
|
||||
if not isinstance(code, str): continue
|
||||
|
||||
if code.startswith('SH'):
|
||||
stock_list.append(f"{code[2:]}.SH")
|
||||
elif code.startswith('SZ'):
|
||||
stock_list.append(f"{code[2:]}.SZ")
|
||||
elif code.startswith('BJ'):
|
||||
stock_list.append(f"{code[2:]}.BJ")
|
||||
|
||||
return stock_list
|
||||
|
||||
def get_momentum_indicators(self, stock_code: str, industry_codes: List[str]) -> Optional[Dict]:
|
||||
"""
|
||||
获取单个股票的动量指标数据。
|
||||
|
||||
Args:
|
||||
stock_code: 目标股票代码 (e.g., '600036.SH')
|
||||
industry_codes: 相关的股票代码列表 (e.g., ['600036.SH', '600000.SH'])
|
||||
|
||||
Returns:
|
||||
动量指标数据字典或None
|
||||
"""
|
||||
try:
|
||||
payload = {
|
||||
# 接口需要无后缀的代码列表
|
||||
"code_list": industry_codes,
|
||||
"target_code": stock_code
|
||||
}
|
||||
|
||||
response = requests.post(self.momentum_api_url, json=payload, timeout=500)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"获取动量指标失败({stock_code}): HTTP {response.status_code}, {response.text}")
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
# 为返回结果补充股票代码和名称
|
||||
data['stock_code'] = stock_code
|
||||
return data
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"获取动量指标超时({stock_code})")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取动量指标异常({stock_code}): {str(e)}")
|
||||
return None
|
||||
|
||||
def analyze_momentum_by_name(self, name: str, is_concept: bool = False) -> Dict:
|
||||
"""
|
||||
根据行业或概念名称,批量获取其中所有股票的动量指标。
|
||||
|
||||
Args:
|
||||
name: 行业或概念的名称
|
||||
is_concept: 是否为概念板块
|
||||
|
||||
Returns:
|
||||
包含所有股票动量数据的字典
|
||||
"""
|
||||
# 1. 获取板块内所有股票代码
|
||||
stock_list = self.get_stocks_by_name(name, is_concept)
|
||||
if not stock_list:
|
||||
return {'success': False, 'message': f'未找到板块 "{name}" 中的股票'}
|
||||
|
||||
all_results = []
|
||||
|
||||
# 2. 依次请求所有股票的动量数据
|
||||
for stock_code in stock_list:
|
||||
try:
|
||||
temp_list = [stock_code]
|
||||
result = self.get_momentum_indicators(stock_code, temp_list)
|
||||
if result:
|
||||
all_results.append(result)
|
||||
except Exception as exc:
|
||||
logger.error(f"处理股票 {stock_code} 的动量分析时产生异常: {exc}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'plate_name': name,
|
||||
'is_concept': is_concept,
|
||||
'stock_count': len(stock_list),
|
||||
'results_count': len(all_results),
|
||||
'data': all_results
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 示例用法
|
||||
analyzer = MomentumAnalyzer()
|
||||
|
||||
# 1. 测试行业
|
||||
industry_name = "证券"
|
||||
industry_results = analyzer.analyze_momentum_by_name(industry_name, is_concept=False)
|
||||
print(f"\n行业 '{industry_name}' 动量分析结果 (前5条):")
|
||||
if industry_results['success']:
|
||||
# 打印部分结果
|
||||
for item in industry_results['data'][:5]:
|
||||
print(item)
|
||||
else:
|
||||
print(industry_results['message'])
|
||||
|
||||
# 2. 测试概念
|
||||
concept_name = "芯片"
|
||||
concept_results = analyzer.analyze_momentum_by_name(concept_name, is_concept=True)
|
||||
print(f"\n概念 '{concept_name}' 动量分析结果 (前5条):")
|
||||
if concept_results['success']:
|
||||
# 打印部分结果
|
||||
for item in concept_results['data'][:5]:
|
||||
print(item)
|
||||
else:
|
||||
print(concept_results['message'])
|
|
@ -11,7 +11,7 @@ XUEQIU_HEADERS = {
|
|||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Client-Version': 'v2.44.75',
|
||||
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; snbim_minify=true; xq_a_token=ef79e6da376751a4bf6c1538103e9894d44473e1; xqat=ef79e6da376751a4bf6c1538103e9894d44473e1; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzUxNTE1MDgxLCJjdG0iOjE3NDg5MjMwODE2NDQsImNpZCI6ImQ5ZDBuNEFadXAifQ.gQrIt4VI73JLUFGVSTKpXidhFIMwlusBKyrzYwClwCBszXCooQY3WnFqlbXqSX3SwnMapuveOFUM5sGIOoZ8oDF8cZYs3HDz5vezR-2nes9gfZr2nZcUfZzNRJ299wlX3Zis5NbnzNlfnisUhv9GUfEZjQ_Rs37B4qRbQZVC2kdN1Z0xB8j1MplSTOsYj4IliQntuaTo-8SBh-4zz5244dnF85xREBVxtFzzCtHUhn9B-mzxE81_42nwrDscvow-4_jtlJXlqbehiAFxld-dCWDXwmCju9lRWu_WzdoQe19n-c6jhCZZ1pU1JGsYyhIAsd1gV064jQ6FxfN38so1Eg; xq_r_token=30a80318ebcabffbe194e7deecb108b665e8c894; Hm_lvt_1db88642e346389874251b5a1eded6e3=1749028611; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1750034926; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q07hqDyliiYwirG4tshiQBKD/KlYeDZDGFdDqx0Ei6FiBFKCezjCGbKBACQ5xC3o0aOyndbV3Ab3t8NXiK3y4xB3DExGkR0iYeK4DxrPD5xDTDWeDGDD3WxGaDmeDeho+D0bmHUOvrU7oD7eDXxGCDQFor4GWDiPD7Po45iim=KxD0xD1ESkEDDPDaroxDG5NlQ9weDi3rfgsLFbLDdwIqQimD753DlcqwXLXmktxGfoyzd=bdI8fDCKDjxdIx93QW33vimn4xqiDb37Yn4qe2qW+QKGxAiUmrKiirFBDFAQQDT7GN0xq3DPQGQBwOb7Y3FrjAPdmrRiYIvZRHm9HV4hY3Ybz777DbRGxi4T/48Bxt3GhUhtBhNfqtRDWq2qADPfYYsihWYPeD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q07hqDyliiYwirG4tshiQYeDA4pg2w+RYD7POqKepgqDlcb9wAqieP5+6V+KK5w6Q4dMdqCLomgLyxQn93CdLjc3pXEYq0/0h+jCdiOWudEmrIjmRf1+lLX0OGj02GXiiblBod5++dyGbWSnufTL+nxBWIQimWCI3ueZSne50WYT6afRSyCo79FGa6WEk2j30a5d9LFRZFb==8bO73cfarqe=kkkK09RmTUISi6qQwqZfChNd3Ktj6E3tj9GjXLWwV59vpUqOnFXIp9/rujWHt7v3KhIHMUrH70=mn1em1A7ujba3Y4jwqKyWRDR4q7/rCDFoyF7AiK4rNz018Ix0rYfYx+OYm2=nxNlxPGTKYStcOPuEDD',
|
||||
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; Hm_lvt_1db88642e346389874251b5a1eded6e3=1749028611; acw_tc=0a27aa3317504105803118918e00823643107980bbedc1c9307d37d1cf7fb7; xq_a_token=5b11f7f5a3986a802def7dea48868a3b2849e719; xqat=5b11f7f5a3986a802def7dea48868a3b2849e719; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzUzMDAzNjExLCJjdG0iOjE3NTA0MTE2MTEyODIsImNpZCI6ImQ5ZDBuNEFadXAifQ.FB12KEYSdWo5g3UqQbnfqR-Gopar8JkuDf54eSf86FzmuGG9XugW7osl3idav9oTgLzgWBut4X6a5-gbqn61wPPV7OV3dMO8oNyBZUxMjisaMBW_-IcUuQ1z-gtXBcHleNamANA-2H3Xf5mZNdVXAW_E0rQZE_y0TEqzeiLxfU5B_RJOTR1Zq_-BQaaOn_Tk0or_hu-nOZR-26lBtcBl1VoTR2Ov1tm_CRN375ohMcZniA265X8umpL_tysQ4m7oazNyezopJE6W7jt-djNGJXZAbLoVXF1U2ULKV325dPWHvPcSZOevxGprItb665QNZvXEzhBB-4fuzhAnYBsqGw; xq_r_token=2ba0614b400ec779704c3adaa7f17c2c2c88143b; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1750411602; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=Jg9N/8vN3mjfEOHOPlAxHQ+1x+X4nN7jc9vkKRkIGulMwceWqptDd3OUgWPM6XqKNq/15EvM032gWoeeYMHgRg%3D%3D; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0OHhqDyliGQQmhGtKq0aCDD/KlYeDZDGFdDqx0Ei6FiDHICezjQgDKgACjktpeBflQR5RYGlcNpp=0IDpnOAGdeGLDY=DCTKK420iDYYfDBYD74G+DDeDih3Dj4GmDGY=aeDFIQutVCRKdxDwDB=DmqG23ObDm4DfDDLorBD4Il2YDDtDAkaGNPDADA3doDDlYD84Kdb4DYpogQ0FdgahphusIeDMixGXzAlzx9CnoiWtV/vfrf2aHPGuDG=OcC0Hh2bmRT3f8hGxYDo5Qe8hx+Bx3rKq0DW7HRYqYYeYAh+2DR0DQhxRDxgGYgEw/rdPrd5kh6WdYYrcqsMkbZMshie5QhNiNQDoOBtQgdeAde6D/r5l05Dr=grAWG4HmmNBiQm44D; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0OHhqDyliGQQmhGtKq0aeDWhYebouIdHFW5NsDoenRT6eeD',
|
||||
'Referer': 'https://weibo.com/u/7735765253',
|
||||
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
# coding:utf-8
|
||||
# 更新港股列表的代码
|
||||
|
||||
import requests
|
||||
import pandas as pd
|
||||
from sqlalchemy import create_engine, text
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将项目根目录添加到Python路径,以便导入config
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from src.scripts.config import XUEQIU_HEADERS
|
||||
|
||||
def collect_hk_stock_codes(db_url):
|
||||
"""
|
||||
采集雪球港股列表数据,并存储到数据库。
|
||||
"""
|
||||
engine = create_engine(db_url)
|
||||
headers = XUEQIU_HEADERS
|
||||
base_url = "https://stock.xueqiu.com/v5/stock/screener/quote/list.json"
|
||||
page = 1
|
||||
page_size = 90
|
||||
all_data = []
|
||||
|
||||
print("--- Starting to collect Hong Kong stock codes ---")
|
||||
|
||||
# 采集前先清空表
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("TRUNCATE TABLE gp_code_hk"))
|
||||
print("Table `gp_code_hk` has been truncated.")
|
||||
except Exception as e:
|
||||
print(f"Error truncating table `gp_code_hk`: {e}")
|
||||
return
|
||||
|
||||
while True:
|
||||
params = {
|
||||
'page': page,
|
||||
'size': page_size,
|
||||
'order': 'desc',
|
||||
'order_by': 'market_capital',
|
||||
'market': 'HK',
|
||||
'type': 'hk',
|
||||
'is_delay': 'true'
|
||||
}
|
||||
|
||||
print(f"Fetching page {page}...")
|
||||
try:
|
||||
response = requests.get(base_url, headers=headers, params=params, timeout=20)
|
||||
if response.status_code != 200:
|
||||
print(f"Request failed with status code {response.status_code}")
|
||||
break
|
||||
|
||||
data = response.json()
|
||||
if data.get('error_code') != 0:
|
||||
print(f"API error: {data.get('error_description')}")
|
||||
break
|
||||
|
||||
stock_list = data.get('data', {}).get('list', [])
|
||||
if not stock_list:
|
||||
print("No more data found. Collection finished.")
|
||||
break
|
||||
|
||||
all_data.extend(stock_list)
|
||||
|
||||
# 如果获取到的数据少于每页数量,说明是最后一页
|
||||
if len(stock_list) < page_size:
|
||||
print("Reached the last page. Collection finished.")
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Request exception on page {page}: {e}")
|
||||
break
|
||||
|
||||
if all_data:
|
||||
print(f"--- Collected a total of {len(all_data)} stocks. Preparing to save to database. ---")
|
||||
df = pd.DataFrame(all_data)
|
||||
|
||||
# 数据映射和转换
|
||||
df_to_save = pd.DataFrame()
|
||||
df_to_save['gp_name'] = df['name']
|
||||
df_to_save['gp_code'] = df['symbol']
|
||||
df_to_save['gp_code_two'] = 'HK' + df['symbol'].astype(str)
|
||||
df_to_save['market_cap'] = df['market_capital']
|
||||
|
||||
try:
|
||||
df_to_save.to_sql('gp_code_hk', engine, if_exists='append', index=False)
|
||||
print("--- Successfully saved all data to `gp_code_hk`. ---")
|
||||
except Exception as e:
|
||||
print(f"Error saving data to database: {e}")
|
||||
else:
|
||||
print("--- No data collected. ---")
|
||||
|
||||
engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
|
||||
collect_hk_stock_codes(db_url)
|
|
@ -3,7 +3,7 @@
|
|||
import requests
|
||||
import pandas as pd
|
||||
from sqlalchemy import create_engine, text
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
from src.scripts.config import XUEQIU_HEADERS
|
||||
import gc
|
||||
|
@ -30,16 +30,22 @@ class StockDailyDataCollector:
|
|||
query_zs = "SELECT gp_code FROM gp_code_zs"
|
||||
df_zs = pd.read_sql(query_zs, self.engine)
|
||||
codes_zs = df_zs['gp_code'].tolist()
|
||||
|
||||
# 从gp_code_hk获取股票代码
|
||||
query_hk = "SELECT gp_code FROM gp_code_hk"
|
||||
df_hk = pd.read_sql(query_hk, self.engine)
|
||||
codes_hk = df_hk['gp_code'].tolist()
|
||||
|
||||
# 合并去重
|
||||
all_codes = list(set(codes_all + codes_zs))
|
||||
print(f"获取到股票代码: {len(codes_all)}个来自gp_code_all, {len(codes_zs)}个来自gp_code_zs, 去重后共{len(all_codes)}个")
|
||||
all_codes = list(set(codes_all + codes_zs + codes_hk))
|
||||
print(f"获取到股票代码: {len(codes_all)}个来自gp_code_all, {len(codes_zs)}个来自gp_code_zs, {len(codes_hk)}个来自gp_code_hk, 去重后共{len(all_codes)}个")
|
||||
return all_codes
|
||||
|
||||
def fetch_daily_stock_data(self, symbol, begin):
|
||||
url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-1&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance"
|
||||
def fetch_daily_stock_data(self, symbol, begin, count=-1):
|
||||
"""获取日线数据,count=-1表示最新一天,-2表示最近两天,-1800表示最近1800天"""
|
||||
url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count={count}&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance"
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, timeout=10)
|
||||
response = requests.get(url, headers=self.headers, timeout=20)
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Request error for {symbol}: {e}")
|
||||
|
@ -110,10 +116,155 @@ class StockDailyDataCollector:
|
|||
self.engine.dispose()
|
||||
print(f"Daily data fetching and saving completed for {date_str}.")
|
||||
|
||||
def delete_stock_history(self, symbol):
|
||||
"""删除指定股票的全部历史数据"""
|
||||
delete_query = text("DELETE FROM gp_day_data WHERE symbol = :symbol")
|
||||
try:
|
||||
with self.engine.begin() as conn:
|
||||
conn.execute(delete_query, {"symbol": symbol})
|
||||
print(f"Deleted history for {symbol}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error deleting history for {symbol}: {e}")
|
||||
return False
|
||||
|
||||
def refetch_and_save_history(self, symbol, days=1800):
|
||||
"""重新获取并保存指定股票的长期历史数据"""
|
||||
print(f"Refetching last {days} days for {symbol}...")
|
||||
begin = int(datetime.now().timestamp() * 1000)
|
||||
data = self.fetch_daily_stock_data(symbol, begin, count=-days)
|
||||
if data.get('error_code') == 0:
|
||||
df = self.transform_data(data, symbol)
|
||||
if df is not None and not df.empty:
|
||||
self.save_batch_to_database([df])
|
||||
print(f"Successfully refetched and saved history for {symbol}.")
|
||||
else:
|
||||
print(f"No data transformed for {symbol} after refetch.")
|
||||
else:
|
||||
print(f"Error refetching history for {symbol}: {data.get('error_description')}")
|
||||
|
||||
def check_and_fix_ex_rights_data(self):
|
||||
"""
|
||||
检查所有股票是否发生除权,如果发生,则删除历史数据并重新获取。
|
||||
新逻辑:直接用API返回的上个交易日的时间戳去数据库查询,更稳妥。
|
||||
记录:除权日期、股票代码()、
|
||||
"""
|
||||
all_codes = self.fetch_all_stock_codes()
|
||||
ex_rights_log_data = []
|
||||
|
||||
print("--- Step 1: Checking for ex-rights stocks ---")
|
||||
for symbol in tqdm(all_codes, desc="Comparing prices"):
|
||||
# 1. 从API获取最近两天的日线数据
|
||||
begin = int(datetime.now().timestamp() * 1000)
|
||||
data = self.fetch_daily_stock_data(symbol, begin, count=-2)
|
||||
|
||||
api_timestamp_str = None
|
||||
api_close = None
|
||||
|
||||
if data.get('error_code') == 0 and data.get('data', {}).get('item') and len(data['data']['item']) >= 2:
|
||||
try:
|
||||
# API返回的数据是按时间升序的,[-2]是上个交易日
|
||||
prev_day_data = data['data']['item'][-2]
|
||||
columns = data['data']['column']
|
||||
|
||||
timestamp_index = columns.index('timestamp')
|
||||
close_index = columns.index('close')
|
||||
|
||||
api_timestamp_ms = prev_day_data[timestamp_index]
|
||||
api_close = prev_day_data[close_index]
|
||||
|
||||
# 将毫秒时间戳转换为'YYYY-MM-DD'格式,用于数据库查询
|
||||
api_timestamp_str = pd.to_datetime(api_timestamp_ms, unit='ms', utc=True).tz_convert('Asia/Shanghai').strftime('%Y-%m-%d')
|
||||
except (ValueError, IndexError, TypeError) as e:
|
||||
print(f"\nError parsing API data for {symbol}: {e}")
|
||||
continue # 处理下一只股票
|
||||
else:
|
||||
# 获取API数据失败或数据不足,跳过此股票
|
||||
continue
|
||||
|
||||
# 如果未能从API解析出上个交易日的数据,则跳过
|
||||
if api_timestamp_str is None or api_close is None:
|
||||
continue
|
||||
|
||||
# 2. 根据API返回的时间戳,从数据库查询当天的收盘价
|
||||
db_close = None
|
||||
query = text("SELECT `close` FROM gp_day_data WHERE symbol = :symbol AND `timestamp` LIKE :date_str")
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
result = conn.execute(query, {"symbol": symbol, "date_str": f"{api_timestamp_str}%"}).fetchone()
|
||||
db_close = result[0] if result else None
|
||||
except Exception as e:
|
||||
print(f"\nError getting DB close for {symbol} on {api_timestamp_str}: {e}")
|
||||
continue
|
||||
|
||||
# 3. 比较价格
|
||||
if db_close is not None:
|
||||
# 注意:数据库中取出的db_close可能是Decimal类型,需要转换
|
||||
if not abs(float(db_close) - api_close) < 0.001:
|
||||
print(f"\nEx-rights detected for {symbol} on {api_timestamp_str}: DB_close={db_close}, API_close={api_close}")
|
||||
ex_rights_log_data.append({
|
||||
'symbol': symbol,
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'db_price': float(db_close),
|
||||
'api_price': api_close,
|
||||
'log_time': datetime.now()
|
||||
})
|
||||
# 如果数据库当天没有数据,我们无法比较,所以不处理。
|
||||
# 这可能是新股或之前采集失败,不属于除权范畴。
|
||||
|
||||
# 4. 对发生除权的股票进行记录和修复
|
||||
if not ex_rights_log_data:
|
||||
print("\n--- No ex-rights stocks found. Data is consistent. ---")
|
||||
self.engine.dispose()
|
||||
return
|
||||
|
||||
# 在修复前,先将日志保存到数据库
|
||||
self.save_ex_rights_log(ex_rights_log_data)
|
||||
|
||||
# 从日志数据中提取出需要修复的股票代码列表
|
||||
ex_rights_stocks = [item['symbol'] for item in ex_rights_log_data]
|
||||
|
||||
print(f"\n--- Step 2: Found {len(ex_rights_stocks)} stocks to fix: {ex_rights_stocks} ---")
|
||||
for symbol in tqdm(ex_rights_stocks, desc="Fixing data"):
|
||||
if self.delete_stock_history(symbol):
|
||||
self.refetch_and_save_history(symbol, days=1800)
|
||||
|
||||
self.engine.dispose()
|
||||
print("\n--- Ex-rights data fixing process completed. ---")
|
||||
|
||||
def save_ex_rights_log(self, log_data: list):
|
||||
"""将除权日志保存到数据库"""
|
||||
if not log_data:
|
||||
return
|
||||
|
||||
print(f"--- Saving {len(log_data)} ex-rights events to log table... ---")
|
||||
try:
|
||||
df = pd.DataFrame(log_data)
|
||||
# 确保列名与数据库字段匹配
|
||||
df = df.rename(columns={
|
||||
'symbol': 'stock_code',
|
||||
'date': 'change_date',
|
||||
'db_price': 'before_price',
|
||||
'api_price': 'after_price',
|
||||
'log_time': 'update_time'
|
||||
})
|
||||
df.to_sql('gp_ex_rights_log', self.engine, if_exists='append', index=False)
|
||||
print("--- Ex-rights log saved successfully. ---")
|
||||
except Exception as e:
|
||||
print(f"!!! Error saving ex-rights log: {e}")
|
||||
|
||||
def collect_stock_daily_data(db_url, date=None):
|
||||
collector = StockDailyDataCollector(db_url)
|
||||
collector.fetch_data_for_date(date)
|
||||
collector.check_and_fix_ex_rights_data()
|
||||
|
||||
if __name__ == "__main__":
|
||||
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
|
||||
collect_stock_daily_data(db_url)
|
||||
|
||||
# --- 使用方式 ---
|
||||
# 1. 日常采集当天数据
|
||||
# collect_stock_daily_data(db_url)
|
||||
|
||||
# 2. 手动执行除权检查和数据修复
|
||||
# collector = StockDailyDataCollector(db_url)
|
||||
# collector.check_and_fix_ex_rights_data()
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
测试脚本: 验证导入路径是否正常工作
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 显示当前工作目录和Python路径
|
||||
logger.info(f"当前工作目录: {os.getcwd()}")
|
||||
logger.info(f"Python路径: {sys.path}")
|
||||
|
||||
def test_imports():
|
||||
"""测试导入各个模块"""
|
||||
success = True
|
||||
|
||||
# 测试导入 chat_bot
|
||||
try:
|
||||
logger.info("尝试导入 chat_bot 模块...")
|
||||
from fundamentals_llm.chat_bot import ChatBot
|
||||
logger.info("成功导入 chat_bot 模块")
|
||||
except ImportError as e:
|
||||
logger.error(f"导入 chat_bot 模块失败: {str(e)}")
|
||||
success = False
|
||||
|
||||
# 尝试替代路径
|
||||
try:
|
||||
from src.fundamentals_llm.chat_bot import ChatBot
|
||||
logger.info("成功从替代路径导入 chat_bot 模块")
|
||||
success = True
|
||||
except ImportError as e2:
|
||||
logger.error(f"从替代路径导入 chat_bot 模块失败: {str(e2)}")
|
||||
|
||||
# 测试导入 chat_bot_with_offline
|
||||
try:
|
||||
logger.info("尝试导入 chat_bot_with_offline 模块...")
|
||||
from fundamentals_llm.chat_bot_with_offline import ChatBot
|
||||
logger.info("成功导入 chat_bot_with_offline 模块")
|
||||
except ImportError as e:
|
||||
logger.error(f"导入 chat_bot_with_offline 模块失败: {str(e)}")
|
||||
success = False
|
||||
|
||||
# 尝试替代路径
|
||||
try:
|
||||
from src.fundamentals_llm.chat_bot_with_offline import ChatBot
|
||||
logger.info("成功从替代路径导入 chat_bot_with_offline 模块")
|
||||
success = True
|
||||
except ImportError as e2:
|
||||
logger.error(f"从替代路径导入 chat_bot_with_offline 模块失败: {str(e2)}")
|
||||
|
||||
# 测试导入 fundamental_analysis
|
||||
try:
|
||||
logger.info("尝试导入 fundamental_analysis 模块...")
|
||||
from fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
||||
logger.info("成功导入 fundamental_analysis 模块")
|
||||
except ImportError as e:
|
||||
logger.error(f"导入 fundamental_analysis 模块失败: {str(e)}")
|
||||
success = False
|
||||
|
||||
# 尝试替代路径
|
||||
try:
|
||||
from src.fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
||||
logger.info("成功从替代路径导入 fundamental_analysis 模块")
|
||||
success = True
|
||||
except ImportError as e2:
|
||||
logger.error(f"从替代路径导入 fundamental_analysis 模块失败: {str(e2)}")
|
||||
|
||||
# 测试导入 pdf_generator
|
||||
try:
|
||||
logger.info("尝试导入 pdf_generator 模块...")
|
||||
from fundamentals_llm.pdf_generator import generate_investment_report
|
||||
logger.info("成功导入 pdf_generator 模块")
|
||||
except ImportError as e:
|
||||
logger.error(f"导入 pdf_generator 模块失败: {str(e)}")
|
||||
success = False
|
||||
|
||||
# 尝试替代路径
|
||||
try:
|
||||
from src.fundamentals_llm.pdf_generator import generate_investment_report
|
||||
logger.info("成功从替代路径导入 pdf_generator 模块")
|
||||
success = True
|
||||
except ImportError as e2:
|
||||
logger.error(f"从替代路径导入 pdf_generator 模块失败: {str(e2)}")
|
||||
|
||||
return success
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("开始测试导入...")
|
||||
if test_imports():
|
||||
logger.info("所有导入测试通过")
|
||||
sys.exit(0)
|
||||
else:
|
||||
logger.error("导入测试失败")
|
||||
sys.exit(1)
|
Loading…
Reference in New Issue