from typing import List, Dict, Any, Tuple from loguru import logger from sqlalchemy import text import time class BatchPositionSync: """持仓数据批量同步工具(使用临时表,最高性能)""" def __init__(self, db_manager, batch_size: int = 500): self.db_manager = db_manager self.batch_size = batch_size def sync_positions_batch(self, all_positions: List[Dict]) -> Tuple[bool, Dict]: """批量同步持仓数据(最高效版本)""" if not all_positions: return True, {'total': 0, 'updated': 0, 'inserted': 0, 'deleted': 0} session = self.db_manager.get_session() try: start_time = time.time() # 按账号分组 positions_by_account = self._group_positions_by_account(all_positions) total_stats = {'total': 0, 'updated': 0, 'inserted': 0, 'deleted': 0} with session.begin(): # 处理每个账号 for (k_id, st_id), positions in positions_by_account.items(): success, stats = self._sync_account_using_temp_table( session, k_id, st_id, positions ) if success: total_stats['total'] += stats['total'] total_stats['updated'] += stats['updated'] total_stats['inserted'] += stats['inserted'] total_stats['deleted'] += stats['deleted'] elapsed = time.time() - start_time logger.info(f"持仓批量同步完成: 处理 {len(positions_by_account)} 个账号," f"总持仓 {total_stats['total']} 条,耗时 {elapsed:.2f}秒") return True, total_stats except Exception as e: logger.error(f"持仓批量同步失败: {e}") return False, {'total': 0, 'updated': 0, 'inserted': 0, 'deleted': 0} finally: session.close() def _group_positions_by_account(self, all_positions: List[Dict]) -> Dict[Tuple[int, int], List[Dict]]: """按账号分组持仓数据""" groups = {} for position in all_positions: k_id = position.get('k_id') st_id = position.get('st_id', 0) key = (k_id, st_id) if key not in groups: groups[key] = [] groups[key].append(position) return groups def _sync_account_using_temp_table(self, session, k_id: int, st_id: int, positions: List[Dict]) -> Tuple[bool, Dict]: """使用临时表同步单个账号的持仓数据""" try: # 1. 创建临时表 session.execute(text(""" CREATE TEMPORARY TABLE IF NOT EXISTS temp_positions ( st_id INT, k_id INT, asset VARCHAR(32), symbol VARCHAR(50), side VARCHAR(10), price FLOAT, `sum` FLOAT, asset_num DECIMAL(20, 8), asset_profit DECIMAL(20, 8), leverage INT, uptime INT, profit_price DECIMAL(20, 8), stop_price DECIMAL(20, 8), liquidation_price DECIMAL(20, 8), PRIMARY KEY (k_id, st_id, symbol, side) ) """)) # 2. 清空临时表 session.execute(text("TRUNCATE TABLE temp_positions")) # 3. 批量插入数据到临时表 self._batch_insert_to_temp_table(session, positions) # 4. 使用临时表更新主表 # 更新已存在的记录 update_result = session.execute(text(f""" UPDATE deh_strategy_position_new main INNER JOIN temp_positions temp ON main.k_id = temp.k_id AND main.st_id = temp.st_id AND main.symbol = temp.symbol AND main.side = temp.side SET main.price = temp.price, main.`sum` = temp.`sum`, main.asset_num = temp.asset_num, main.asset_profit = temp.asset_profit, main.leverage = temp.leverage, main.uptime = temp.uptime, main.profit_price = temp.profit_price, main.stop_price = temp.stop_price, main.liquidation_price = temp.liquidation_price WHERE main.k_id = {k_id} AND main.st_id = {st_id} """)) updated_count = update_result.rowcount # 插入新记录 insert_result = session.execute(text(f""" INSERT INTO deh_strategy_position_new (st_id, k_id, asset, symbol, side, price, `sum`, asset_num, asset_profit, leverage, uptime, profit_price, stop_price, liquidation_price) SELECT st_id, k_id, asset, symbol, side, price, `sum`, asset_num, asset_profit, leverage, uptime, profit_price, stop_price, liquidation_price FROM temp_positions temp WHERE NOT EXISTS ( SELECT 1 FROM deh_strategy_position_new main WHERE main.k_id = temp.k_id AND main.st_id = temp.st_id AND main.symbol = temp.symbol AND main.side = temp.side ) AND temp.k_id = {k_id} AND temp.st_id = {st_id} """)) inserted_count = insert_result.rowcount # 5. 删除多余持仓(在临时表中不存在但在主表中存在的) delete_result = session.execute(text(f""" DELETE main FROM deh_strategy_position_new main LEFT JOIN temp_positions temp ON main.k_id = temp.k_id AND main.st_id = temp.st_id AND main.symbol = temp.symbol AND main.side = temp.side WHERE main.k_id = {k_id} AND main.st_id = {st_id} AND temp.symbol IS NULL """)) deleted_count = delete_result.rowcount # 6. 删除临时表 session.execute(text("DROP TEMPORARY TABLE IF EXISTS temp_positions")) stats = { 'total': len(positions), 'updated': updated_count, 'inserted': inserted_count, 'deleted': deleted_count } logger.debug(f"账号({k_id},{st_id})持仓同步: 更新{updated_count} 插入{inserted_count} 删除{deleted_count}") return True, stats except Exception as e: logger.error(f"临时表同步账号({k_id},{st_id})持仓失败: {e}") return False, {'total': 0, 'updated': 0, 'inserted': 0, 'deleted': 0} def _batch_insert_to_temp_table(self, session, positions: List[Dict]): """批量插入数据到临时表(使用参数化查询)""" if not positions: return # 分块处理 for i in range(0, len(positions), self.batch_size): chunk = positions[i:i + self.batch_size] # 准备参数化数据 insert_data = [] for position in chunk: try: data = self._convert_position_for_temp(position) if not all([data.get('symbol'), data.get('side')]): continue insert_data.append({ 'st_id': data['st_id'], 'k_id': data['k_id'], 'asset': data.get('asset', 'USDT'), 'symbol': data['symbol'], 'side': data['side'], 'price': data.get('price'), 'sum_val': data.get('sum'), # 注意字段名 'asset_num': data.get('asset_num'), 'asset_profit': data.get('asset_profit'), 'leverage': data.get('leverage'), 'uptime': data.get('uptime'), 'profit_price': data.get('profit_price'), 'stop_price': data.get('stop_price'), 'liquidation_price': data.get('liquidation_price') }) except Exception as e: logger.error(f"转换持仓数据失败: {position}, error={e}") continue if insert_data: sql = """ INSERT INTO temp_positions (st_id, k_id, asset, symbol, side, price, `sum`, asset_num, asset_profit, leverage, uptime, profit_price, stop_price, liquidation_price) VALUES (:st_id, :k_id, :asset, :symbol, :side, :price, :sum_val, :asset_num, :asset_profit, :leverage, :uptime, :profit_price, :stop_price, :liquidation_price) """ session.execute(text(sql), insert_data) def _convert_position_for_temp(self, data: Dict) -> Dict: """转换持仓数据格式用于临时表""" # 使用安全转换 def safe_float(value): try: return float(value) if value is not None else None except: return None def safe_int(value): try: return int(value) if value is not None else None except: return None return { 'st_id': safe_int(data.get('st_id')) or 0, 'k_id': safe_int(data.get('k_id')) or 0, 'asset': data.get('asset', 'USDT'), 'symbol': str(data.get('symbol', '')), 'side': str(data.get('side', '')), 'price': safe_float(data.get('price')), 'sum': safe_float(data.get('qty')), # 注意:这里直接使用sum 'asset_num': safe_float(data.get('asset_num')), 'asset_profit': safe_float(data.get('asset_profit')), 'leverage': safe_int(data.get('leverage')), 'uptime': safe_int(data.get('uptime')), 'profit_price': safe_float(data.get('profit_price')), 'stop_price': safe_float(data.get('stop_price')), 'liquidation_price': safe_float(data.get('liquidation_price')) }