mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-10 16:58:08 +00:00
feat: 添加 EdgeX 交易所适配器及相关客户端实现,支持订单、深度和K线数据处理
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import math
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from ..internal.async_client import AsyncClient
|
||||
from .types import (
|
||||
CreateOrderParams,
|
||||
CancelOrderParams,
|
||||
GetActiveOrderParams,
|
||||
OrderFillTransactionParams,
|
||||
TimeInForce,
|
||||
OrderType
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
"""Client for order-related API endpoints."""
|
||||
|
||||
def __init__(self, async_client: AsyncClient):
|
||||
"""
|
||||
Initialize the order client.
|
||||
|
||||
Args:
|
||||
async_client: The async client for common functionality
|
||||
"""
|
||||
self.async_client = async_client
|
||||
|
||||
async def create_order(self, params: CreateOrderParams, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new order with the given parameters.
|
||||
|
||||
Args:
|
||||
params: Order parameters
|
||||
metadata: Exchange metadata
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The created order
|
||||
|
||||
Raises:
|
||||
ValueError: If required parameters are missing or invalid
|
||||
"""
|
||||
# Set default TimeInForce based on order type if not specified
|
||||
if not params.time_in_force:
|
||||
if params.type == OrderType.MARKET:
|
||||
params.time_in_force = TimeInForce.IMMEDIATE_OR_CANCEL
|
||||
elif params.type == OrderType.LIMIT:
|
||||
params.time_in_force = TimeInForce.GOOD_TIL_CANCEL
|
||||
|
||||
# Find the contract from metadata
|
||||
contract = None
|
||||
contract_list = metadata.get("contractList", [])
|
||||
for c in contract_list:
|
||||
if c.get("contractId") == params.contract_id:
|
||||
contract = c
|
||||
break
|
||||
|
||||
if not contract:
|
||||
raise ValueError(f"contract not found: {params.contract_id}")
|
||||
|
||||
# Get collateral coin from metadata
|
||||
global_data = metadata.get("global", {})
|
||||
collateral_coin = global_data.get("starkExCollateralCoin", {})
|
||||
|
||||
# Parse decimal values
|
||||
try:
|
||||
size = Decimal(params.size)
|
||||
price = Decimal(params.price)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse size or price")
|
||||
|
||||
# Convert hex resolution to decimal
|
||||
hex_resolution = contract.get("starkExResolution", "0x0")
|
||||
# Remove "0x" prefix if present
|
||||
hex_resolution = hex_resolution.replace("0x", "")
|
||||
# Parse hex string to int
|
||||
try:
|
||||
resolution_int = int(hex_resolution, 16)
|
||||
resolution = Decimal(resolution_int)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse hex resolution")
|
||||
|
||||
client_order_id = params.client_order_id or self.async_client.generate_uuid()
|
||||
|
||||
# Calculate values
|
||||
value_dm = price * size
|
||||
amount_synthetic = int(size * resolution)
|
||||
amount_collateral = int(value_dm * Decimal("1000000")) # Shift 6 decimal places
|
||||
|
||||
# Calculate fee based on order type (maker/taker)
|
||||
try:
|
||||
fee_rate = Decimal(contract.get("defaultTakerFeeRate", "0"))
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError("failed to parse fee rate")
|
||||
|
||||
# Calculate fee amount in decimal with ceiling to integer
|
||||
amount_fee_dm = Decimal(str(math.ceil(float(value_dm * fee_rate))))
|
||||
amount_fee_str = str(amount_fee_dm)
|
||||
|
||||
# Convert to the required integer format for the protocol
|
||||
amount_fee = int(amount_fee_dm * Decimal("1000000")) # Shift 6 decimal places
|
||||
|
||||
nonce = self.async_client.calc_nonce(client_order_id)
|
||||
l2_expire_time = int(time.time() * 1000) + (14 * 24 * 60 * 60 * 1000) # 14 days
|
||||
|
||||
# Calculate signature using asset IDs from metadata
|
||||
expire_time_unix = l2_expire_time // (60 * 60 * 1000)
|
||||
|
||||
sig_hash = self.async_client.calc_limit_order_hash(
|
||||
contract.get("starkExSyntheticAssetId", ""),
|
||||
collateral_coin.get("starkExAssetId", ""),
|
||||
collateral_coin.get("starkExAssetId", ""),
|
||||
params.side.value == "BUY",
|
||||
amount_synthetic,
|
||||
amount_collateral,
|
||||
amount_fee,
|
||||
nonce,
|
||||
self.async_client.get_account_id(),
|
||||
expire_time_unix
|
||||
)
|
||||
|
||||
# Sign the order
|
||||
sig = self.async_client.sign(sig_hash)
|
||||
|
||||
# Convert signature to string (include v component like Go SDK, even though it's empty)
|
||||
sig_str = f"{sig.r}{sig.s}{sig.v if hasattr(sig, 'v') and sig.v else ''}"
|
||||
|
||||
|
||||
|
||||
# Create order request
|
||||
account_id = str(self.async_client.get_account_id())
|
||||
nonce_str = str(nonce)
|
||||
l2_expire_time_str = str(l2_expire_time)
|
||||
expire_time_str = str(l2_expire_time - 864000000) # 10 days earlier
|
||||
value_str = str(value_dm)
|
||||
|
||||
price_str = params.price if params.type == OrderType.LIMIT else "0"
|
||||
|
||||
# Prepare request data
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"contractId": params.contract_id,
|
||||
"price": price_str,
|
||||
"size": params.size,
|
||||
"type": params.type.value, # Use .value to get the string value
|
||||
"timeInForce": params.time_in_force.value, # Use .value to get the string value
|
||||
"side": params.side.value, # Use .value to get the string value
|
||||
"l2Signature": sig_str,
|
||||
"l2Nonce": nonce_str,
|
||||
"l2ExpireTime": l2_expire_time_str,
|
||||
"l2Value": value_str,
|
||||
"l2Size": params.size,
|
||||
"l2LimitFee": amount_fee_str,
|
||||
"clientOrderId": client_order_id,
|
||||
"expireTime": expire_time_str,
|
||||
"reduceOnly": params.reduce_only
|
||||
}
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/order/createOrder",
|
||||
data=request_data
|
||||
)
|
||||
|
||||
async def cancel_order(self, params: CancelOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Cancel a specific order.
|
||||
|
||||
Args:
|
||||
params: Cancel order parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The cancellation result
|
||||
|
||||
Raises:
|
||||
ValueError: If required parameters are missing or invalid
|
||||
"""
|
||||
account_id = str(self.async_client.get_account_id())
|
||||
|
||||
if params.order_id:
|
||||
path = "/api/v1/private/order/cancelOrderById"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"orderIdList": [params.order_id]
|
||||
}
|
||||
elif params.client_id:
|
||||
path = "/api/v1/private/order/cancelOrderByClientOrderId"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"clientOrderIdList": [params.client_id]
|
||||
}
|
||||
elif params.contract_id:
|
||||
path = "/api/v1/private/order/cancelAllOrder"
|
||||
request_data = {
|
||||
"accountId": account_id,
|
||||
"filterContractIdList": [params.contract_id]
|
||||
}
|
||||
else:
|
||||
raise ValueError("must provide either order_id, client_id, or contract_id")
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path=path,
|
||||
data=request_data
|
||||
)
|
||||
|
||||
async def get_active_orders(self, params: GetActiveOrderParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get active orders with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Active order query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The active orders
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build query parameters
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
if params.filter_type_list:
|
||||
query_params["filterTypeList"] = ",".join(params.filter_type_list)
|
||||
if params.filter_status_list:
|
||||
query_params["filterStatusList"] = ",".join(params.filter_status_list)
|
||||
|
||||
# Add boolean filters
|
||||
if params.filter_is_liquidate is not None:
|
||||
query_params["filterIsLiquidateList"] = str(params.filter_is_liquidate).lower()
|
||||
if params.filter_is_deleverage is not None:
|
||||
query_params["filterIsDeleverageList"] = str(params.filter_is_deleverage).lower()
|
||||
if params.filter_is_position_tpsl is not None:
|
||||
query_params["filterIsPositionTpslList"] = str(params.filter_is_position_tpsl).lower()
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/order/getActiveOrderPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_order_fill_transactions(self, params: OrderFillTransactionParams) -> Dict[str, Any]:
|
||||
"""
|
||||
Get order fill transactions with pagination and filters.
|
||||
|
||||
Args:
|
||||
params: Order fill transaction query parameters
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The order fill transactions
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build query parameters
|
||||
query_params = {
|
||||
"accountId": str(self.async_client.get_account_id())
|
||||
}
|
||||
|
||||
# Add pagination parameters
|
||||
if params.size:
|
||||
query_params["size"] = params.size
|
||||
if params.offset_data:
|
||||
query_params["offsetData"] = params.offset_data
|
||||
|
||||
# Add filter parameters
|
||||
if params.filter_coin_id_list:
|
||||
query_params["filterCoinIdList"] = ",".join(params.filter_coin_id_list)
|
||||
if params.filter_contract_id_list:
|
||||
query_params["filterContractIdList"] = ",".join(params.filter_contract_id_list)
|
||||
if params.filter_order_id_list:
|
||||
query_params["filterOrderIdList"] = ",".join(params.filter_order_id_list)
|
||||
|
||||
# Add boolean filters
|
||||
if params.filter_is_liquidate is not None:
|
||||
query_params["filterIsLiquidateList"] = str(params.filter_is_liquidate).lower()
|
||||
if params.filter_is_deleverage is not None:
|
||||
query_params["filterIsDeleverageList"] = str(params.filter_is_deleverage).lower()
|
||||
if params.filter_is_position_tpsl is not None:
|
||||
query_params["filterIsPositionTpslList"] = str(params.filter_is_position_tpsl).lower()
|
||||
|
||||
# Add time filters
|
||||
if params.filter_start_created_time_inclusive > 0:
|
||||
query_params["filterStartCreatedTimeInclusive"] = str(params.filter_start_created_time_inclusive)
|
||||
if params.filter_end_created_time_exclusive > 0:
|
||||
query_params["filterEndCreatedTimeExclusive"] = str(params.filter_end_created_time_exclusive)
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="GET",
|
||||
path="/api/v1/private/order/getHistoryOrderFillTransactionPage",
|
||||
params=query_params
|
||||
)
|
||||
|
||||
async def get_max_order_size(self, contract_id: str, price: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the maximum order size for a given contract and price.
|
||||
|
||||
Args:
|
||||
contract_id: The contract ID
|
||||
price: The price
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The maximum order size information
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
# Build request body (API expects POST with JSON body)
|
||||
data = {
|
||||
"accountId": str(self.async_client.get_account_id()),
|
||||
"contractId": contract_id,
|
||||
"price": str(price)
|
||||
}
|
||||
|
||||
# Execute request using async client
|
||||
return await self.async_client.make_authenticated_request(
|
||||
method="POST",
|
||||
path="/api/v1/private/order/getMaxCreateOrderSize",
|
||||
data=data
|
||||
)
|
||||
Reference in New Issue
Block a user