first add code

This commit is contained in:
2025-08-15 13:22:58 +08:00
parent 8f8471bc1f
commit 0ddcdfacc6
13 changed files with 5801 additions and 0 deletions

View 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("处理过程中出现错误")

View 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

File diff suppressed because it is too large Load Diff

135
PyCode/MySQLHelper.py Normal file
View 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
View 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
View 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
View 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
View 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() # 结束后记得关闭当条连接,防止连接条数用尽

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

694
data/missing_tables.txt Normal file
View 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