254 lines
11 KiB
Python
254 lines
11 KiB
Python
|
|
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'))
|
|||
|
|
}
|