updata updatekline.py
This commit is contained in:
@@ -1,298 +0,0 @@
|
||||
"""
|
||||
计算平均市值
|
||||
|
||||
根据 conditionalselection 表格中的股票代码,查找日K数据表格
|
||||
每个月计算一次结果存放在 hk_monthly_avg_2410_2508 表格中
|
||||
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from futu import *
|
||||
from pymysql import Error
|
||||
from MySQLHelper import MySQLHelper # MySQLHelper类保存为单独文件
|
||||
from typing import Optional, List, Dict, Union, Tuple
|
||||
|
||||
|
||||
def create_monthly_avg_table(db_config: dict, target_table: str = "monthly_close_avg") -> bool:
|
||||
"""
|
||||
创建专门存储2024年10月至2025年8月月度均值的表结构
|
||||
|
||||
Args:
|
||||
db_config: 数据库配置
|
||||
target_table: 目标表名
|
||||
|
||||
Returns:
|
||||
bool: 是否创建成功
|
||||
"""
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
create_sql = f"""
|
||||
CREATE TABLE IF NOT EXISTS {target_table} (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
stock_code VARCHAR(20) NOT NULL COMMENT '股票代码',
|
||||
stock_name VARCHAR(50) COMMENT '股票名称',
|
||||
ym_2410 DECIMAL(10, 3) COMMENT '2024年10月均收盘价',
|
||||
ym_2411 DECIMAL(10, 3) COMMENT '2024年11月均收盘价',
|
||||
ym_2412 DECIMAL(10, 3) COMMENT '2024年12月均收盘价',
|
||||
ym_2501 DECIMAL(10, 3) COMMENT '2025年1月均收盘价',
|
||||
ym_2502 DECIMAL(10, 3) COMMENT '2025年2月均收盘价',
|
||||
ym_2503 DECIMAL(10, 3) COMMENT '2025年3月均收盘价',
|
||||
ym_2504 DECIMAL(10, 3) COMMENT '2025年4月均收盘价',
|
||||
ym_2505 DECIMAL(10, 3) COMMENT '2025年5月均收盘价',
|
||||
ym_2506 DECIMAL(10, 3) COMMENT '2025年6月均收盘价',
|
||||
ym_2507 DECIMAL(10, 3) COMMENT '2025年7月均收盘价',
|
||||
ym_2508 DECIMAL(10, 3) COMMENT '2025年8月均收盘价',
|
||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
UNIQUE KEY uk_stock_code (stock_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='月度收盘价均值表(2024.10-2025.08)'
|
||||
"""
|
||||
db.execute_update(create_sql)
|
||||
# logging.info(f"创建/确认表 {target_table} 结构成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"创建表失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def calculate_and_save_monthly_avg(db_config: dict,
|
||||
source_table: str = "stock_quotes",
|
||||
target_table: str = "monthly_close_avg") -> bool:
|
||||
"""
|
||||
计算并保存2024年10月至2025年8月的月度收盘价均值
|
||||
|
||||
Args:
|
||||
db_config: 数据库配置
|
||||
source_table: 源数据表名
|
||||
target_table: 目标表名
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
# 定义分析的时间范围
|
||||
month_ranges = {
|
||||
'ym_2410': ('2024-10-01', '2024-10-31'),
|
||||
'ym_2411': ('2024-11-01', '2024-11-30'),
|
||||
'ym_2412': ('2024-12-01', '2024-12-31'),
|
||||
'ym_2501': ('2025-01-01', '2025-01-31'),
|
||||
'ym_2502': ('2025-02-01', '2025-02-28'),
|
||||
'ym_2503': ('2025-03-01', '2025-03-31'),
|
||||
'ym_2504': ('2025-04-01', '2025-04-30'),
|
||||
'ym_2505': ('2025-05-01', '2025-05-31'),
|
||||
'ym_2506': ('2025-06-01', '2025-06-30'),
|
||||
'ym_2507': ('2025-07-01', '2025-07-31'),
|
||||
'ym_2508': ('2025-08-01', '2025-08-31')
|
||||
}
|
||||
|
||||
try:
|
||||
# 确保表结构存在
|
||||
if not create_monthly_avg_table(db_config, target_table):
|
||||
return False
|
||||
|
||||
with MySQLHelper(**db_config) as db:
|
||||
# 获取所有股票代码和名称
|
||||
stock_info = db.execute_query(
|
||||
f"SELECT DISTINCT stock_code, stock_name FROM {source_table}"
|
||||
)
|
||||
|
||||
if not stock_info:
|
||||
logging.error("没有获取到股票基本信息")
|
||||
return False
|
||||
|
||||
# 为每只股票计算各月均值
|
||||
for stock in stock_info:
|
||||
stock_code = stock['stock_code']
|
||||
stock_name = stock['stock_name']
|
||||
|
||||
monthly_data = {'stock_code': stock_code, 'stock_name': stock_name}
|
||||
|
||||
# 计算每个月的均值
|
||||
for month_col, (start_date, end_date) in month_ranges.items():
|
||||
sql = f"""
|
||||
SELECT AVG(close_price) as avg_close
|
||||
FROM {source_table}
|
||||
WHERE stock_code = %s
|
||||
AND trade_date BETWEEN %s AND %s
|
||||
"""
|
||||
result = db.execute_query(sql, (stock_code, start_date, end_date))
|
||||
monthly_data[month_col] = float(result[0]['avg_close']) if result and result[0]['avg_close'] else None
|
||||
|
||||
# 插入或更新数据
|
||||
upsert_sql = f"""
|
||||
INSERT INTO {target_table} (
|
||||
stock_code, stock_name,
|
||||
ym_2410, ym_2411, ym_2412,
|
||||
ym_2501, ym_2502, ym_2503, ym_2504,
|
||||
ym_2505, ym_2506, ym_2507, ym_2508
|
||||
) VALUES (
|
||||
%(stock_code)s, %(stock_name)s,
|
||||
%(ym_2410)s, %(ym_2411)s, %(ym_2412)s,
|
||||
%(ym_2501)s, %(ym_2502)s, %(ym_2503)s, %(ym_2504)s,
|
||||
%(ym_2505)s, %(ym_2506)s, %(ym_2507)s, %(ym_2508)s
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
stock_name = VALUES(stock_name),
|
||||
ym_2410 = VALUES(ym_2410),
|
||||
ym_2411 = VALUES(ym_2411),
|
||||
ym_2412 = VALUES(ym_2412),
|
||||
ym_2501 = VALUES(ym_2501),
|
||||
ym_2502 = VALUES(ym_2502),
|
||||
ym_2503 = VALUES(ym_2503),
|
||||
ym_2504 = VALUES(ym_2504),
|
||||
ym_2505 = VALUES(ym_2505),
|
||||
ym_2506 = VALUES(ym_2506),
|
||||
ym_2507 = VALUES(ym_2507),
|
||||
ym_2508 = VALUES(ym_2508),
|
||||
update_time = CURRENT_TIMESTAMP
|
||||
"""
|
||||
db.execute_update(upsert_sql, monthly_data)
|
||||
|
||||
logging.info("月度均值计算和保存完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"计算和保存月度均值失败: {str(e)}")
|
||||
return False
|
||||
|
||||
# 安全转换函数
|
||||
def safe_float(v) -> Optional[float]:
|
||||
"""安全转换为float,处理N/A和空值"""
|
||||
try:
|
||||
return float(v) if pd.notna(v) and str(v).upper() != 'N/A' else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def safe_int(v) -> Optional[int]:
|
||||
"""安全转换为int,处理N/A和空值"""
|
||||
try:
|
||||
return int(v) if pd.notna(v) and str(v).upper() != 'N/A' else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def safe_parse_date(date_str, date_format='%Y-%m-%d'):
|
||||
"""
|
||||
安全解析日期字符串
|
||||
:param date_str: 日期字符串
|
||||
:param date_format: 日期格式
|
||||
:return: 解析后的datetime对象或None
|
||||
"""
|
||||
if not date_str or pd.isna(date_str) or str(date_str).strip() == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(str(date_str), date_format)
|
||||
except ValueError:
|
||||
logging.warning(f"无法解析日期字符串: {date_str}")
|
||||
return None
|
||||
|
||||
def validate_market_data(dataset: list) -> list:
|
||||
"""
|
||||
验证市场数据有效性
|
||||
|
||||
Args:
|
||||
dataset (list): 原始数据集
|
||||
|
||||
Returns:
|
||||
list: 通过验证的数据集
|
||||
"""
|
||||
validated_data = []
|
||||
for item in dataset:
|
||||
try:
|
||||
# 必要字段检查
|
||||
if not item.get('code') or not item.get('name'):
|
||||
logging.warning(f"跳过无效数据: 缺少必要字段 code或name")
|
||||
continue
|
||||
|
||||
# 筛选股票名称
|
||||
if item.get('name')[-1] == 'R':
|
||||
continue
|
||||
|
||||
# 数值范围验证
|
||||
if item.get('lot_size') is not None and item['lot_size'] < 0:
|
||||
logging.warning(f"股票 {item['code']} 的lot_size为负值: {item['lot_size']}")
|
||||
item['lot_size'] = None
|
||||
|
||||
validated_data.append(item)
|
||||
except Exception as e:
|
||||
logging.warning(f"数据验证失败,跳过记录 {item.get('code')}: {str(e)}")
|
||||
continue
|
||||
|
||||
return validated_data
|
||||
|
||||
def get_market_data(market: Market) -> List[str]:
|
||||
"""
|
||||
从Futu API获取指定市场的股票代码列表
|
||||
|
||||
Args:
|
||||
market (Market): 市场枚举值,如 Market.SH, Market.SZ
|
||||
|
||||
Returns:
|
||||
List[str]: 股票代码列表
|
||||
"""
|
||||
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||
try:
|
||||
ret, data = quote_ctx.get_stock_basicinfo(market, SecurityType.STOCK)
|
||||
if ret == RET_OK:
|
||||
# 提取code列并转换为列表
|
||||
codes = data['code'].astype(str).tolist()
|
||||
logging.info(f"获取到 {market} 市场 {len(codes)} 个股票代码")
|
||||
return codes
|
||||
else:
|
||||
logging.error(f"获取股票代码失败: {data}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码时发生异常: {str(e)}")
|
||||
return []
|
||||
finally:
|
||||
quote_ctx.close()
|
||||
|
||||
def get_stock_codes() -> List[str]:
|
||||
"""从conditionalselection表获取所有股票代码"""
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
sql = f"SELECT DISTINCT stock_code,stock_name FROM conditionalselection"
|
||||
results = db.execute_query(sql)
|
||||
return [
|
||||
row['stock_code']
|
||||
for row in results
|
||||
if row['stock_code'] and (row.get('stock_name', '') and row['stock_name'][-1] != 'R') # 排除 name 以 R 结尾的票
|
||||
]
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码失败: {str(e)}")
|
||||
return []
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'klinedata_1d_hk'
|
||||
}
|
||||
|
||||
# market_data = get_market_data(Market.HK)
|
||||
market_data = get_stock_codes() # 使用按照价格和流通股数量筛选的那个表格
|
||||
|
||||
for code in market_data:
|
||||
|
||||
tablename = 'hk_' + code[3:]
|
||||
# 计算并保存月度均值
|
||||
success = calculate_and_save_monthly_avg(
|
||||
db_config=db_config,
|
||||
source_table=tablename,
|
||||
target_table="hk_monthly_avg_2410_2508"
|
||||
)
|
||||
|
||||
if success:
|
||||
logging.info("月度均值处理成功完成")
|
||||
else:
|
||||
logging.error("处理过程中出现错误")
|
||||
@@ -1,16 +1,17 @@
|
||||
"""
|
||||
#
|
||||
# 使用筛选器来更新股票列表
|
||||
# 使用筛选器来更新股票列表 <临时使用,功能是完整的,后面继续优化>
|
||||
#
|
||||
"""
|
||||
|
||||
from futu import *
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from MySQLHelper import MySQLHelper # 假设您已有MySQLHelper类
|
||||
from LogHelper import LogHelper
|
||||
|
||||
# 基本用法(自动创建日期日志+控制台输出)
|
||||
logger = LogHelper().setup()
|
||||
logger = LogHelper(logger_name = 'floatShare').setup()
|
||||
|
||||
class FutuStockFilter:
|
||||
def __init__(self, db_config:dict):
|
||||
@@ -24,7 +25,7 @@ class FutuStockFilter:
|
||||
self.quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"连接Futu API失败: {str(e)}")
|
||||
logger.error(f"连接Futu API失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
@@ -48,10 +49,10 @@ class FutuStockFilter:
|
||||
try:
|
||||
with MySQLHelper(**self.db_config) as db:
|
||||
db.execute_update(create_table_sql)
|
||||
logging.info("数据库表准备就绪")
|
||||
logger.info("数据库表准备就绪")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"准备数据库表失败: {str(e)}")
|
||||
logger.error(f"准备数据库表失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def process_filter_result(self, ret_list: List) -> List[Dict]:
|
||||
@@ -66,7 +67,7 @@ class FutuStockFilter:
|
||||
}
|
||||
processed_data.append(data)
|
||||
except Exception as e:
|
||||
logging.error(f"处理股票 {getattr(item, 'stock_code', '未知')} 数据失败: {str(e)}")
|
||||
logger.error(f"处理股票 {getattr(item, 'stock_code', '未知')} 数据失败: {str(e)}")
|
||||
|
||||
return processed_data
|
||||
|
||||
@@ -98,10 +99,10 @@ class FutuStockFilter:
|
||||
params.append(tuple(item.get(col) for col in columns))
|
||||
|
||||
affected_rows = db.execute_many(sql, params)
|
||||
logging.info(f"批量保存了 {len(data_batch)} 条记录,影响 {affected_rows} 行")
|
||||
logger.info(f"批量保存了 {len(data_batch)} 条记录,影响 {affected_rows} 行")
|
||||
return affected_rows
|
||||
except Exception as e:
|
||||
logging.error(f"批量保存失败: {str(e)}")
|
||||
logger.error(f"批量保存失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
def run_direct_import(self):
|
||||
@@ -113,7 +114,7 @@ class FutuStockFilter:
|
||||
self.disconnect()
|
||||
return False
|
||||
|
||||
logging.info(f"================= start ===================")
|
||||
logger.info(f"================= start ===================")
|
||||
simple_filter = SimpleFilter()
|
||||
simple_filter.filter_min = 0
|
||||
simple_filter.filter_max = 100000000000000
|
||||
@@ -134,7 +135,7 @@ class FutuStockFilter:
|
||||
|
||||
if ret == RET_OK:
|
||||
last_page, all_count, ret_list = ls
|
||||
logging.info(f'获取股票列表进度: {nBegin}/{all_count}')
|
||||
logger.info(f'获取股票列表进度: {nBegin}/{all_count}')
|
||||
|
||||
# 处理并保存当前批次数据
|
||||
processed_data = self.process_filter_result(ret_list)
|
||||
@@ -144,17 +145,17 @@ class FutuStockFilter:
|
||||
|
||||
nBegin += len(ret_list)
|
||||
else:
|
||||
logging.error(f'获取股票列表错误: {ls}')
|
||||
logger.error(f'获取股票列表错误: {ls}')
|
||||
break
|
||||
|
||||
time.sleep(3) # 避免触发限频
|
||||
|
||||
logging.info(f"导入完成! 共导入 {total_imported} 条记录")
|
||||
logging.info(f"================= end ===================")
|
||||
logger.info(f"导入完成! 共导入 {total_imported} 条记录")
|
||||
logger.info(f"================= end ===================")
|
||||
return total_imported > 0
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"导入过程中发生错误: {str(e)}")
|
||||
logger.error(f"导入过程中发生错误: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
self.disconnect()
|
||||
@@ -1,315 +0,0 @@
|
||||
import csv
|
||||
import pandas as pd
|
||||
from MySQLHelper import MySQLHelper
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
def get_monthly_avg_data(db_config: dict, table_name: str) -> Optional[List[Dict]]:
|
||||
"""
|
||||
从数据库读取月度均值数据
|
||||
|
||||
Args:
|
||||
db_config: 数据库配置
|
||||
table_name: 源数据表名
|
||||
|
||||
Returns:
|
||||
List[Dict]: 查询结果数据集,失败返回None
|
||||
"""
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
# 获取表结构信息
|
||||
columns = db.execute_query(f"""
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
|
||||
ORDER BY ORDINAL_POSITION
|
||||
""", (db_config['database'], table_name))
|
||||
|
||||
if not columns:
|
||||
logging.error(f"表 {table_name} 不存在或没有列")
|
||||
return None
|
||||
|
||||
# 获取列名列表(排除id和update_time)
|
||||
field_names = [col['COLUMN_NAME'] for col in columns
|
||||
if col['COLUMN_NAME'] not in ('id', 'update_time')]
|
||||
|
||||
# 查询数据
|
||||
data = db.execute_query(f"""
|
||||
SELECT {', '.join(field_names)}
|
||||
FROM {table_name}
|
||||
ORDER BY stock_code
|
||||
""")
|
||||
|
||||
if not data:
|
||||
logging.error(f"表 {table_name} 中没有数据")
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"从数据库读取月度均值数据失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_float_share_data(db_config: dict, table_name: str) -> Optional[List[Dict]]:
|
||||
"""
|
||||
从conditionalselection表读取流通股本数据
|
||||
|
||||
Args:
|
||||
db_config: 数据库配置
|
||||
table_name: 源数据表名
|
||||
|
||||
Returns:
|
||||
List[Dict]: 查询结果数据集,失败返回None
|
||||
"""
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
# 查询流通股本数据
|
||||
data = db.execute_query(f"""
|
||||
SELECT stock_code, stock_name, float_share
|
||||
FROM {table_name}
|
||||
ORDER BY stock_code
|
||||
""")
|
||||
|
||||
if not data:
|
||||
logging.error(f"表 {table_name} 中没有流通股本数据")
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"从数据库读取流通股本数据失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def merge_data(monthly_data: List[Dict], float_share_data: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
合并月度均价数据和流通股本数据
|
||||
|
||||
Args:
|
||||
monthly_data: 月度均价数据
|
||||
float_share_data: 流通股本数据
|
||||
|
||||
Returns:
|
||||
List[Dict]: 合并后的数据集
|
||||
"""
|
||||
merged_data = []
|
||||
float_share_dict = {item['stock_code']: item['float_share'] for item in float_share_data}
|
||||
|
||||
for item in monthly_data:
|
||||
merged_item = item.copy()
|
||||
merged_item['float_share'] = float_share_dict.get(item['stock_code'], 'N/A')
|
||||
merged_data.append(merged_item)
|
||||
|
||||
return merged_data
|
||||
|
||||
def export_to_csv(data: List[Dict], output_file: str) -> bool:
|
||||
"""
|
||||
将合并后的数据导出到CSV文件
|
||||
|
||||
Args:
|
||||
data: 要导出的数据集
|
||||
output_file: 输出的CSV文件路径
|
||||
|
||||
Returns:
|
||||
bool: 是否导出成功
|
||||
"""
|
||||
if not data:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 获取字段名(使用第一个数据的键)
|
||||
field_names = list(data[0].keys())
|
||||
|
||||
# 字段名到中文的映射
|
||||
header_map = {
|
||||
'stock_code': '股票代码',
|
||||
'stock_name': '股票名称',
|
||||
'float_share': '流通股本(千股)',
|
||||
'ym_2410': '2024年10月均收盘价',
|
||||
'ym_2411': '2024年11月均收盘价',
|
||||
'ym_2412': '2024年12月均收盘价',
|
||||
'ym_2501': '2025年1月均收盘价',
|
||||
'ym_2502': '2025年2月均收盘价',
|
||||
'ym_2503': '2025年3月均收盘价',
|
||||
'ym_2504': '2025年4月均收盘价',
|
||||
'ym_2505': '2025年5月均收盘价',
|
||||
'ym_2506': '2025年6月均收盘价',
|
||||
'ym_2507': '2025年7月均收盘价',
|
||||
'ym_2508': '2025年8月均收盘价'
|
||||
}
|
||||
|
||||
with open(output_file, mode='w', newline='', encoding='utf-8-sig') as csvfile:
|
||||
writer = csv.DictWriter(csvfile, fieldnames=field_names)
|
||||
|
||||
# 写入中文表头
|
||||
writer.writerow({col: header_map.get(col, col) for col in field_names})
|
||||
|
||||
# 写入数据
|
||||
writer.writerows(data)
|
||||
|
||||
logging.info(f"成功导出 {len(data)} 条记录到CSV文件: {output_file}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"导出到CSV失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def export_to_excel(data: List[Dict], output_file: str) -> bool:
|
||||
"""
|
||||
将合并后的数据导出为Excel文件(包含多个工作表)
|
||||
|
||||
Args:
|
||||
data: 要导出的数据集
|
||||
output_file: 输出的Excel文件路径
|
||||
|
||||
Returns:
|
||||
bool: 是否导出成功
|
||||
"""
|
||||
if not data:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 转换为DataFrame
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# 设置股票代码为索引
|
||||
if 'stock_code' in df.columns:
|
||||
df.set_index('stock_code', inplace=True)
|
||||
|
||||
# 创建Excel writer对象
|
||||
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
||||
# 1. 原始数据工作表
|
||||
df.to_excel(writer, sheet_name='合并数据')
|
||||
|
||||
# 2. 统计信息工作表(仅当有数值列时)
|
||||
numeric_cols = [col for col in df.columns if col.startswith('ym_') and pd.api.types.is_numeric_dtype(df[col])]
|
||||
|
||||
if numeric_cols:
|
||||
try:
|
||||
stats = df[numeric_cols].describe().loc[['mean', 'min', 'max', 'std']]
|
||||
stats.to_excel(writer, sheet_name='统计信息')
|
||||
except KeyError:
|
||||
logging.warning("无法生成完整的统计信息,数据可能不足")
|
||||
# 生成简化版统计信息
|
||||
stats = df[numeric_cols].agg(['mean', 'min', 'max', 'std'])
|
||||
stats.to_excel(writer, sheet_name='统计信息')
|
||||
|
||||
# 3. 涨幅排名工作表(需要至少两个月份数据)
|
||||
if len(numeric_cols) >= 2:
|
||||
first_month = numeric_cols[0]
|
||||
last_month = numeric_cols[-1]
|
||||
|
||||
try:
|
||||
df['涨幅(%)'] = (df[last_month] - df[first_month]) / df[first_month] * 100
|
||||
result_df = df[['stock_name', '涨幅(%)', 'float_share']].copy()
|
||||
result_df.dropna(subset=['涨幅(%)'], inplace=True)
|
||||
result_df.sort_values('涨幅(%)', ascending=False, inplace=True)
|
||||
result_df.to_excel(writer, sheet_name='涨幅排名')
|
||||
except Exception as e:
|
||||
logging.warning(f"无法计算涨幅: {str(e)}")
|
||||
|
||||
# 4. 月度趋势工作表
|
||||
if numeric_cols:
|
||||
try:
|
||||
trend_df = df[numeric_cols].transpose()
|
||||
trend_df.index = [col.replace('ym_', '') for col in numeric_cols]
|
||||
trend_df.to_excel(writer, sheet_name='月度趋势')
|
||||
except Exception as e:
|
||||
logging.warning(f"无法生成月度趋势: {str(e)}")
|
||||
|
||||
# 5. 流通股本分析工作表
|
||||
if 'float_share' in df.columns and pd.api.types.is_numeric_dtype(df['float_share']):
|
||||
try:
|
||||
float_stats = df['float_share'].describe().to_frame().T
|
||||
float_stats.to_excel(writer, sheet_name='流通股本分析')
|
||||
except Exception as e:
|
||||
logging.warning(f"无法生成流通股本分析: {str(e)}")
|
||||
|
||||
logging.info(f"成功导出Excel文件: {output_file}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"导出Excel失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def export_combined_data(db_config: dict,
|
||||
monthly_table: str,
|
||||
float_share_table: str,
|
||||
csv_file: str = None,
|
||||
excel_file: str = None) -> bool:
|
||||
"""
|
||||
导出合并后的数据到CSV和/或Excel
|
||||
|
||||
Args:
|
||||
db_config: 数据库配置
|
||||
monthly_table: 月度均价表名
|
||||
float_share_table: 流通股本表名
|
||||
csv_file: CSV输出路径(可选)
|
||||
excel_file: Excel输出路径(可选)
|
||||
|
||||
Returns:
|
||||
bool: 是否至少有一种格式导出成功
|
||||
"""
|
||||
if not csv_file and not excel_file:
|
||||
logging.error("必须指定至少一种输出格式")
|
||||
return False
|
||||
|
||||
# 从数据库获取数据
|
||||
monthly_data = get_monthly_avg_data(db_config, monthly_table)
|
||||
if not monthly_data:
|
||||
logging.error("无法获取月度均价数据")
|
||||
return False
|
||||
|
||||
float_share_data = get_float_share_data(db_config, float_share_table)
|
||||
if not float_share_data:
|
||||
logging.error("无法获取流通股本数据")
|
||||
return False
|
||||
|
||||
# 合并数据
|
||||
merged_data = merge_data(monthly_data, float_share_data)
|
||||
|
||||
# 导出结果
|
||||
csv_success = True
|
||||
excel_success = True
|
||||
|
||||
if csv_file:
|
||||
csv_success = export_to_csv(merged_data, csv_file)
|
||||
|
||||
if excel_file:
|
||||
excel_success = export_to_excel(merged_data, excel_file)
|
||||
|
||||
return csv_success or excel_success
|
||||
|
||||
# 主程序入口
|
||||
if __name__ == "__main__":
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('export_combined_data.log', encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'klinedata_1d_hk_akshare'
|
||||
}
|
||||
|
||||
# 导出合并数据
|
||||
success = export_combined_data(
|
||||
db_config=db_config,
|
||||
monthly_table="hk_monthly_avg_2410_2508",
|
||||
float_share_table="conditionalselection",
|
||||
csv_file="hk_stocks_combined_data.csv",
|
||||
excel_file="hk_stocks_combined_data.xlsx"
|
||||
)
|
||||
|
||||
if success:
|
||||
logging.info("数据合并导出成功完成")
|
||||
else:
|
||||
logging.error("数据合并导出过程中出现错误")
|
||||
@@ -1,47 +1,9 @@
|
||||
# import logging
|
||||
# import sys
|
||||
|
||||
# class LogHelper:
|
||||
# def __init__(self,
|
||||
# level=logging.INFO,
|
||||
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
# handlers=None):
|
||||
# """
|
||||
# 初始化日志配置
|
||||
|
||||
# :param level: 日志级别,默认为 logging.INFO
|
||||
# :param format: 日志格式字符串
|
||||
# :param handlers: 日志处理器列表,默认为空(会自动添加控制台处理器)
|
||||
# """
|
||||
# self.level = level
|
||||
# self.format = format
|
||||
# self.handlers = handlers if handlers is not None else []
|
||||
|
||||
# def add_console_handler(self, stream=sys.stdout, encoding='utf-8'):
|
||||
# """添加控制台处理器"""
|
||||
# console_handler = logging.StreamHandler(stream)
|
||||
# console_handler.setFormatter(logging.Formatter(self.format))
|
||||
# # 设置控制台编码
|
||||
# if encoding:
|
||||
# console_handler.encoding = encoding
|
||||
# self.handlers.append(console_handler)
|
||||
|
||||
# def add_file_handler(self, filename, encoding='utf-8'):
|
||||
# """添加文件处理器(解决中文乱码问题)"""
|
||||
# # 使用支持UTF-8编码的文件处理器
|
||||
# filepath = "log/" + filename
|
||||
# file_handler = logging.FileHandler(filepath, encoding=encoding)
|
||||
# file_handler.setFormatter(logging.Formatter(self.format))
|
||||
# self.handlers.append(file_handler)
|
||||
|
||||
# def setup(self):
|
||||
# """应用日志配置"""
|
||||
# logging.basicConfig(
|
||||
# level=self.level,
|
||||
# format=self.format,
|
||||
# handlers=self.handlers
|
||||
# )
|
||||
"""
|
||||
日志管理类
|
||||
|
||||
日志输出到运行目录指定文件夹
|
||||
日志文件按照:YYYY-MM-dd.log 格式输出
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
@@ -49,11 +11,21 @@ import os
|
||||
from datetime import datetime
|
||||
|
||||
class LogHelper:
|
||||
# 内部日志级别映射表
|
||||
_LEVEL_MAP = {
|
||||
'DEBUG': 10,
|
||||
'INFO': 20,
|
||||
'WARNING': 30,
|
||||
'ERROR': 40,
|
||||
'CRITICAL': 50
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
level=logging.INFO,
|
||||
level='INFO',
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=None,
|
||||
log_dir="logs"):
|
||||
log_dir="logs",
|
||||
logger_name=None):
|
||||
"""
|
||||
初始化日志配置
|
||||
|
||||
@@ -62,10 +34,11 @@ class LogHelper:
|
||||
:param handlers: 日志处理器列表,默认为空(会自动添加控制台处理器)
|
||||
:param log_dir: 日志存储目录,默认为"logs"
|
||||
"""
|
||||
self.level = level
|
||||
self.level = self._LEVEL_MAP.get(level.upper(), 20) # 默认INFO
|
||||
self.format = format
|
||||
self.handlers = handlers if handlers is not None else []
|
||||
self.log_dir = log_dir
|
||||
self.logger_name = logger_name
|
||||
# 确保日志目录存在
|
||||
os.makedirs(self.log_dir, exist_ok=True)
|
||||
|
||||
@@ -76,6 +49,7 @@ class LogHelper:
|
||||
if encoding:
|
||||
console_handler.encoding = encoding
|
||||
self.handlers.append(console_handler)
|
||||
return self # 支持链式调用
|
||||
|
||||
def _get_daily_log_path(self):
|
||||
"""生成基于当前日期的日志文件路径"""
|
||||
@@ -89,6 +63,7 @@ class LogHelper:
|
||||
file_handler = logging.FileHandler(log_path, encoding=encoding)
|
||||
file_handler.setFormatter(logging.Formatter(self.format))
|
||||
self.handlers.append(file_handler)
|
||||
return self # 支持链式调用
|
||||
|
||||
def setup(self):
|
||||
"""应用日志配置(自动添加日期文件处理器)"""
|
||||
@@ -97,9 +72,19 @@ class LogHelper:
|
||||
self.add_console_handler()
|
||||
self._add_daily_file_handler()
|
||||
|
||||
# 应用配置
|
||||
logging.basicConfig(
|
||||
level=self.level,
|
||||
format=self.format,
|
||||
handlers=self.handlers
|
||||
)
|
||||
# 获取或创建logger
|
||||
logger = logging.getLogger(self.logger_name)
|
||||
logger.setLevel(self.level)
|
||||
|
||||
# 移除所有现有处理器(避免重复添加)
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# 添加配置的处理器
|
||||
for handler in self.handlers:
|
||||
logger.addHandler(handler)
|
||||
|
||||
# 确保日志消息不会传递给父logger(避免重复记录)
|
||||
logger.propagate = False
|
||||
|
||||
return logger
|
||||
@@ -1,6 +1,25 @@
|
||||
"""
|
||||
MySqlHelper 增强版
|
||||
—— 增加事务管理
|
||||
—— 增加ID获取
|
||||
—— 增加表操作等使用功能
|
||||
"""
|
||||
import pymysql
|
||||
from pymysql import Error
|
||||
from typing import List, Dict, Union, Optional, Tuple
|
||||
from contextlib import contextmanager
|
||||
from LogHelper import LogHelper
|
||||
|
||||
# 基本用法(自动创建日期日志+控制台输出)
|
||||
logger = LogHelper(logger_name = 'database').setup()
|
||||
|
||||
# # 高级用法(自定义配置)
|
||||
# logger = LogHelper(
|
||||
# level=logging.DEBUG,
|
||||
# log_dir="databaselogs",
|
||||
# format='%(levelname)s - %(message)s'
|
||||
# ).setup()
|
||||
|
||||
|
||||
class MySQLHelper:
|
||||
def __init__(self, host: str, user: str, password: str, database: str,
|
||||
@@ -39,10 +58,10 @@ class MySQLHelper:
|
||||
cursorclass=pymysql.cursors.DictCursor # 返回字典形式的结果
|
||||
)
|
||||
self.cursor = self.connection.cursor()
|
||||
print("MySQL数据库连接成功")
|
||||
logger.info("MySQL数据库连接成功")
|
||||
return True
|
||||
except Error as e:
|
||||
print(f"连接MySQL数据库失败: {e}")
|
||||
logger.error(f"连接MySQL数据库失败: {e}")
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
@@ -53,7 +72,7 @@ class MySQLHelper:
|
||||
self.cursor.close()
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
print("MySQL数据库连接已关闭")
|
||||
logger.info("MySQL数据库连接已关闭")
|
||||
|
||||
def execute_query(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> List[Dict]:
|
||||
"""
|
||||
@@ -66,7 +85,7 @@ class MySQLHelper:
|
||||
self.cursor.execute(sql, params)
|
||||
return self.cursor.fetchall()
|
||||
except Error as e:
|
||||
print(f"查询执行失败: {e}")
|
||||
logger.error(f"查询执行失败: {e}")
|
||||
return []
|
||||
|
||||
def execute_update(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> int:
|
||||
@@ -82,7 +101,7 @@ class MySQLHelper:
|
||||
return affected_rows
|
||||
except Error as e:
|
||||
self.connection.rollback()
|
||||
print(f"更新执行失败: {e}")
|
||||
logger.error(f"更新执行失败: {e}")
|
||||
return 0
|
||||
|
||||
def execute_many(self, sql: str, params_list: List[Union[Tuple, List, Dict]]) -> int:
|
||||
@@ -98,9 +117,41 @@ class MySQLHelper:
|
||||
return affected_rows
|
||||
except Error as e:
|
||||
self.connection.rollback()
|
||||
print(f"批量执行失败: {e}")
|
||||
logger.error(f"批量执行失败: {e}")
|
||||
return 0
|
||||
|
||||
# ================== 新增功能方法 ==================
|
||||
def get_last_insert_id(self) -> int:
|
||||
"""
|
||||
获取最后插入行的自增ID
|
||||
:return: 自增ID值
|
||||
"""
|
||||
return self.cursor.lastrowid if self.cursor else 0
|
||||
|
||||
def execute_insert(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> int:
|
||||
"""
|
||||
执行插入操作并返回自增ID
|
||||
:return: 自增ID值
|
||||
"""
|
||||
self.execute_update(sql, params)
|
||||
return self.get_last_insert_id()
|
||||
|
||||
@contextmanager
|
||||
def transaction(self):
|
||||
"""
|
||||
事务上下文管理器,确保操作原子性
|
||||
用法:
|
||||
with db.transaction():
|
||||
db.execute_update(...)
|
||||
db.execute_many(...)
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
self.connection.commit()
|
||||
except Exception as e:
|
||||
self.connection.rollback()
|
||||
raise e
|
||||
|
||||
def get_one(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> Optional[Dict]:
|
||||
"""
|
||||
获取单条记录
|
||||
@@ -112,7 +163,7 @@ class MySQLHelper:
|
||||
self.cursor.execute(sql, params)
|
||||
return self.cursor.fetchone()
|
||||
except Error as e:
|
||||
print(f"获取单条记录失败: {e}")
|
||||
logger.error(f"获取单条记录失败: {e}")
|
||||
return None
|
||||
|
||||
def table_exists(self, table_name: str) -> bool:
|
||||
@@ -124,12 +175,61 @@ class MySQLHelper:
|
||||
sql = "SHOW TABLES LIKE %s"
|
||||
result = self.execute_query(sql, (table_name,))
|
||||
return len(result) > 0
|
||||
|
||||
def create_table(self, sql: str) -> bool:
|
||||
"""
|
||||
执行建表语句
|
||||
:param sql: CREATE TABLE语句
|
||||
:return: 是否成功
|
||||
"""
|
||||
try:
|
||||
self.cursor.execute(sql)
|
||||
self.connection.commit()
|
||||
return True
|
||||
except Error as e:
|
||||
logger.error(f"创建表失败: {e}")
|
||||
return False
|
||||
|
||||
def drop_table(self, table_name: str) -> bool:
|
||||
"""
|
||||
删除表
|
||||
:param table_name: 表名
|
||||
:return: 是否成功
|
||||
"""
|
||||
try:
|
||||
self.cursor.execute(f"DROP TABLE IF EXISTS {table_name}")
|
||||
self.connection.commit()
|
||||
return True
|
||||
except Error as e:
|
||||
logger.error(f"删除表失败: {e}")
|
||||
return False
|
||||
|
||||
def get_columns(self, table_name: str) -> List[Dict]:
|
||||
"""
|
||||
获取表的列信息
|
||||
:param table_name: 表名
|
||||
:return: 列信息字典列表
|
||||
"""
|
||||
return self.execute_query(f"DESCRIBE {table_name}")
|
||||
|
||||
|
||||
# ================== 事务控制方法 ==================
|
||||
def start_transaction(self):
|
||||
"""显式开始事务"""
|
||||
self.connection.begin()
|
||||
|
||||
def commit(self):
|
||||
"""提交事务"""
|
||||
self.connection.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""回滚事务"""
|
||||
self.connection.rollback()
|
||||
|
||||
# ================== 上下文管理器 ==================
|
||||
def __enter__(self):
|
||||
"""支持with上下文管理"""
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""支持with上下文管理"""
|
||||
self.close()
|
||||
@@ -1,34 +1,29 @@
|
||||
"""
|
||||
检查K线数据是否下载完成
|
||||
|
||||
检测表格中的股票是否都有对应的表格
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
from MySQLHelper import MySQLHelper # 假设您已有MySQLHelper类
|
||||
from LogHelper import LogHelper
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
# 基本用法(自动创建日期日志+控制台输出)
|
||||
logger = LogHelper(logger_name = 'checkTable').setup()
|
||||
|
||||
class StockTableChecker:
|
||||
def __init__(self, db_config: dict):
|
||||
self.db_config = db_config
|
||||
self.conditional_selection_table = "conditionalselection"
|
||||
self.stock_list_table = "stock_filter"
|
||||
|
||||
def get_stock_codes(self) -> List[str]:
|
||||
"""从conditionalselection表获取所有股票代码"""
|
||||
try:
|
||||
with MySQLHelper(**self.db_config) as db:
|
||||
sql = f"SELECT DISTINCT stock_code FROM {self.conditional_selection_table}"
|
||||
sql = f"SELECT DISTINCT stock_code FROM {self.stock_list_table}"
|
||||
results = db.execute_query(sql)
|
||||
return [row['stock_code'] for row in results if row['stock_code']]
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码失败: {str(e)}")
|
||||
logger.error(f"获取股票代码失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def check_tables_exist(self, stock_codes: List[str]) -> dict:
|
||||
@@ -57,18 +52,18 @@ class StockTableChecker:
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.error(f"检查表存在性失败: {str(e)}")
|
||||
logger.error(f"检查表存在性失败: {str(e)}")
|
||||
return {'exists': [], 'not_exists': []}
|
||||
|
||||
def write_missing_codes_to_txt(self, missing_codes: list, filename: str = "missing_tables.txt"):
|
||||
def write_missing_codes_to_txt(self, missing_codes: list, filename: str = "data\missing_tables.txt"):
|
||||
"""将缺失的股票代码写入TXT文件"""
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
for code in missing_codes:
|
||||
f.write(f"{code}\n")
|
||||
logging.info(f"已将 {len(missing_codes)} 个缺失表对应的股票代码写入 {filename}")
|
||||
logger.info(f"已将 {len(missing_codes)} 个缺失表对应的股票代码写入 {filename}")
|
||||
except Exception as e:
|
||||
logging.error(f"写入TXT文件失败: {str(e)}")
|
||||
logger.error(f"写入TXT文件失败: {str(e)}")
|
||||
|
||||
def read_missing_codes_basic(file_path='missing_tables.txt'):
|
||||
"""基础读取方法 - 按行读取所有内容"""
|
||||
@@ -88,15 +83,15 @@ class StockTableChecker:
|
||||
|
||||
def run_check(self):
|
||||
"""执行完整的检查流程"""
|
||||
logging.info("开始检查股票代码对应表...")
|
||||
logger.info("开始检查股票代码对应表...")
|
||||
|
||||
# 1. 获取所有股票代码
|
||||
stock_codes = self.get_stock_codes()
|
||||
if not stock_codes:
|
||||
logging.error("没有获取到任何股票代码")
|
||||
logger.error("没有获取到任何股票代码")
|
||||
return
|
||||
|
||||
logging.info(f"共获取到 {len(stock_codes)} 个股票代码")
|
||||
logger.info(f"共获取到 {len(stock_codes)} 个股票代码")
|
||||
|
||||
# 2. 检查表存在性
|
||||
check_result = self.check_tables_exist(stock_codes)
|
||||
@@ -104,20 +99,20 @@ class StockTableChecker:
|
||||
not_exists_count = len(check_result['not_exists'])
|
||||
|
||||
# 3. 输出结果
|
||||
logging.info("\n检查结果:")
|
||||
logging.info(f"存在的表数量: {exists_count}")
|
||||
logging.info(f"不存在的表数量: {not_exists_count}")
|
||||
logger.info("\n检查结果:")
|
||||
logger.info(f"存在的表数量: {exists_count}")
|
||||
logger.info(f"不存在的表数量: {not_exists_count}")
|
||||
|
||||
if not_exists_count > 0:
|
||||
logging.info("\n不存在的表对应的股票代码:")
|
||||
logger.info("\n不存在的表对应的股票代码:")
|
||||
for code in check_result['not_exists']:
|
||||
logging.info(code)
|
||||
logger.info(code)
|
||||
|
||||
# 4. 统计信息
|
||||
logging.info("\n统计摘要:")
|
||||
logging.info(f"总股票代码数: {len(stock_codes)}")
|
||||
logging.info(f"存在对应表的比例: {exists_count/len(stock_codes):.2%}")
|
||||
logging.info(f"缺失对应表的比例: {not_exists_count/len(stock_codes):.2%}")
|
||||
logger.info("\n统计摘要:")
|
||||
logger.info(f"总股票代码数: {len(stock_codes)}")
|
||||
logger.info(f"存在对应表的比例: {exists_count/len(stock_codes):.2%}")
|
||||
logger.info(f"缺失对应表的比例: {not_exists_count/len(stock_codes):.2%}")
|
||||
|
||||
# 4. 将缺失的股票代码写入TXT文件
|
||||
if check_result['not_exists']:
|
||||
@@ -131,7 +126,7 @@ if __name__ == "__main__":
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'klinedata_1d_hk'
|
||||
'database': 'hk_kline_1d'
|
||||
}
|
||||
|
||||
# 创建检查器并运行
|
||||
|
||||
@@ -10,18 +10,20 @@
|
||||
—— 根据股票代码,获取股票历史数据
|
||||
—— 根据股票代码,获取流通股数量
|
||||
—— 根据股票代码,新建/更新数据表
|
||||
—— 更新完成需检查 ,数据是否完整 -> checktable.py 操作界面做好了之后,再融合到整体代码中
|
||||
"""
|
||||
from futu import *
|
||||
from pymysql import Error
|
||||
from MySQLHelper import MySQLHelper # MySQLHelper类保存为单独文件
|
||||
from LogHelper import LogHelper
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Union, Tuple
|
||||
import time
|
||||
from typing import Optional, List, Dict
|
||||
from ConditionalSelection import FutuStockFilter
|
||||
from tqdm import tqdm
|
||||
import pandas as pd
|
||||
import time
|
||||
|
||||
# 基本用法(自动创建日期日志+控制台输出)
|
||||
logger = LogHelper(logger_name = 'KLine').setup()
|
||||
|
||||
def get_market_data(market: Market) -> List[str]:
|
||||
"""
|
||||
@@ -39,13 +41,13 @@ def get_market_data(market: Market) -> List[str]:
|
||||
if ret == RET_OK:
|
||||
# 提取code列并转换为列表
|
||||
codes = data['code'].astype(str).tolist()
|
||||
logging.info(f"获取到 {market} 市场 {len(codes)} 个股票代码")
|
||||
logger.info(f"获取到 {market} 市场 {len(codes)} 个股票代码")
|
||||
return codes
|
||||
else:
|
||||
logging.error(f"获取股票代码失败: {data}")
|
||||
logger.error(f"获取股票代码失败: {data}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码时发生异常: {str(e)}")
|
||||
logger.error(f"获取股票代码时发生异常: {str(e)}")
|
||||
return []
|
||||
finally:
|
||||
quote_ctx.close()
|
||||
@@ -89,7 +91,7 @@ def preprocess_quote_data(df: pd.DataFrame, float_share: Optional[int] = None) -
|
||||
}
|
||||
processed_data.append(item)
|
||||
except Exception as e:
|
||||
logging.warning(f"处理行情数据时跳过异常行 {row.get('code', '未知')}: {str(e)}")
|
||||
logger.warning(f"处理行情数据时跳过异常行 {row.get('code', '未知')}: {str(e)}")
|
||||
continue
|
||||
|
||||
return processed_data
|
||||
@@ -115,7 +117,7 @@ def get_float_share(quote_ctx: OpenQuoteContext, code: str) -> Optional[int]:
|
||||
return int(float_share)
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票 {code} 的流通股数量失败: {str(e)}")
|
||||
logger.error(f"获取股票 {code} 的流通股数量失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_float_share_data(db_config: dict, table_name: str) -> Optional[Dict[str, int]]:
|
||||
@@ -139,7 +141,7 @@ def get_float_share_data(db_config: dict, table_name: str) -> Optional[Dict[str,
|
||||
""")
|
||||
|
||||
if not data:
|
||||
logging.error(f"表 {table_name} 中没有流通股本数据")
|
||||
logger.error(f"表 {table_name} 中没有流通股本数据")
|
||||
return None
|
||||
|
||||
# 构建股票代码到流通股数量的映射字典
|
||||
@@ -150,11 +152,11 @@ def get_float_share_data(db_config: dict, table_name: str) -> Optional[Dict[str,
|
||||
if stock_code and float_share is not None:
|
||||
float_share_dict[stock_code] = int(float_share)
|
||||
|
||||
logging.info(f"成功从 {table_name} 表加载 {len(float_share_dict)} 条流通股数据")
|
||||
logger.info(f"成功从 {table_name} 表加载 {len(float_share_dict)} 条流通股数据")
|
||||
return float_share_dict
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"从数据库读取流通股本数据失败: {str(e)}")
|
||||
logger.error(f"从数据库读取流通股本数据失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def save_quotes_to_db(db_config: dict, quote_data: pd.DataFrame, table_name: str = 'stock_quotes', float_share: Optional[int] = None) -> bool:
|
||||
@@ -173,7 +175,7 @@ def save_quotes_to_db(db_config: dict, quote_data: pd.DataFrame, table_name: str
|
||||
# 预处理数据
|
||||
processed_data = preprocess_quote_data(quote_data, float_share)
|
||||
if not processed_data:
|
||||
logging.error("没有有效数据需要保存")
|
||||
logger.error("没有有效数据需要保存")
|
||||
return False
|
||||
|
||||
# 动态生成SQL插入语句
|
||||
@@ -230,13 +232,13 @@ def save_quotes_to_db(db_config: dict, quote_data: pd.DataFrame, table_name: str
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票行情数据表'
|
||||
"""
|
||||
db.execute_update(create_table_sql)
|
||||
logging.info(f"创建了新表: {table_name}")
|
||||
logger.info(f"创建了新表: {table_name}")
|
||||
|
||||
affected_rows = db.execute_many(insert_sql, processed_data)
|
||||
logging.info(f"成功插入/更新 {affected_rows} 条行情记录到表 {table_name}")
|
||||
logger.info(f"成功插入/更新 {affected_rows} 条行情记录到表 {table_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"保存行情数据到表 {table_name} 失败: {str(e)}")
|
||||
logger.error(f"保存行情数据到表 {table_name} 失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def read_single_account_stock_codes(file_path='data\missing_tables_小航富途.txt'):
|
||||
@@ -262,22 +264,28 @@ def get_stock_codes() -> List[str]:
|
||||
results = db.execute_query(sql)
|
||||
return [row['stock_code'] for row in results if row['stock_code']]
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码失败: {str(e)}")
|
||||
logger.error(f"获取股票代码失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def write_missing_codes_to_txt(missing_codes: list, filename: str = "config\HK_futu.txt"):
|
||||
"""将缺失的股票代码追加到TXT文件,如果文件不存在则创建"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
directory = os.path.dirname(filename)
|
||||
if directory and not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
logger.info(f"创建目录: {directory}")
|
||||
|
||||
# # 检查文件是否存在
|
||||
# file_exists = os.path.exists(filename)
|
||||
with open(filename, 'a', encoding='utf-8') as f:
|
||||
for code in missing_codes:
|
||||
f.write(f"\n{code}")
|
||||
logger.info(f"已将 {len(missing_codes)} 个缺失表对应的股票代码写入 {filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"写入TXT文件失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 基本用法(自动创建日期日志+控制台输出)
|
||||
logger = LogHelper().setup()
|
||||
|
||||
# # 高级用法(自定义配置)
|
||||
# logger = LogHelper(
|
||||
# level=logging.DEBUG,
|
||||
# log_dir="my_logs",
|
||||
# format='%(levelname)s - %(message)s'
|
||||
# ).setup()
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
@@ -286,19 +294,25 @@ if __name__ == "__main__":
|
||||
'database': 'hk_kline_1d'
|
||||
}
|
||||
|
||||
# 创建导入器并运行, 用于每日更新流通股数量
|
||||
# 创建导入器并运行, 用于每日更新流通股数量 -> 操作界面中增加一个开关,每天只需要更新一次
|
||||
futuStockFilter = FutuStockFilter(db_config)
|
||||
futuStockFilter.run_direct_import()
|
||||
|
||||
# 每个账号获取的数据独立开来
|
||||
# 每个账号获取的数据独立开来 -> 操作见面可以选择
|
||||
market_data_all = get_stock_codes()
|
||||
market_data_hang = read_single_account_stock_codes('config\hang_futu.txt')
|
||||
market_data_kevin= read_single_account_stock_codes('config\kevin_futu.txt')
|
||||
|
||||
market_data = list(set(market_data_all) - set(market_data_hang) - set(market_data_kevin))
|
||||
market_data_HK= read_single_account_stock_codes('config\HK_futu.txt')
|
||||
market_data_new = list(set(market_data_all) - set(market_data_hang) - set(market_data_kevin) - set(market_data_HK))
|
||||
|
||||
# 每天收盘后更新数据
|
||||
start_date = (datetime.now() - timedelta(days = 3 * 365)).strftime("%Y-%m-%d")
|
||||
# write_missing_codes_to_txt(market_data_new) # 新股票添加到文件中 -> 暂时手动设置
|
||||
|
||||
# 动态调整
|
||||
"""*********************************************"""
|
||||
market_data = market_data_new
|
||||
|
||||
# 每天收盘后更新数据 -> 操作界面中,这个参数需要放出来
|
||||
start_date = (datetime.now() - timedelta(days = 5)).strftime("%Y-%m-%d")
|
||||
end_date = (datetime.now() + timedelta(days = 1)).strftime("%Y-%m-%d")
|
||||
|
||||
# 获取流通股数据字典
|
||||
@@ -319,7 +333,7 @@ if __name__ == "__main__":
|
||||
if float_share is not None:
|
||||
logger.info(f"从API获取股票 {code} 的流通股数量: {float_share}")
|
||||
else:
|
||||
logging.warning(f"无法获取股票 {code} 的流通股数量")
|
||||
logger.warning(f"无法获取股票 {code} 的流通股数量")
|
||||
|
||||
# 获取历史K线数据
|
||||
ret, data, page_req_key = quote_ctx.request_history_kline(code, start=start_date, end=end_date, max_count=100)
|
||||
@@ -341,9 +355,9 @@ if __name__ == "__main__":
|
||||
else:
|
||||
logger.error(f'error:{ data}')
|
||||
|
||||
time.sleep(1)
|
||||
time.sleep(0.7)
|
||||
|
||||
if isWhile == False:
|
||||
time.sleep(1)
|
||||
time.sleep(0.7)
|
||||
|
||||
quote_ctx.close() # 结束后记得关闭当条连接,防止连接条数用尽
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
"""
|
||||
该代码用于更新日 K 数据
|
||||
"""
|
||||
from futu import *
|
||||
from pymysql import Error
|
||||
from MySQLHelper import MySQLHelper # MySQLHelper类保存为单独文件
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Union, Tuple
|
||||
import time
|
||||
import akshare as ak
|
||||
|
||||
def get_market_data(market: Market) -> List[str]:
|
||||
"""
|
||||
从Futu API获取指定市场的股票代码列表
|
||||
|
||||
Args:
|
||||
market (Market): 市场枚举值,如 Market.SH, Market.SZ
|
||||
|
||||
Returns:
|
||||
List[str]: 股票代码列表
|
||||
"""
|
||||
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||
try:
|
||||
ret, data = quote_ctx.get_stock_basicinfo(market, SecurityType.STOCK)
|
||||
if ret == RET_OK:
|
||||
# 提取code列并转换为列表
|
||||
codes = data['code'].astype(str).tolist()
|
||||
logging.info(f"获取到 {market} 市场 {len(codes)} 个股票代码")
|
||||
return codes
|
||||
else:
|
||||
logging.error(f"获取股票代码失败: {data}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码时发生异常: {str(e)}")
|
||||
return []
|
||||
finally:
|
||||
quote_ctx.close()
|
||||
|
||||
def preprocess_quote_data(df: pd.DataFrame) -> List[Dict]:
|
||||
"""
|
||||
预处理行情数据,转换为适合数据库存储的格式
|
||||
|
||||
Args:
|
||||
df (pd.DataFrame): 原始行情数据DataFrame
|
||||
|
||||
Returns:
|
||||
List[Dict]: 处理后的数据列表
|
||||
"""
|
||||
processed_data = []
|
||||
for _, row in df.iterrows():
|
||||
try:
|
||||
# 提取市场标识
|
||||
# market = row['code'].split('.')[0] if '.' in row['code'] else 'UNKNOWN'
|
||||
|
||||
# 转换时间格式
|
||||
# trade_time = datetime.strptime(row['date'], '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
item = {
|
||||
'trade_date': str(row['date']),
|
||||
'open_price': float(row['open']),
|
||||
'close_price': float(row['close']),
|
||||
'high_price': float(row['high']),
|
||||
'low_price': float(row['low']),
|
||||
'volume': int(row['volume']) if pd.notna(row['volume']) else None,
|
||||
}
|
||||
processed_data.append(item)
|
||||
except Exception as e:
|
||||
logging.warning(f"处理行情数据时跳过异常行 {row.get('code', '未知')}: {str(e)}")
|
||||
continue
|
||||
|
||||
return processed_data
|
||||
|
||||
def save_quotes_to_db(db_config: dict, quote_data: pd.DataFrame, table_name: str = 'stock_quotes') -> bool:
|
||||
"""
|
||||
将行情数据保存到数据库
|
||||
|
||||
Args:
|
||||
db_config (dict): 数据库配置
|
||||
quote_data (pd.DataFrame): 行情数据DataFrame
|
||||
table_name (str): 目标表名(默认为'stock_quotes')
|
||||
|
||||
Returns:
|
||||
bool: 是否成功保存
|
||||
"""
|
||||
# 预处理数据
|
||||
processed_data = preprocess_quote_data(quote_data)
|
||||
if not processed_data:
|
||||
logging.error("没有有效数据需要保存")
|
||||
return False
|
||||
|
||||
# 动态生成SQL插入语句
|
||||
insert_sql = f"""
|
||||
INSERT INTO {table_name} (
|
||||
trade_date, open_price, close_price, high_price, low_price, volume
|
||||
) VALUES (
|
||||
%(trade_date)s, %(open_price)s, %(close_price)s, %(high_price)s, %(low_price)s, %(volume)s
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
open_price = VALUES(open_price),
|
||||
close_price = VALUES(close_price),
|
||||
high_price = VALUES(high_price),
|
||||
low_price = VALUES(low_price),
|
||||
volume = VALUES(volume)
|
||||
"""
|
||||
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
# 检查表是否存在,不存在则创建
|
||||
if not db.table_exists(table_name):
|
||||
create_table_sql = f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_name} (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
trade_date DATETIME NOT NULL COMMENT '交易日期时间',
|
||||
open_price DECIMAL(10, 3) COMMENT '开盘价',
|
||||
close_price DECIMAL(10, 3) COMMENT '收盘价',
|
||||
high_price DECIMAL(10, 3) COMMENT '最高价',
|
||||
low_price DECIMAL(10, 3) COMMENT '最低价',
|
||||
volume BIGINT COMMENT '成交量(股)',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE KEY idx_trade_date (trade_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='股票行情数据表'
|
||||
"""
|
||||
db.execute_update(create_table_sql)
|
||||
logging.info(f"创建了新表: {table_name}")
|
||||
|
||||
affected_rows = db.execute_many(insert_sql, processed_data)
|
||||
logging.info(f"成功插入/更新 {affected_rows} 条行情记录到表 {table_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"保存行情数据到表 {table_name} 失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def read_missing_codes_basic(file_path='data\missing_tables_小航富途.txt'):
|
||||
"""基础读取方法 - 按行读取所有内容"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
# 去除每行末尾的换行符,并过滤空行
|
||||
codes = [line.strip() for line in lines if line.strip()]
|
||||
return codes
|
||||
except FileNotFoundError:
|
||||
print(f"文件 {file_path} 不存在")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"读取文件失败: {str(e)}")
|
||||
return []
|
||||
|
||||
def get_stock_codes() -> List[str]:
|
||||
"""从conditionalselection表获取所有股票代码"""
|
||||
try:
|
||||
with MySQLHelper(**db_config) as db:
|
||||
sql = f"SELECT DISTINCT stock_code FROM conditionalselection"
|
||||
results = db.execute_query(sql)
|
||||
return [row['stock_code'] for row in results if row['stock_code']]
|
||||
except Exception as e:
|
||||
logging.error(f"获取股票代码失败: {str(e)}")
|
||||
return []
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'klinedata_1d_hk_akshare'
|
||||
}
|
||||
|
||||
|
||||
# market_data = get_market_data(Market.HK) # 获取香港市场数据,后面需要改成按照筛选来获取
|
||||
|
||||
# # 小航富途
|
||||
# market_data_hang = read_missing_codes_basic()
|
||||
#
|
||||
|
||||
# market_data = list(set(market_data_all) - set(market_data_hang))
|
||||
|
||||
nCount = 0 # 记录账号获取多少只股票的数据
|
||||
market_data_all = get_stock_codes()
|
||||
for code in market_data_all:
|
||||
stock_hk_index_daily_sina_df = ak.stock_hk_index_daily_sina(symbol=code[3:])
|
||||
custom_table_name = 'hk_' + code[3:] # 自定义表名
|
||||
success = save_quotes_to_db(db_config, stock_hk_index_daily_sina_df, table_name=custom_table_name)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user