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()