Files
exchange_monitor_sync/utils/batch_position_sync.py
lz_db 803d40b88e 1
2025-12-03 14:40:14 +08:00

254 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'))
}