Files
sanguo_vnpy_v2/sanguo_web/api/converter.py
T
claude_dev 918bbed0fc fix: 修复登录500错误和移除明文密码提示
- 修复 deps.py 中 get_vn_service 的引用错误 (vn_service.vn_service -> vn_service)
- 移除登录页面上的明文密码提示
- 改进前端错误处理,避免数据加载失败导致登录显示错误
2026-07-02 12:23:55 +08:00

513 lines
17 KiB
Python

"""
数据转换工具
将 VeighNa 数据对象转换为 API 响应模型
处理日期时间格式化和枚举类型转换
"""
from datetime import datetime
from typing import Optional, List, Any
try:
from vnpy.trader.object import (
TickData, OrderData, TradeData, PositionData,
AccountData, ContractData, QuoteData
)
from vnpy.trader.constant import Direction, Offset, Status, OrderType, Exchange, Product
VNPY_AVAILABLE = True
except ImportError:
VNPY_AVAILABLE = False
from ..models import (
TickData as TickDataModel,
OrderResponse,
PositionData as PositionDataModel,
AccountData as AccountDataModel,
KlineData,
)
# ============================================
# 枚举值映射
# ============================================
# VeighNa Direction (LONG/SHORT) -> API (buy/sell)
DIRECTION_MAP = {
"LONG": "buy",
"SHORT": "sell",
"NET": "net",
}
# VeighNa OrderType -> API
ORDER_TYPE_MAP = {
"限价": "limit",
"LIMIT": "limit",
"市价": "market",
"MARKET": "market",
"STOP": "stop",
"FAK": "fak",
"FOK": "fok",
}
# VeighNa Status -> API
STATUS_MAP = {
"提交中": "submitting",
"SUBMITTING": "submitting",
"未成交": "not_traded",
"NOTTRADED": "not_traded",
"部分成交": "part_traded",
"PARTTRADED": "part_traded",
"全部成交": "all_traded",
"ALLTRADED": "all_traded",
"已撤销": "cancelled",
"CANCELLED": "cancelled",
"拒单": "rejected",
"REJECTED": "rejected",
}
# VeighNa Exchange -> API
EXCHANGE_NAMES = {
"CFFEX": "CFFEX",
"SHFE": "SHFE",
"CZCE": "CZCE",
"DCE": "DCE",
"INE": "INE",
"GFEX": "GFEX",
"SSE": "SSE",
"SZSE": "SZSE",
"BSE": "BSE",
"SMART": "SMART",
"IBKRATS": "IBKRATS",
"LOCAL": "LOCAL",
}
# ============================================
# 辅助函数
# ============================================
def format_datetime(dt: Optional[datetime]) -> Optional[datetime]:
"""格式化日期时间"""
if dt is None:
return None
return dt
def convert_direction(direction: Any) -> str:
"""转换方向枚举"""
if direction is None:
return "unknown"
if isinstance(direction, str):
return DIRECTION_MAP.get(direction, direction.lower())
return DIRECTION_MAP.get(direction.value, "unknown")
def convert_order_type(order_type: Any) -> str:
"""转换订单类型枚举"""
if isinstance(order_type, str):
return ORDER_TYPE_MAP.get(order_type, order_type.lower())
return ORDER_TYPE_MAP.get(order_type.value, "unknown")
def convert_status(status: Any) -> str:
"""转换状态枚举"""
if isinstance(status, str):
return STATUS_MAP.get(status, status.lower())
return STATUS_MAP.get(status.value, "unknown")
def convert_exchange(exchange: Any) -> str:
"""转换交易所枚举"""
if isinstance(exchange, str):
return exchange
return exchange.value if hasattr(exchange, 'value') else str(exchange)
def safe_float(value: Any, default: float = 0.0) -> float:
"""安全转换浮点数"""
try:
return float(value) if value is not None else default
except (TypeError, ValueError):
return default
def safe_int(value: Any, default: int = 0) -> int:
"""安全转换整数"""
try:
return int(value) if value is not None else default
except (TypeError, ValueError):
return default
# ============================================
# TickData 转换
# ============================================
def convert_tick_data(tick: Any) -> TickDataModel:
"""转换 TickData 对象"""
if not VNPY_AVAILABLE:
# 模拟数据转换
return TickDataModel(
symbol=getattr(tick, 'symbol', ''),
exchange=getattr(tick, 'exchange', ''),
datetime=getattr(tick, 'datetime', datetime.now()),
name=getattr(tick, 'name', None),
last_price=safe_float(getattr(tick, 'last_price', 0)),
bid_price_1=safe_float(getattr(tick, 'bid_price_1', None)),
ask_price_1=safe_float(getattr(tick, 'ask_price_1', None)),
bid_volume_1=safe_float(getattr(tick, 'bid_volume_1', None)),
ask_volume_1=safe_float(getattr(tick, 'ask_volume_1', None)),
volume=safe_float(getattr(tick, 'volume', None)),
open_interest=safe_float(getattr(tick, 'open_interest', None)),
)
# VeighNa TickData 转换
return TickDataModel(
symbol=tick.symbol,
exchange=tick.exchange.value,
datetime=tick.datetime,
name=tick.name or None,
last_price=tick.last_price,
bid_price_1=tick.bid_price_1 or None,
ask_price_1=tick.ask_price_1 or None,
bid_volume_1=tick.bid_volume_1 or None,
ask_volume_1=tick.ask_volume_1 or None,
volume=tick.volume or None,
open_interest=tick.open_interest or None,
)
def convert_tick_to_dict(tick: Any) -> dict:
"""转换 TickData 为字典(用于缓存和 WebSocket)"""
if not VNPY_AVAILABLE:
return {
"symbol": getattr(tick, 'symbol', ''),
"exchange": getattr(tick, 'exchange', ''),
"datetime": getattr(tick, 'datetime', datetime.now()).isoformat(),
"name": getattr(tick, 'name', ''),
"last_price": safe_float(getattr(tick, 'last_price', 0)),
"bid_price_1": safe_float(getattr(tick, 'bid_price_1', 0)),
"ask_price_1": safe_float(getattr(tick, 'ask_price_1', 0)),
"bid_volume_1": safe_float(getattr(tick, 'bid_volume_1', 0)),
"ask_volume_1": safe_float(getattr(tick, 'ask_volume_1', 0)),
"volume": safe_float(getattr(tick, 'volume', 0)),
"open_interest": safe_float(getattr(tick, 'open_interest', 0)),
}
return {
"symbol": tick.symbol,
"exchange": tick.exchange.value,
"datetime": tick.datetime.isoformat(),
"name": tick.name,
"last_price": tick.last_price,
"bid_price_1": tick.bid_price_1,
"ask_price_1": tick.ask_price_1,
"bid_volume_1": tick.bid_volume_1,
"ask_volume_1": tick.ask_volume_1,
"volume": tick.volume,
"open_interest": tick.open_interest,
"open_price": tick.open_price,
"high_price": tick.high_price,
"low_price": tick.low_price,
"pre_close": tick.pre_close,
}
# ============================================
# OrderData 转换
# ============================================
def convert_order_data(order: Any) -> OrderResponse:
"""转换 OrderData 对象"""
if not VNPY_AVAILABLE:
return OrderResponse(
order_id=getattr(order, 'vt_orderid', ''),
symbol=getattr(order, 'symbol', ''),
exchange=getattr(order, 'exchange', ''),
direction=convert_direction(getattr(order, 'direction', None)),
order_type=convert_order_type(getattr(order, 'type', None)),
volume=safe_float(getattr(order, 'volume', 0)),
price=safe_float(getattr(order, 'price', None)),
traded=safe_float(getattr(order, 'traded', 0)),
status=convert_status(getattr(order, 'status', None)),
time=format_datetime(getattr(order, 'datetime', None)),
reference=getattr(order, 'reference', None),
)
return OrderResponse(
order_id=order.vt_orderid,
symbol=order.symbol,
exchange=order.exchange.value,
direction=convert_direction(order.direction),
order_type=convert_order_type(order.type),
volume=order.volume,
price=order.price or None,
traded=order.traded,
status=convert_status(order.status),
time=format_datetime(order.datetime),
reference=order.reference or None,
)
def convert_order_to_dict(order: Any) -> dict:
"""转换 OrderData 为字典"""
if not VNPY_AVAILABLE:
return {
"order_id": getattr(order, 'vt_orderid', ''),
"symbol": getattr(order, 'symbol', ''),
"exchange": getattr(order, 'exchange', ''),
"direction": convert_direction(getattr(order, 'direction', None)),
"order_type": convert_order_type(getattr(order, 'type', None)),
"volume": safe_float(getattr(order, 'volume', 0)),
"price": safe_float(getattr(order, 'price', 0)),
"traded": safe_float(getattr(order, 'traded', 0)),
"status": convert_status(getattr(order, 'status', None)),
"time": format_datetime(getattr(order, 'datetime', None)),
"reference": getattr(order, 'reference', ''),
}
return {
"order_id": order.vt_orderid,
"symbol": order.symbol,
"exchange": order.exchange.value,
"direction": convert_direction(order.direction),
"order_type": convert_order_type(order.type),
"offset": order.offset.value if order.offset else "",
"volume": order.volume,
"price": order.price,
"traded": order.traded,
"status": convert_status(order.status),
"time": format_datetime(order.datetime),
"reference": order.reference,
"gateway_name": order.gateway_name,
}
# ============================================
# TradeData 转换
# ============================================
def convert_trade_to_dict(trade: Any) -> dict:
"""转换 TradeData 为字典"""
if not VNPY_AVAILABLE:
return {
"trade_id": getattr(trade, 'vt_tradeid', ''),
"order_id": getattr(trade, 'vt_orderid', ''),
"symbol": getattr(trade, 'symbol', ''),
"exchange": getattr(trade, 'exchange', ''),
"direction": convert_direction(getattr(trade, 'direction', None)),
"offset": getattr(trade, 'offset', ''),
"volume": safe_float(getattr(trade, 'volume', 0)),
"price": safe_float(getattr(trade, 'price', 0)),
"time": format_datetime(getattr(trade, 'datetime', None)),
}
return {
"trade_id": trade.vt_tradeid,
"order_id": trade.vt_orderid,
"symbol": trade.symbol,
"exchange": trade.exchange.value,
"direction": convert_direction(trade.direction),
"offset": trade.offset.value,
"volume": trade.volume,
"price": trade.price,
"time": format_datetime(trade.datetime),
"gateway_name": trade.gateway_name,
}
# ============================================
# PositionData 转换
# ============================================
def convert_position_data(position: Any) -> PositionDataModel:
"""转换 PositionData 对象"""
if not VNPY_AVAILABLE:
return PositionDataModel(
symbol=getattr(position, 'symbol', ''),
exchange=getattr(position, 'exchange', ''),
direction=convert_direction(getattr(position, 'direction', None)),
volume=safe_float(getattr(position, 'volume', 0)),
price=safe_float(getattr(position, 'price', 0)),
pnl=safe_float(getattr(position, 'pnl', 0)),
pnl_ratio=safe_float(getattr(position, 'pnl_ratio', 0)),
frozen=safe_float(getattr(position, 'frozen', 0)),
yd_volume=safe_float(getattr(position, 'yd_volume', 0)),
)
# 计算 pnl_ratio (VeighNa 的 PositionData 没有 pnl_ratio 属性)
pnl_ratio = 0.0
if position.volume and position.price:
try:
pnl_ratio = (position.pnl / (position.volume * position.price * 100)) if position.volume * position.price else 0.0
except (ZeroDivisionError, TypeError):
pnl_ratio = 0.0
return PositionDataModel(
symbol=position.symbol,
exchange=position.exchange.value,
direction=convert_direction(position.direction),
volume=position.volume,
price=position.price,
pnl=position.pnl,
pnl_ratio=pnl_ratio,
frozen=position.frozen,
yd_volume=position.yd_volume,
)
def convert_position_to_dict(position: Any) -> dict:
"""转换 PositionData 为字典"""
if not VNPY_AVAILABLE:
return {
"symbol": getattr(position, 'symbol', ''),
"exchange": getattr(position, 'exchange', ''),
"direction": convert_direction(getattr(position, 'direction', None)),
"volume": safe_float(getattr(position, 'volume', 0)),
"price": safe_float(getattr(position, 'price', 0)),
"pnl": safe_float(getattr(position, 'pnl', 0)),
"pnl_ratio": safe_float(getattr(position, 'pnl_ratio', 0)),
"frozen": safe_float(getattr(position, 'frozen', 0)),
"yd_volume": safe_float(getattr(position, 'yd_volume', 0)),
}
pnl_ratio = 0.0
if position.volume and position.price:
try:
pnl_ratio = (position.pnl / (position.volume * position.price * 100)) if position.volume * position.price else 0.0
except (ZeroDivisionError, TypeError):
pnl_ratio = 0.0
return {
"symbol": position.symbol,
"exchange": position.exchange.value,
"direction": convert_direction(position.direction),
"volume": position.volume,
"price": position.price,
"pnl": position.pnl,
"pnl_ratio": pnl_ratio,
"frozen": position.frozen,
"yd_volume": position.yd_volume,
"gateway_name": position.gateway_name,
}
# ============================================
# AccountData 转换
# ============================================
def convert_account_data(account: Any) -> AccountDataModel:
"""转换 AccountData 对象"""
if not VNPY_AVAILABLE:
return AccountDataModel(
account_id=getattr(account, 'vt_accountid', ''),
balance=safe_float(getattr(account, 'balance', 0)),
available=safe_float(getattr(account, 'available', 0)),
frozen=safe_float(getattr(account, 'frozen', 0)),
margin=safe_float(getattr(account, 'margin', 0)),
)
return AccountDataModel(
account_id=account.vt_accountid,
balance=account.balance,
available=account.available,
frozen=account.frozen,
margin=safe_float(getattr(account, 'margin', 0)),
)
def convert_account_to_dict(account: Any) -> dict:
"""转换 AccountData 为字典"""
if not VNPY_AVAILABLE:
return {
"account_id": getattr(account, 'vt_accountid', ''),
"balance": safe_float(getattr(account, 'balance', 0)),
"available": safe_float(getattr(account, 'available', 0)),
"frozen": safe_float(getattr(account, 'frozen', 0)),
}
return {
"account_id": account.vt_accountid,
"balance": account.balance,
"available": account.available,
"frozen": account.frozen,
"gateway_name": account.gateway_name,
}
# ============================================
# ContractData 转换
# ============================================
def convert_contract_to_dict(contract: Any) -> dict:
"""转换 ContractData 为字典"""
if not VNPY_AVAILABLE:
return {
"symbol": getattr(contract, 'symbol', ''),
"exchange": getattr(contract, 'exchange', ''),
"name": getattr(contract, 'name', ''),
"product": getattr(contract, 'product', ''),
"size": safe_float(getattr(contract, 'size', 1)),
"pricetick": safe_float(getattr(contract, 'pricetick', 0)),
"min_volume": safe_float(getattr(contract, 'min_volume', 1)),
"max_volume": safe_float(getattr(contract, 'max_volume', None)),
"vt_symbol": getattr(contract, 'vt_symbol', ''),
"stop_supported": getattr(contract, 'stop_supported', False),
"net_position": getattr(contract, 'net_position', False),
}
return {
"symbol": contract.symbol,
"exchange": contract.exchange.value,
"name": contract.name,
"product": contract.product.value,
"size": contract.size,
"pricetick": contract.pricetick,
"min_volume": contract.min_volume,
"max_volume": contract.max_volume,
"vt_symbol": contract.vt_symbol,
"stop_supported": contract.stop_supported,
"net_position": contract.net_position,
"history_data": contract.history_data,
}
# ============================================
# 批量转换函数
# ============================================
def convert_tick_list(ticks: List[Any]) -> List[TickDataModel]:
"""批量转换 TickData 列表"""
return [convert_tick_data(tick) for tick in ticks]
def convert_order_list(orders: List[Any]) -> List[OrderResponse]:
"""批量转换 OrderData 列表"""
return [convert_order_data(order) for order in orders]
def convert_position_list(positions: List[Any]) -> List[PositionDataModel]:
"""批量转换 PositionData 列表"""
return [convert_position_data(pos) for pos in positions]
def convert_account_list(accounts: List[Any]) -> List[AccountDataModel]:
"""批量转换 AccountData 列表"""
return [convert_account_data(acc) for acc in accounts]
__all__ = [
"convert_tick_data",
"convert_order_data",
"convert_position_data",
"convert_account_data",
"convert_tick_to_dict",
"convert_order_to_dict",
"convert_trade_to_dict",
"convert_position_to_dict",
"convert_account_to_dict",
"convert_contract_to_dict",
"convert_tick_list",
"convert_order_list",
"convert_position_list",
"convert_account_list",
]