""" 数据转换工具 将 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", ]