first add code
This commit is contained in:
298
PyCode/CalculateCapitalization.py
Normal file
298
PyCode/CalculateCapitalization.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
计算平均市值
|
||||
|
||||
根据 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("处理过程中出现错误")
|
||||
234
PyCode/ConditionalSelection.py
Normal file
234
PyCode/ConditionalSelection.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from futu import *
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from MySQLHelper import MySQLHelper # 假设您已有MySQLHelper类
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
class FutuDirectDataImporter:
|
||||
def __init__(self, db_config: Dict):
|
||||
self.db_config = db_config
|
||||
self.table_name = "conditionalselection"
|
||||
self.quote_ctx = None
|
||||
|
||||
def connect_to_futu(self) -> bool:
|
||||
"""连接Futu API"""
|
||||
try:
|
||||
self.quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"连接Futu API失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""断开连接"""
|
||||
if self.quote_ctx:
|
||||
self.quote_ctx.close()
|
||||
|
||||
def prepare_db_structure(self) -> bool:
|
||||
"""准备数据库表结构"""
|
||||
create_table_sql = f"""
|
||||
CREATE TABLE IF NOT EXISTS `{self.table_name}` (
|
||||
`stock_code` varchar(255) NOT NULL,
|
||||
`stock_name` varchar(255) DEFAULT NULL,
|
||||
`cur_price` float DEFAULT NULL,
|
||||
`price` float DEFAULT NULL,
|
||||
`high_price_to_highest_52weeks_ratio` float DEFAULT NULL,
|
||||
`low_price_to_lowest_52weeks_ratio` float DEFAULT NULL,
|
||||
`volume` float DEFAULT NULL,
|
||||
`turnover` float DEFAULT NULL,
|
||||
`turnover_rate` float DEFAULT NULL,
|
||||
`change_rate` float DEFAULT NULL,
|
||||
`amplitude` float DEFAULT NULL,
|
||||
`pe_ttm` float DEFAULT NULL,
|
||||
`pb_rate` float DEFAULT NULL,
|
||||
`market_val` float DEFAULT NULL,
|
||||
`total_share` float DEFAULT NULL,
|
||||
`float_share` float DEFAULT NULL,
|
||||
`float_market_val` float DEFAULT NULL,
|
||||
`basic_eps` float DEFAULT NULL,
|
||||
`diluted_eps` float DEFAULT NULL,
|
||||
UNIQUE KEY `idx_stock_code` (`stock_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
|
||||
try:
|
||||
with MySQLHelper(**self.db_config) as db:
|
||||
db.execute_update(create_table_sql)
|
||||
logging.info("数据库表准备就绪")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"准备数据库表失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def process_filter_result(self, ret_list: List) -> List[Dict]:
|
||||
"""处理筛选结果并转换为数据库格式"""
|
||||
processed_data = []
|
||||
for item in ret_list:
|
||||
try:
|
||||
data = {
|
||||
'stock_code': item.stock_code,
|
||||
'stock_name': item.stock_name,
|
||||
'float_share': item.float_share
|
||||
}
|
||||
# data = {
|
||||
# 'stock_code': item.stock_code,
|
||||
# 'stock_name': item.stock_name,
|
||||
# 'cur_price': item.cur_price,
|
||||
# 'price': item.cur_price,
|
||||
# 'high_price_to_highest_52weeks_ratio': item.high_price_to_highest_52weeks_ratio,
|
||||
# 'low_price_to_lowest_52weeks_ratio': item.low_price_to_lowest_52weeks_ratio,
|
||||
# 'volume': item.volume,
|
||||
# 'turnover': item.turnover,
|
||||
# 'turnover_rate': item.turnover_rate,
|
||||
# 'change_rate': item.change_rate,
|
||||
# 'amplitude': item.amplitude,
|
||||
# 'pe_ttm': item.pe_ttm,
|
||||
# 'pb_rate': item.pb,
|
||||
# 'market_val': item.market_val,
|
||||
# 'total_share': item.total_shares,
|
||||
# 'float_share': item.float_shares,
|
||||
# 'float_market_val': item.float_market_val,
|
||||
# 'basic_eps': item.basic_eps,
|
||||
# 'diluted_eps': item.diluted_eps
|
||||
# }
|
||||
|
||||
# # 计算衍生字段
|
||||
# if data['cur_price'] and data['high_price_to_highest_52weeks_ratio']:
|
||||
# data['cur_price_to_highest_52weeks_ratio'] = (
|
||||
# data['cur_price'] / data['high_price_to_highest_52weeks_ratio']
|
||||
# )
|
||||
|
||||
# if data['cur_price'] and data['low_price_to_lowest_52weeks_ratio']:
|
||||
# data['cur_price_to_lowest_52weeks_ratio'] = (
|
||||
# data['cur_price'] / data['low_price_to_lowest_52weeks_ratio']
|
||||
# )
|
||||
|
||||
processed_data.append(data)
|
||||
except Exception as e:
|
||||
logging.error(f"处理股票 {getattr(item, 'stock_code', '未知')} 数据失败: {str(e)}")
|
||||
|
||||
return processed_data
|
||||
|
||||
def save_batch_to_database(self, data_batch: List[Dict]) -> int:
|
||||
"""批量保存数据到数据库"""
|
||||
if not data_batch:
|
||||
return 0
|
||||
|
||||
# 准备SQL
|
||||
columns = [
|
||||
'stock_code', 'stock_name', 'cur_price', 'price',
|
||||
'high_price_to_highest_52weeks_ratio', 'low_price_to_lowest_52weeks_ratio',
|
||||
'volume', 'turnover', 'turnover_rate', 'change_rate', 'amplitude',
|
||||
'pe_ttm', 'pb_rate', 'market_val', 'total_share', 'float_share',
|
||||
'float_market_val', 'basic_eps', 'diluted_eps',
|
||||
'cur_price_to_highest_52weeks_ratio', 'cur_price_to_lowest_52weeks_ratio'
|
||||
]
|
||||
|
||||
placeholders = ', '.join(['%s'] * len(columns))
|
||||
columns_str = ', '.join([f'`{col}`' for col in columns])
|
||||
updates = ', '.join([f'`{col}`=VALUES(`{col}`)' for col in columns if col != 'stock_code'])
|
||||
|
||||
sql = f"""
|
||||
INSERT INTO `{self.table_name}` ({columns_str})
|
||||
VALUES ({placeholders})
|
||||
ON DUPLICATE KEY UPDATE {updates}
|
||||
"""
|
||||
|
||||
try:
|
||||
with MySQLHelper(**self.db_config) as db:
|
||||
# 准备参数列表
|
||||
params = []
|
||||
for item in data_batch:
|
||||
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} 行")
|
||||
return affected_rows
|
||||
except Exception as e:
|
||||
logging.error(f"批量保存失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
def run_direct_import(self):
|
||||
"""运行直接导入流程"""
|
||||
if not self.connect_to_futu():
|
||||
return False
|
||||
|
||||
if not self.prepare_db_structure():
|
||||
self.disconnect()
|
||||
return False
|
||||
|
||||
simple_filter = SimpleFilter()
|
||||
simple_filter.filter_min = 0
|
||||
simple_filter.filter_max = 100000000000000
|
||||
simple_filter.stock_field = StockField.FLOAT_SHARE
|
||||
simple_filter.is_no_filter = False
|
||||
|
||||
# simple_filter1 = SimpleFilter()
|
||||
# simple_filter1.filter_min = 0
|
||||
# simple_filter1.filter_max = 100000000000000000000
|
||||
# simple_filter1.stock_field = StockField.CUR_PRICE
|
||||
# simple_filter1.is_no_filter = False
|
||||
|
||||
|
||||
nBegin = 0
|
||||
last_page = False
|
||||
total_imported = 0
|
||||
|
||||
try:
|
||||
while not last_page:
|
||||
ret, ls = self.quote_ctx.get_stock_filter(
|
||||
market=Market.HK,
|
||||
filter_list=[simple_filter],
|
||||
begin=nBegin
|
||||
)
|
||||
|
||||
if ret == RET_OK:
|
||||
last_page, all_count, ret_list = ls
|
||||
logging.info(f'获取股票列表进度: {nBegin}/{all_count}')
|
||||
|
||||
# 处理并保存当前批次数据
|
||||
processed_data = self.process_filter_result(ret_list)
|
||||
if processed_data:
|
||||
affected_rows = self.save_batch_to_database(processed_data)
|
||||
total_imported += affected_rows
|
||||
|
||||
nBegin += len(ret_list)
|
||||
else:
|
||||
logging.error(f'获取股票列表错误: {ls}')
|
||||
break
|
||||
|
||||
time.sleep(3) # 避免触发限频
|
||||
|
||||
logging.info(f"导入完成! 共导入 {total_imported} 条记录")
|
||||
return total_imported > 0
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"导入过程中发生错误: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
self.disconnect()
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'fullmarketdata_hk'
|
||||
}
|
||||
|
||||
# 创建导入器并运行
|
||||
importer = FutuDirectDataImporter(db_config)
|
||||
importer.run_direct_import()
|
||||
1142
PyCode/ExportData.py
Normal file
1142
PyCode/ExportData.py
Normal file
File diff suppressed because it is too large
Load Diff
135
PyCode/MySQLHelper.py
Normal file
135
PyCode/MySQLHelper.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import pymysql
|
||||
from pymysql import Error
|
||||
from typing import List, Dict, Union, Optional, Tuple
|
||||
|
||||
class MySQLHelper:
|
||||
def __init__(self, host: str, user: str, password: str, database: str,
|
||||
port: int = 3306, charset: str = 'utf8mb4'):
|
||||
"""
|
||||
初始化MySQL连接参数
|
||||
:param host: 数据库地址
|
||||
:param user: 用户名
|
||||
:param password: 密码
|
||||
:param database: 数据库名
|
||||
:param port: 端口,默认3306
|
||||
:param charset: 字符集,默认utf8mb4
|
||||
"""
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.port = port
|
||||
self.charset = charset
|
||||
self.connection = None
|
||||
self.cursor = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""
|
||||
连接到MySQL数据库
|
||||
:return: 连接成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
self.connection = pymysql.connect(
|
||||
host=self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
database=self.database,
|
||||
port=self.port,
|
||||
charset=self.charset,
|
||||
cursorclass=pymysql.cursors.DictCursor # 返回字典形式的结果
|
||||
)
|
||||
self.cursor = self.connection.cursor()
|
||||
print("MySQL数据库连接成功")
|
||||
return True
|
||||
except Error as e:
|
||||
print(f"连接MySQL数据库失败: {e}")
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
关闭数据库连接
|
||||
"""
|
||||
if self.cursor:
|
||||
self.cursor.close()
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
print("MySQL数据库连接已关闭")
|
||||
|
||||
def execute_query(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> List[Dict]:
|
||||
"""
|
||||
执行查询操作
|
||||
:param sql: SQL语句
|
||||
:param params: 参数,可以是元组、列表或字典
|
||||
:return: 查询结果列表
|
||||
"""
|
||||
try:
|
||||
self.cursor.execute(sql, params)
|
||||
return self.cursor.fetchall()
|
||||
except Error as e:
|
||||
print(f"查询执行失败: {e}")
|
||||
return []
|
||||
|
||||
def execute_update(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> int:
|
||||
"""
|
||||
执行更新操作(INSERT/UPDATE/DELETE)
|
||||
:param sql: SQL语句
|
||||
:param params: 参数,可以是元组、列表或字典
|
||||
:return: 影响的行数
|
||||
"""
|
||||
try:
|
||||
affected_rows = self.cursor.execute(sql, params)
|
||||
self.connection.commit()
|
||||
return affected_rows
|
||||
except Error as e:
|
||||
self.connection.rollback()
|
||||
print(f"更新执行失败: {e}")
|
||||
return 0
|
||||
|
||||
def execute_many(self, sql: str, params_list: List[Union[Tuple, List, Dict]]) -> int:
|
||||
"""
|
||||
批量执行操作
|
||||
:param sql: SQL语句
|
||||
:param params_list: 参数列表
|
||||
:return: 影响的行数
|
||||
"""
|
||||
try:
|
||||
affected_rows = self.cursor.executemany(sql, params_list)
|
||||
self.connection.commit()
|
||||
return affected_rows
|
||||
except Error as e:
|
||||
self.connection.rollback()
|
||||
print(f"批量执行失败: {e}")
|
||||
return 0
|
||||
|
||||
def get_one(self, sql: str, params: Union[Tuple, List, Dict, None] = None) -> Optional[Dict]:
|
||||
"""
|
||||
获取单条记录
|
||||
:param sql: SQL语句
|
||||
:param params: 参数,可以是元组、列表或字典
|
||||
:return: 单条记录或None
|
||||
"""
|
||||
try:
|
||||
self.cursor.execute(sql, params)
|
||||
return self.cursor.fetchone()
|
||||
except Error as e:
|
||||
print(f"获取单条记录失败: {e}")
|
||||
return None
|
||||
|
||||
def table_exists(self, table_name: str) -> bool:
|
||||
"""
|
||||
检查表是否存在
|
||||
:param table_name: 表名
|
||||
:return: 存在返回True,否则返回False
|
||||
"""
|
||||
sql = "SHOW TABLES LIKE %s"
|
||||
result = self.execute_query(sql, (table_name,))
|
||||
return len(result) > 0
|
||||
|
||||
def __enter__(self):
|
||||
"""支持with上下文管理"""
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""支持with上下文管理"""
|
||||
self.close()
|
||||
139
PyCode/checktable.py
Normal file
139
PyCode/checktable.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
检查K线数据是否下载完成
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
from MySQLHelper import MySQLHelper # 假设您已有MySQLHelper类
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
class StockTableChecker:
|
||||
def __init__(self, db_config: dict):
|
||||
self.db_config = db_config
|
||||
self.conditional_selection_table = "conditionalselection"
|
||||
|
||||
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}"
|
||||
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 []
|
||||
|
||||
def check_tables_exist(self, stock_codes: List[str]) -> dict:
|
||||
"""
|
||||
检查数据库中是否存在与股票代码同名的表
|
||||
返回: {'exists': [], 'not_exists': []}
|
||||
"""
|
||||
if not stock_codes:
|
||||
return {'exists': [], 'not_exists': []}
|
||||
|
||||
try:
|
||||
with MySQLHelper(**self.db_config) as db:
|
||||
# 获取数据库中所有表名
|
||||
sql = "SHOW TABLES"
|
||||
tables = [list(row.values())[0] for row in db.execute_query(sql)]
|
||||
|
||||
# 检查每个股票代码对应的表是否存在
|
||||
result = {'exists': [], 'not_exists': []}
|
||||
for code in stock_codes:
|
||||
# 处理股票代码中的特殊字符,例如将'.'替换为'_'
|
||||
table_name = 'hk_' + code[3:]
|
||||
if table_name in tables:
|
||||
result['exists'].append(code)
|
||||
else:
|
||||
result['not_exists'].append(code)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logging.error(f"检查表存在性失败: {str(e)}")
|
||||
return {'exists': [], 'not_exists': []}
|
||||
|
||||
def write_missing_codes_to_txt(self, missing_codes: list, filename: str = "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}")
|
||||
except Exception as e:
|
||||
logging.error(f"写入TXT文件失败: {str(e)}")
|
||||
|
||||
def read_missing_codes_basic(file_path='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 run_check(self):
|
||||
"""执行完整的检查流程"""
|
||||
logging.info("开始检查股票代码对应表...")
|
||||
|
||||
# 1. 获取所有股票代码
|
||||
stock_codes = self.get_stock_codes()
|
||||
if not stock_codes:
|
||||
logging.error("没有获取到任何股票代码")
|
||||
return
|
||||
|
||||
logging.info(f"共获取到 {len(stock_codes)} 个股票代码")
|
||||
|
||||
# 2. 检查表存在性
|
||||
check_result = self.check_tables_exist(stock_codes)
|
||||
exists_count = len(check_result['exists'])
|
||||
not_exists_count = len(check_result['not_exists'])
|
||||
|
||||
# 3. 输出结果
|
||||
logging.info("\n检查结果:")
|
||||
logging.info(f"存在的表数量: {exists_count}")
|
||||
logging.info(f"不存在的表数量: {not_exists_count}")
|
||||
|
||||
if not_exists_count > 0:
|
||||
logging.info("\n不存在的表对应的股票代码:")
|
||||
for code in check_result['not_exists']:
|
||||
logging.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%}")
|
||||
|
||||
# 4. 将缺失的股票代码写入TXT文件
|
||||
if check_result['not_exists']:
|
||||
self.write_missing_codes_to_txt(check_result['not_exists'])
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'klinedata_1d_hk'
|
||||
}
|
||||
|
||||
# 创建检查器并运行
|
||||
checker = StockTableChecker(db_config)
|
||||
checker.run_check()
|
||||
308
PyCode/fullmarketdata.py
Normal file
308
PyCode/fullmarketdata.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
全市场数据更新
|
||||
- 每日更新一次全市场数据
|
||||
- 重点关注:新发、停盘、退市
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('Debug.log', encoding='utf-8'), # 关键在这里
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# 安全转换函数
|
||||
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 get_market_data(market: Market) -> Optional[List[Dict]]:
|
||||
"""
|
||||
获取指定市场股票基础数据
|
||||
|
||||
Args:
|
||||
market (Market): 市场枚举值,如 Market.SH, Market.SZ 等
|
||||
|
||||
Returns:
|
||||
Optional[List[Dict]]: 处理后的数据集或None(获取失败时)
|
||||
"""
|
||||
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:
|
||||
logging.info(f"成功获取 {market} 市场 {len(data)} 条股票数据")
|
||||
processed_data = []
|
||||
for _, row in data.iterrows():
|
||||
try:
|
||||
item = {
|
||||
'code': str(row['code']),
|
||||
'name': str(row['name']),
|
||||
'lot_size': safe_int(row['lot_size']),
|
||||
'stock_type': str(row['stock_type']),
|
||||
'stock_child_type': str(row['stock_child_type']),
|
||||
'stock_owner': str(row['stock_owner']) if pd.notna(row['stock_owner']) else None,
|
||||
'option_type': str(row['option_type']) if pd.notna(row['option_type']) else None,
|
||||
'strike_time': str(row['strike_time']) if pd.notna(row['strike_time']) else None,
|
||||
'strike_price': safe_float(row['strike_price']),
|
||||
'suspension': safe_int(row['suspension']),
|
||||
'listing_date': safe_parse_date(row['listing_date']),
|
||||
'stock_id': safe_int(row['stock_id']),
|
||||
'delisting': safe_int(row['delisting']),
|
||||
'index_option_type': str(row['index_option_type']) if pd.notna(row['index_option_type']) else None,
|
||||
'main_contract': safe_int(row['main_contract']),
|
||||
'last_trade_time': safe_parse_date(row['last_trade_time']),
|
||||
'exchange_type': safe_int(row['exchange_type']),
|
||||
# 'market': market.name # 添加市场标识
|
||||
}
|
||||
processed_data.append(item)
|
||||
except Exception as e:
|
||||
logging.warning(f"处理股票数据时跳过异常数据 {row.get('code', '未知')}: {str(e)}")
|
||||
continue
|
||||
return processed_data
|
||||
else:
|
||||
logging.error(f"获取 {market} 市场数据失败: {data}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"获取 {market} 市场数据时发生异常: {str(e)}")
|
||||
return None
|
||||
finally:
|
||||
quote_ctx.close()
|
||||
|
||||
def update_market_data(db_config: dict, markets: List[Market], truncate_table: bool = False) -> bool:
|
||||
"""
|
||||
更新全市场数据到数据库
|
||||
|
||||
Args:
|
||||
db_config (dict): 数据库配置
|
||||
markets (List[Market]): 要更新的市场列表
|
||||
truncate_table (bool): 是否在插入前清空表
|
||||
|
||||
Returns:
|
||||
bool: 操作是否成功
|
||||
"""
|
||||
# 设置默认值
|
||||
db_config.setdefault('port', 3306)
|
||||
db_config.setdefault('charset', 'utf8mb4')
|
||||
|
||||
try:
|
||||
all_data = []
|
||||
|
||||
# 获取所有指定市场的数据
|
||||
for market in markets:
|
||||
market_data = get_market_data(market)
|
||||
if market_data:
|
||||
all_data.extend(market_data)
|
||||
|
||||
if not all_data:
|
||||
logging.error("未获取到任何有效数据,终止更新")
|
||||
return False
|
||||
|
||||
# 数据验证
|
||||
validated_data = validate_market_data(all_data)
|
||||
if not validated_data:
|
||||
logging.error("没有通过验证的有效数据,终止更新")
|
||||
return False
|
||||
|
||||
logging.info(f"准备插入 {len(validated_data)} 条已验证数据")
|
||||
|
||||
with MySQLHelper(**db_config) as db:
|
||||
try:
|
||||
if truncate_table:
|
||||
db.execute_update("TRUNCATE TABLE staticdata_hk")
|
||||
logging.info("已清空数据表")
|
||||
|
||||
insert_sql = """
|
||||
INSERT INTO staticdata_hk (
|
||||
code, name, lot_size, stock_type, stock_child_type, stock_owner,
|
||||
option_type, strike_time, strike_price, suspension, listing_date,
|
||||
stock_id, delisting, index_option_type, main_contract, last_trade_time,
|
||||
exchange_type
|
||||
) VALUES (
|
||||
%(code)s, %(name)s, %(lot_size)s, %(stock_type)s, %(stock_child_type)s, %(stock_owner)s,
|
||||
%(option_type)s, %(strike_time)s, %(strike_price)s, %(suspension)s, %(listing_date)s,
|
||||
%(stock_id)s, %(delisting)s, %(index_option_type)s, %(main_contract)s, %(last_trade_time)s,
|
||||
%(exchange_type)s
|
||||
)
|
||||
"""
|
||||
|
||||
affected_rows = db.execute_many(insert_sql, validated_data)
|
||||
logging.info(f"成功插入 {affected_rows} 条记录")
|
||||
|
||||
check_special_cases(db)
|
||||
generate_data_quality_report(validated_data)
|
||||
|
||||
return True
|
||||
|
||||
except Error as e:
|
||||
logging.error(f"数据库操作失败: {str(e)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"更新过程中发生未预期异常: {str(e)}")
|
||||
return False
|
||||
|
||||
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('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 generate_data_quality_report(dataset: list) -> None:
|
||||
"""
|
||||
生成数据质量报告
|
||||
|
||||
Args:
|
||||
dataset (list): 数据集
|
||||
"""
|
||||
if not dataset:
|
||||
logging.warning("无法生成数据质量报告: 数据集为空")
|
||||
return
|
||||
|
||||
total = len(dataset)
|
||||
stats = {
|
||||
'missing_last_trade': sum(1 for x in dataset if x.get('last_trade_time') is None),
|
||||
'missing_listing': sum(1 for x in dataset if x.get('listing_date') is None),
|
||||
'missing_strike_price': sum(1 for x in dataset if x.get('strike_price') is None),
|
||||
'suspended': sum(1 for x in dataset if x.get('suspension') == 1),
|
||||
'delisted': sum(1 for x in dataset if x.get('delisting') == 1)
|
||||
}
|
||||
|
||||
logging.info("\n=== 数据质量报告 ===")
|
||||
logging.info(f"总记录数: {total}")
|
||||
logging.info(f"缺失最后交易时间: {stats['missing_last_trade']} ({stats['missing_last_trade']/total:.1%})")
|
||||
logging.info(f"缺失上市日期: {stats['missing_listing']} ({stats['missing_listing']/total:.1%})")
|
||||
logging.info(f"缺失执行价格: {stats['missing_strike_price']} ({stats['missing_strike_price']/total:.1%})")
|
||||
logging.info(f"停牌股票数: {stats['suspended']} ({stats['suspended']/total:.1%})")
|
||||
logging.info(f"退市股票数: {stats['delisted']} ({stats['delisted']/total:.1%})")
|
||||
|
||||
|
||||
def check_special_cases(db: MySQLHelper):
|
||||
"""
|
||||
检查重点关注项:新发、停盘、退市股票
|
||||
"""
|
||||
try:
|
||||
# 1. 检查新上市股票(最近30天内上市)
|
||||
new_listings = db.execute_query("""
|
||||
SELECT code, name, listing_date
|
||||
FROM staticdata_hk
|
||||
WHERE listing_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
|
||||
ORDER BY listing_date DESC
|
||||
""")
|
||||
if new_listings:
|
||||
logging.info("\n=== 新上市股票 ===")
|
||||
for stock in new_listings:
|
||||
logging.info(f"{stock['code']} {stock['name']} 上市日期: {stock['listing_date']}")
|
||||
|
||||
# 2. 检查停牌股票
|
||||
suspended = db.execute_query("""
|
||||
SELECT code, name, suspension
|
||||
FROM staticdata_hk
|
||||
WHERE suspension = 1
|
||||
ORDER BY code
|
||||
""")
|
||||
if suspended:
|
||||
logging.info("\n=== 停牌股票 ===")
|
||||
for stock in suspended:
|
||||
logging.info(f"{stock['code']} {stock['name']}")
|
||||
|
||||
# 3. 检查退市股票
|
||||
delisted = db.execute_query("""
|
||||
SELECT code, name, delisting
|
||||
FROM staticdata_hk
|
||||
WHERE delisting = 1
|
||||
ORDER BY code
|
||||
""")
|
||||
if delisted:
|
||||
logging.info("\n=== 退市股票 ===")
|
||||
for stock in delisted:
|
||||
logging.info(f"{stock['code']} {stock['name']}")
|
||||
|
||||
except Error as e:
|
||||
logging.error(f"检查重点关注项时出错: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 配置要更新的市场
|
||||
# target_markets = [Market.SH, Market.SZ, Market.HK] # 上证、深证、港股
|
||||
target_markets = [Market.HK] # 上证、深证、港股
|
||||
|
||||
# 数据库配置
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': 'bzskmysql',
|
||||
'database': 'fullmarketdata_a'
|
||||
}
|
||||
|
||||
logging.info("=== 开始全市场数据更新 ===")
|
||||
|
||||
# 执行更新(首次运行可设置truncate_table=True)
|
||||
success = update_market_data(
|
||||
db_config=db_config,
|
||||
markets=target_markets,
|
||||
truncate_table=True
|
||||
)
|
||||
|
||||
if success:
|
||||
logging.info("市场数据更新成功完成")
|
||||
else:
|
||||
logging.error("市场数据更新失败")
|
||||
31
PyCode/updateExecl.py
Normal file
31
PyCode/updateExecl.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import pandas as pd
|
||||
# 检查文件是否存在且有效
|
||||
import os
|
||||
if not os.path.exists('202410_202508_平均流通市值.xlsx'):
|
||||
print("文件不存在")
|
||||
else:
|
||||
# 尝试用 openpyxl 直接打开检查
|
||||
from openpyxl import load_workbook
|
||||
try:
|
||||
wb = load_workbook('202410_202508_平均流通市值.xlsx')
|
||||
print(f"文件中包含的工作表: {wb.sheetnames}")
|
||||
except Exception as e:
|
||||
print(f"文件损坏或不是有效的Excel文件: {e}")
|
||||
|
||||
# 读取原始Excel文件
|
||||
df = pd.read_excel('202410_202508_平均流通市值.xlsx') # 假设你的文件名为"股票数据.xlsx"
|
||||
|
||||
# 筛选股票名称最后一个字符不是"R"的股票
|
||||
# 方法1:使用字符串的.endswith()方法
|
||||
filtered_df = df[~df['股票名称'].str.endswith('R')]
|
||||
|
||||
# 方法2:使用正则表达式
|
||||
# filtered_df = df[~df['股票名称'].str.contains('R$')] # $表示字符串结尾
|
||||
|
||||
# 方法3:使用字符串切片
|
||||
# filtered_df = df[df['股票名称'].str[-1] != 'R']
|
||||
|
||||
# 将筛选结果写入新Excel文件
|
||||
filtered_df.to_excel('202410_202508_平均流通市值_不带R的股票.xlsx', index=False)
|
||||
|
||||
print("筛选完成,结果已保存到'筛选结果_不带R的股票.xlsx'")
|
||||
232
PyCode/updatekline.py
Normal file
232
PyCode/updatekline.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
该代码用于更新日 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
|
||||
|
||||
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['time_key'], '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
item = {
|
||||
'stock_code': row['code'],
|
||||
'stock_name': row['name'],
|
||||
'trade_date': trade_time,
|
||||
'open_price': float(row['open']),
|
||||
'close_price': float(row['close']),
|
||||
'high_price': float(row['high']),
|
||||
'low_price': float(row['low']),
|
||||
'pe_ratio': float(row['pe_ratio']) if pd.notna(row['pe_ratio']) else None,
|
||||
'turnover_rate': float(row['turnover_rate']) if pd.notna(row['turnover_rate']) else None,
|
||||
'volume': int(row['volume']) if pd.notna(row['volume']) else None,
|
||||
'turnover': float(row['turnover']) if pd.notna(row['turnover']) else None,
|
||||
'change_rate': float(row['change_rate']) if pd.notna(row['change_rate']) else None,
|
||||
'last_close': float(row['last_close']) if pd.notna(row['last_close']) else None,
|
||||
'market': market
|
||||
}
|
||||
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} (
|
||||
stock_code, stock_name, trade_date, open_price, close_price, high_price, low_price,
|
||||
pe_ratio, turnover_rate, volume, turnover, change_rate, last_close, market
|
||||
) VALUES (
|
||||
%(stock_code)s, %(stock_name)s, %(trade_date)s, %(open_price)s, %(close_price)s, %(high_price)s, %(low_price)s,
|
||||
%(pe_ratio)s, %(turnover_rate)s, %(volume)s, %(turnover)s, %(change_rate)s, %(last_close)s, %(market)s
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
stock_name = VALUES(stock_name),
|
||||
open_price = VALUES(open_price),
|
||||
close_price = VALUES(close_price),
|
||||
high_price = VALUES(high_price),
|
||||
low_price = VALUES(low_price),
|
||||
pe_ratio = VALUES(pe_ratio),
|
||||
turnover_rate = VALUES(turnover_rate),
|
||||
volume = VALUES(volume),
|
||||
turnover = VALUES(turnover),
|
||||
change_rate = VALUES(change_rate),
|
||||
last_close = VALUES(last_close)
|
||||
"""
|
||||
|
||||
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,
|
||||
stock_code VARCHAR(20) NOT NULL COMMENT '股票代码',
|
||||
stock_name VARCHAR(50) COMMENT '股票名称',
|
||||
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 '最低价',
|
||||
pe_ratio DECIMAL(10, 3) COMMENT '市盈率',
|
||||
turnover_rate DECIMAL(10, 6) COMMENT '换手率(%)',
|
||||
volume BIGINT COMMENT '成交量(股)',
|
||||
turnover DECIMAL(20, 2) COMMENT '成交额(元)',
|
||||
change_rate DECIMAL(10, 6) COMMENT '涨跌幅(%)',
|
||||
last_close DECIMAL(10, 3) COMMENT '昨收价',
|
||||
market VARCHAR(10) COMMENT '市场标识',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
UNIQUE KEY uk_code_date (stock_code, trade_date),
|
||||
KEY idx_trade_date (trade_date),
|
||||
KEY idx_stock_code (stock_code)
|
||||
) 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='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 []
|
||||
|
||||
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 = read_missing_codes_basic()
|
||||
|
||||
nCount = 0 # 记录账号获取多少只股票的数据
|
||||
for code in market_data:
|
||||
|
||||
# nCount = nCount + 1
|
||||
# if nCount < 450:
|
||||
# continue
|
||||
|
||||
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||
# ret, data, page_req_key = quote_ctx.request_history_kline(code, start='2024-09-01', end='2025-08-20', max_count=100, session=Session.ALL) # 每页5个,请求第一页 session 字段紧美股有用
|
||||
ret, data, page_req_key = quote_ctx.request_history_kline(code, start='2024-10-01', end='2025-08-20', max_count=100) # 每页5个,请求第一页
|
||||
# 保存数据到自定义表
|
||||
custom_table_name = 'hk_' + code[3:] # 自定义表名
|
||||
|
||||
if ret == RET_OK:
|
||||
success = save_quotes_to_db(db_config, data, table_name=custom_table_name)
|
||||
else:
|
||||
print('error:', data)
|
||||
|
||||
while page_req_key != None: # 请求后面的所有结果
|
||||
# print('*************************************')
|
||||
ret, data, page_req_key = quote_ctx.request_history_kline(code, start='2024-10-01', end='2025-08-20', max_count=100, page_req_key=page_req_key) # 请求翻页后的数据
|
||||
if ret == RET_OK:
|
||||
success = save_quotes_to_db(db_config, data, table_name=custom_table_name)
|
||||
else:
|
||||
print('error:', data)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# if nCount == 2000:
|
||||
# break
|
||||
quote_ctx.close() # 结束后记得关闭当条连接,防止连接条数用尽
|
||||
|
||||
|
||||
BIN
data/202410_202508_平均流通市值.xls
Normal file
BIN
data/202410_202508_平均流通市值.xls
Normal file
Binary file not shown.
BIN
data/202410_202508_平均流通市值.xlsx
Normal file
BIN
data/202410_202508_平均流通市值.xlsx
Normal file
Binary file not shown.
2588
data/hk_stocks_combined_data.csv
Normal file
2588
data/hk_stocks_combined_data.csv
Normal file
File diff suppressed because it is too large
Load Diff
BIN
data/hk_stocks_combined_data.xlsx
Normal file
BIN
data/hk_stocks_combined_data.xlsx
Normal file
Binary file not shown.
694
data/missing_tables.txt
Normal file
694
data/missing_tables.txt
Normal file
@@ -0,0 +1,694 @@
|
||||
HK.00405
|
||||
HK.00435
|
||||
HK.00778
|
||||
HK.00808
|
||||
HK.00823
|
||||
HK.01426
|
||||
HK.01503
|
||||
HK.01881
|
||||
HK.02191
|
||||
HK.02778
|
||||
HK.03380
|
||||
HK.03382
|
||||
HK.03383
|
||||
HK.03389
|
||||
HK.03390
|
||||
HK.03393
|
||||
HK.03395
|
||||
HK.03396
|
||||
HK.03398
|
||||
HK.03399
|
||||
HK.03600
|
||||
HK.03601
|
||||
HK.03603
|
||||
HK.03606
|
||||
HK.03613
|
||||
HK.03618
|
||||
HK.03623
|
||||
HK.03626
|
||||
HK.03628
|
||||
HK.03633
|
||||
HK.03638
|
||||
HK.03639
|
||||
HK.03650
|
||||
HK.03658
|
||||
HK.03660
|
||||
HK.03662
|
||||
HK.03666
|
||||
HK.03668
|
||||
HK.03669
|
||||
HK.03677
|
||||
HK.03678
|
||||
HK.03680
|
||||
HK.03681
|
||||
HK.03683
|
||||
HK.03686
|
||||
HK.03688
|
||||
HK.03689
|
||||
HK.03690
|
||||
HK.03692
|
||||
HK.03698
|
||||
HK.03699
|
||||
HK.03700
|
||||
HK.03708
|
||||
HK.03709
|
||||
HK.03718
|
||||
HK.03728
|
||||
HK.03737
|
||||
HK.03738
|
||||
HK.03750
|
||||
HK.03759
|
||||
HK.03768
|
||||
HK.03773
|
||||
HK.03778
|
||||
HK.03788
|
||||
HK.03789
|
||||
HK.03798
|
||||
HK.03800
|
||||
HK.03808
|
||||
HK.03813
|
||||
HK.03816
|
||||
HK.03818
|
||||
HK.03822
|
||||
HK.03828
|
||||
HK.03830
|
||||
HK.03833
|
||||
HK.03836
|
||||
HK.03838
|
||||
HK.03839
|
||||
HK.03848
|
||||
HK.03860
|
||||
HK.03866
|
||||
HK.03868
|
||||
HK.03869
|
||||
HK.03877
|
||||
HK.03878
|
||||
HK.03880
|
||||
HK.03882
|
||||
HK.03883
|
||||
HK.03886
|
||||
HK.03888
|
||||
HK.03889
|
||||
HK.03893
|
||||
HK.03896
|
||||
HK.03898
|
||||
HK.03899
|
||||
HK.03900
|
||||
HK.03908
|
||||
HK.03913
|
||||
HK.03918
|
||||
HK.03919
|
||||
HK.03928
|
||||
HK.03931
|
||||
HK.03933
|
||||
HK.03938
|
||||
HK.03939
|
||||
HK.03958
|
||||
HK.03963
|
||||
HK.03968
|
||||
HK.03969
|
||||
HK.03978
|
||||
HK.03983
|
||||
HK.03988
|
||||
HK.03989
|
||||
HK.03990
|
||||
HK.03991
|
||||
HK.03993
|
||||
HK.03996
|
||||
HK.03997
|
||||
HK.03998
|
||||
HK.03999
|
||||
HK.04333
|
||||
HK.06030
|
||||
HK.06033
|
||||
HK.06036
|
||||
HK.06038
|
||||
HK.06049
|
||||
HK.06055
|
||||
HK.06058
|
||||
HK.06060
|
||||
HK.06063
|
||||
HK.06066
|
||||
HK.06068
|
||||
HK.06069
|
||||
HK.06078
|
||||
HK.06080
|
||||
HK.06083
|
||||
HK.06086
|
||||
HK.06088
|
||||
HK.06093
|
||||
HK.06098
|
||||
HK.06099
|
||||
HK.06100
|
||||
HK.06108
|
||||
HK.06110
|
||||
HK.06113
|
||||
HK.06117
|
||||
HK.06118
|
||||
HK.06119
|
||||
HK.06123
|
||||
HK.06127
|
||||
HK.06128
|
||||
HK.06133
|
||||
HK.06136
|
||||
HK.06138
|
||||
HK.06158
|
||||
HK.06160
|
||||
HK.06162
|
||||
HK.06163
|
||||
HK.06168
|
||||
HK.06169
|
||||
HK.06178
|
||||
HK.06181
|
||||
HK.06182
|
||||
HK.06185
|
||||
HK.06186
|
||||
HK.06188
|
||||
HK.06190
|
||||
HK.06193
|
||||
HK.06196
|
||||
HK.06198
|
||||
HK.06199
|
||||
HK.06288
|
||||
HK.06601
|
||||
HK.06603
|
||||
HK.06608
|
||||
HK.06609
|
||||
HK.06610
|
||||
HK.06613
|
||||
HK.06616
|
||||
HK.06618
|
||||
HK.06622
|
||||
HK.06626
|
||||
HK.06628
|
||||
HK.06633
|
||||
HK.06638
|
||||
HK.06639
|
||||
HK.06655
|
||||
HK.06657
|
||||
HK.06660
|
||||
HK.06661
|
||||
HK.06663
|
||||
HK.06666
|
||||
HK.06668
|
||||
HK.06669
|
||||
HK.06676
|
||||
HK.06677
|
||||
HK.06680
|
||||
HK.06681
|
||||
HK.06682
|
||||
HK.06683
|
||||
HK.06686
|
||||
HK.06690
|
||||
HK.06693
|
||||
HK.06696
|
||||
HK.06698
|
||||
HK.06699
|
||||
HK.06805
|
||||
HK.06806
|
||||
HK.06808
|
||||
HK.06811
|
||||
HK.06812
|
||||
HK.06816
|
||||
HK.06818
|
||||
HK.06820
|
||||
HK.06821
|
||||
HK.06822
|
||||
HK.06823
|
||||
HK.06826
|
||||
HK.06828
|
||||
HK.06829
|
||||
HK.06830
|
||||
HK.06831
|
||||
HK.06833
|
||||
HK.06838
|
||||
HK.06839
|
||||
HK.06855
|
||||
HK.06858
|
||||
HK.06860
|
||||
HK.06862
|
||||
HK.06865
|
||||
HK.06866
|
||||
HK.06868
|
||||
HK.06869
|
||||
HK.06877
|
||||
HK.06881
|
||||
HK.06882
|
||||
HK.06883
|
||||
HK.06885
|
||||
HK.06886
|
||||
HK.06887
|
||||
HK.06888
|
||||
HK.06889
|
||||
HK.06890
|
||||
HK.06893
|
||||
HK.06896
|
||||
HK.06898
|
||||
HK.06908
|
||||
HK.06909
|
||||
HK.06911
|
||||
HK.06913
|
||||
HK.06918
|
||||
HK.06919
|
||||
HK.06922
|
||||
HK.06928
|
||||
HK.06929
|
||||
HK.06933
|
||||
HK.06936
|
||||
HK.06939
|
||||
HK.06955
|
||||
HK.06958
|
||||
HK.06959
|
||||
HK.06963
|
||||
HK.06966
|
||||
HK.06968
|
||||
HK.06969
|
||||
HK.06978
|
||||
HK.06979
|
||||
HK.06988
|
||||
HK.06989
|
||||
HK.06990
|
||||
HK.06993
|
||||
HK.06996
|
||||
HK.06998
|
||||
HK.06999
|
||||
HK.07855
|
||||
HK.08003
|
||||
HK.08005
|
||||
HK.08006
|
||||
HK.08007
|
||||
HK.08013
|
||||
HK.08017
|
||||
HK.08018
|
||||
HK.08019
|
||||
HK.08020
|
||||
HK.08021
|
||||
HK.08023
|
||||
HK.08026
|
||||
HK.08027
|
||||
HK.08028
|
||||
HK.08029
|
||||
HK.08030
|
||||
HK.08031
|
||||
HK.08033
|
||||
HK.08035
|
||||
HK.08036
|
||||
HK.08037
|
||||
HK.08040
|
||||
HK.08041
|
||||
HK.08042
|
||||
HK.08043
|
||||
HK.08049
|
||||
HK.08050
|
||||
HK.08051
|
||||
HK.08052
|
||||
HK.08056
|
||||
HK.08057
|
||||
HK.08059
|
||||
HK.08060
|
||||
HK.08062
|
||||
HK.08065
|
||||
HK.08066
|
||||
HK.08067
|
||||
HK.08069
|
||||
HK.08070
|
||||
HK.08071
|
||||
HK.08072
|
||||
HK.08073
|
||||
HK.08076
|
||||
HK.08079
|
||||
HK.08080
|
||||
HK.08081
|
||||
HK.08082
|
||||
HK.08083
|
||||
HK.08087
|
||||
HK.08091
|
||||
HK.08092
|
||||
HK.08093
|
||||
HK.08095
|
||||
HK.08096
|
||||
HK.08098
|
||||
HK.08100
|
||||
HK.08103
|
||||
HK.08106
|
||||
HK.08107
|
||||
HK.08111
|
||||
HK.08112
|
||||
HK.08113
|
||||
HK.08115
|
||||
HK.08117
|
||||
HK.08118
|
||||
HK.08120
|
||||
HK.08121
|
||||
HK.08123
|
||||
HK.08125
|
||||
HK.08128
|
||||
HK.08131
|
||||
HK.08133
|
||||
HK.08136
|
||||
HK.08137
|
||||
HK.08139
|
||||
HK.08140
|
||||
HK.08143
|
||||
HK.08146
|
||||
HK.08147
|
||||
HK.08148
|
||||
HK.08149
|
||||
HK.08152
|
||||
HK.08153
|
||||
HK.08156
|
||||
HK.08158
|
||||
HK.08159
|
||||
HK.08160
|
||||
HK.08161
|
||||
HK.08162
|
||||
HK.08163
|
||||
HK.08168
|
||||
HK.08169
|
||||
HK.08172
|
||||
HK.08173
|
||||
HK.08176
|
||||
HK.08178
|
||||
HK.08179
|
||||
HK.08181
|
||||
HK.08186
|
||||
HK.08187
|
||||
HK.08188
|
||||
HK.08189
|
||||
HK.08191
|
||||
HK.08193
|
||||
HK.08195
|
||||
HK.08196
|
||||
HK.08198
|
||||
HK.08200
|
||||
HK.08201
|
||||
HK.08203
|
||||
HK.08205
|
||||
HK.08206
|
||||
HK.08208
|
||||
HK.08210
|
||||
HK.08213
|
||||
HK.08217
|
||||
HK.08219
|
||||
HK.08220
|
||||
HK.08221
|
||||
HK.08222
|
||||
HK.08223
|
||||
HK.08225
|
||||
HK.08226
|
||||
HK.08227
|
||||
HK.08229
|
||||
HK.08232
|
||||
HK.08237
|
||||
HK.08238
|
||||
HK.08239
|
||||
HK.08241
|
||||
HK.08245
|
||||
HK.08246
|
||||
HK.08247
|
||||
HK.08249
|
||||
HK.08250
|
||||
HK.08257
|
||||
HK.08262
|
||||
HK.08267
|
||||
HK.08268
|
||||
HK.08269
|
||||
HK.08270
|
||||
HK.08271
|
||||
HK.08275
|
||||
HK.08277
|
||||
HK.08279
|
||||
HK.08280
|
||||
HK.08281
|
||||
HK.08282
|
||||
HK.08283
|
||||
HK.08285
|
||||
HK.08286
|
||||
HK.08290
|
||||
HK.08291
|
||||
HK.08292
|
||||
HK.08293
|
||||
HK.08295
|
||||
HK.08296
|
||||
HK.08299
|
||||
HK.08300
|
||||
HK.08305
|
||||
HK.08307
|
||||
HK.08308
|
||||
HK.08309
|
||||
HK.08310
|
||||
HK.08311
|
||||
HK.08313
|
||||
HK.08315
|
||||
HK.08316
|
||||
HK.08317
|
||||
HK.08319
|
||||
HK.08320
|
||||
HK.08321
|
||||
HK.08326
|
||||
HK.08328
|
||||
HK.08329
|
||||
HK.08331
|
||||
HK.08333
|
||||
HK.08337
|
||||
HK.08340
|
||||
HK.08341
|
||||
HK.08347
|
||||
HK.08348
|
||||
HK.08349
|
||||
HK.08350
|
||||
HK.08356
|
||||
HK.08357
|
||||
HK.08360
|
||||
HK.08362
|
||||
HK.08365
|
||||
HK.08366
|
||||
HK.08367
|
||||
HK.08368
|
||||
HK.08370
|
||||
HK.08371
|
||||
HK.08372
|
||||
HK.08375
|
||||
HK.08377
|
||||
HK.08379
|
||||
HK.08385
|
||||
HK.08391
|
||||
HK.08392
|
||||
HK.08395
|
||||
HK.08400
|
||||
HK.08401
|
||||
HK.08402
|
||||
HK.08403
|
||||
HK.08406
|
||||
HK.08411
|
||||
HK.08412
|
||||
HK.08413
|
||||
HK.08416
|
||||
HK.08417
|
||||
HK.08418
|
||||
HK.08419
|
||||
HK.08420
|
||||
HK.08422
|
||||
HK.08423
|
||||
HK.08425
|
||||
HK.08426
|
||||
HK.08427
|
||||
HK.08429
|
||||
HK.08430
|
||||
HK.08431
|
||||
HK.08432
|
||||
HK.08436
|
||||
HK.08437
|
||||
HK.08439
|
||||
HK.08445
|
||||
HK.08446
|
||||
HK.08447
|
||||
HK.08448
|
||||
HK.08450
|
||||
HK.08451
|
||||
HK.08452
|
||||
HK.08455
|
||||
HK.08456
|
||||
HK.08460
|
||||
HK.08462
|
||||
HK.08471
|
||||
HK.08472
|
||||
HK.08473
|
||||
HK.08476
|
||||
HK.08480
|
||||
HK.08481
|
||||
HK.08482
|
||||
HK.08483
|
||||
HK.08487
|
||||
HK.08489
|
||||
HK.08490
|
||||
HK.08491
|
||||
HK.08493
|
||||
HK.08495
|
||||
HK.08496
|
||||
HK.08500
|
||||
HK.08501
|
||||
HK.08502
|
||||
HK.08507
|
||||
HK.08509
|
||||
HK.08510
|
||||
HK.08511
|
||||
HK.08512
|
||||
HK.08513
|
||||
HK.08516
|
||||
HK.08519
|
||||
HK.08521
|
||||
HK.08526
|
||||
HK.08529
|
||||
HK.08535
|
||||
HK.08536
|
||||
HK.08537
|
||||
HK.08540
|
||||
HK.08545
|
||||
HK.08547
|
||||
HK.08561
|
||||
HK.08565
|
||||
HK.08601
|
||||
HK.08603
|
||||
HK.08606
|
||||
HK.08611
|
||||
HK.08612
|
||||
HK.08613
|
||||
HK.08616
|
||||
HK.08619
|
||||
HK.08620
|
||||
HK.08621
|
||||
HK.08622
|
||||
HK.08623
|
||||
HK.08627
|
||||
HK.08629
|
||||
HK.08635
|
||||
HK.08637
|
||||
HK.08645
|
||||
HK.08646
|
||||
HK.08657
|
||||
HK.08659
|
||||
HK.08668
|
||||
HK.09600
|
||||
HK.09606
|
||||
HK.09608
|
||||
HK.09616
|
||||
HK.09618
|
||||
HK.09626
|
||||
HK.09633
|
||||
HK.09636
|
||||
HK.09638
|
||||
HK.09639
|
||||
HK.09658
|
||||
HK.09660
|
||||
HK.09663
|
||||
HK.09666
|
||||
HK.09668
|
||||
HK.09669
|
||||
HK.09676
|
||||
HK.09677
|
||||
HK.09678
|
||||
HK.09680
|
||||
HK.09686
|
||||
HK.09688
|
||||
HK.09689
|
||||
HK.09690
|
||||
HK.09696
|
||||
HK.09698
|
||||
HK.09699
|
||||
HK.09857
|
||||
HK.09858
|
||||
HK.09860
|
||||
HK.09863
|
||||
HK.09866
|
||||
HK.09868
|
||||
HK.09869
|
||||
HK.09877
|
||||
HK.09878
|
||||
HK.09879
|
||||
HK.09880
|
||||
HK.09881
|
||||
HK.09882
|
||||
HK.09885
|
||||
HK.09886
|
||||
HK.09887
|
||||
HK.09888
|
||||
HK.09889
|
||||
HK.09890
|
||||
HK.09893
|
||||
HK.09896
|
||||
HK.09898
|
||||
HK.09899
|
||||
HK.09900
|
||||
HK.09901
|
||||
HK.09906
|
||||
HK.09908
|
||||
HK.09909
|
||||
HK.09911
|
||||
HK.09913
|
||||
HK.09916
|
||||
HK.09918
|
||||
HK.09919
|
||||
HK.09922
|
||||
HK.09923
|
||||
HK.09926
|
||||
HK.09928
|
||||
HK.09929
|
||||
HK.09930
|
||||
HK.09933
|
||||
HK.09936
|
||||
HK.09938
|
||||
HK.09939
|
||||
HK.09955
|
||||
HK.09956
|
||||
HK.09958
|
||||
HK.09959
|
||||
HK.09960
|
||||
HK.09961
|
||||
HK.09963
|
||||
HK.09966
|
||||
HK.09968
|
||||
HK.09969
|
||||
HK.09978
|
||||
HK.09979
|
||||
HK.09983
|
||||
HK.09985
|
||||
HK.09986
|
||||
HK.09987
|
||||
HK.09988
|
||||
HK.09989
|
||||
HK.09990
|
||||
HK.09991
|
||||
HK.09992
|
||||
HK.09993
|
||||
HK.09995
|
||||
HK.09996
|
||||
HK.09997
|
||||
HK.09998
|
||||
HK.09999
|
||||
HK.80011
|
||||
HK.80016
|
||||
HK.80020
|
||||
HK.80175
|
||||
HK.80291
|
||||
HK.80388
|
||||
HK.80700
|
||||
HK.80737
|
||||
HK.80883
|
||||
HK.80941
|
||||
HK.80992
|
||||
HK.81024
|
||||
HK.81211
|
||||
HK.81299
|
||||
HK.81810
|
||||
HK.82020
|
||||
HK.82318
|
||||
HK.82331
|
||||
HK.82333
|
||||
HK.82388
|
||||
HK.83690
|
||||
HK.86618
|
||||
HK.87001
|
||||
HK.89618
|
||||
HK.89888
|
||||
HK.89988
|
||||
Reference in New Issue
Block a user