kaq-quant-common 0.2.11__py3-none-any.whl → 0.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kaq_quant_common/api/rest/api_client_base.py +146 -1
- kaq_quant_common/api/rest/instruction/helper/commission_helper.py +141 -0
- kaq_quant_common/api/rest/instruction/helper/mock_order_helper.py +344 -0
- kaq_quant_common/api/rest/instruction/helper/order_helper.py +1 -1
- kaq_quant_common/api/rest/instruction/instruction_client.py +204 -2
- kaq_quant_common/api/rest/instruction/instruction_server_base.py +54 -0
- kaq_quant_common/api/rest/instruction/models/order.py +1 -1
- kaq_quant_common/common/modules/funding_rate_helper.py +4 -2
- kaq_quant_common/common/modules/limit_order_helper.py +4 -2
- {kaq_quant_common-0.2.11.dist-info → kaq_quant_common-0.2.13.dist-info}/METADATA +1 -1
- {kaq_quant_common-0.2.11.dist-info → kaq_quant_common-0.2.13.dist-info}/RECORD +12 -10
- {kaq_quant_common-0.2.11.dist-info → kaq_quant_common-0.2.13.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
from typing import Optional, Type, TypeVar
|
|
1
|
+
from typing import Optional, Type, TypeVar, Callable
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
2
5
|
|
|
3
6
|
import requests
|
|
7
|
+
try:
|
|
8
|
+
import httpx
|
|
9
|
+
HTTPX_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
HTTPX_AVAILABLE = False
|
|
12
|
+
|
|
4
13
|
from kaq_quant_common.api.common.auth import get_auth_token
|
|
5
14
|
from kaq_quant_common.utils import logger_utils
|
|
6
15
|
from pydantic import BaseModel
|
|
@@ -12,11 +21,42 @@ class ApiClientBase:
|
|
|
12
21
|
"""
|
|
13
22
|
api 客户端
|
|
14
23
|
"""
|
|
24
|
+
|
|
25
|
+
# 类级别的共享 event loop 线程
|
|
26
|
+
_shared_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
27
|
+
_shared_loop_thread: Optional[threading.Thread] = None
|
|
28
|
+
_loop_lock = threading.Lock()
|
|
15
29
|
|
|
16
30
|
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
17
31
|
self._base_url = base_url.rstrip("/")
|
|
18
32
|
self._token = token if token is not None else get_auth_token()
|
|
19
33
|
self._logger = logger_utils.get_logger(self)
|
|
34
|
+
# 异步客户端(懒加载)
|
|
35
|
+
self._async_client: Optional[httpx.AsyncClient] = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _ensure_shared_loop(cls):
|
|
39
|
+
"""确保共享的 event loop 已创建并运行"""
|
|
40
|
+
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
41
|
+
with cls._loop_lock:
|
|
42
|
+
# 双重检查
|
|
43
|
+
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
44
|
+
def run_loop():
|
|
45
|
+
"""在后台线程中运行 event loop"""
|
|
46
|
+
cls._shared_loop = asyncio.new_event_loop()
|
|
47
|
+
asyncio.set_event_loop(cls._shared_loop)
|
|
48
|
+
cls._shared_loop.run_forever()
|
|
49
|
+
|
|
50
|
+
cls._shared_loop_thread = threading.Thread(
|
|
51
|
+
target=run_loop,
|
|
52
|
+
daemon=True,
|
|
53
|
+
name="ApiClient-EventLoop"
|
|
54
|
+
)
|
|
55
|
+
cls._shared_loop_thread.start()
|
|
56
|
+
|
|
57
|
+
# 等待 loop 启动
|
|
58
|
+
while cls._shared_loop is None:
|
|
59
|
+
time.sleep(0.01)
|
|
20
60
|
|
|
21
61
|
# 发送请求
|
|
22
62
|
def _make_request(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
@@ -40,3 +80,108 @@ class ApiClientBase:
|
|
|
40
80
|
except requests.exceptions.RequestException as e:
|
|
41
81
|
self._logger.error(f"An error occurred: {e}")
|
|
42
82
|
raise
|
|
83
|
+
|
|
84
|
+
# 异步发送请求
|
|
85
|
+
async def _make_request_async(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
86
|
+
"""
|
|
87
|
+
异步发送请求
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
method_name: API方法名
|
|
91
|
+
request_data: 请求数据
|
|
92
|
+
response_model: 响应模型
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
响应对象
|
|
96
|
+
"""
|
|
97
|
+
if not HTTPX_AVAILABLE:
|
|
98
|
+
raise RuntimeError("httpx library is required for async requests. Install it with: pip install httpx")
|
|
99
|
+
|
|
100
|
+
url = f"{self._base_url}/api/{method_name}"
|
|
101
|
+
headers = {}
|
|
102
|
+
if self._token:
|
|
103
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
104
|
+
|
|
105
|
+
# 懒加载创建async client
|
|
106
|
+
if self._async_client is None:
|
|
107
|
+
self._async_client = httpx.AsyncClient(timeout=30.0)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# 发送异步post请求
|
|
111
|
+
response = await self._async_client.post(
|
|
112
|
+
url,
|
|
113
|
+
json=request_data.model_dump(),
|
|
114
|
+
headers=headers or None
|
|
115
|
+
)
|
|
116
|
+
# 检查响应状态码
|
|
117
|
+
if not response.is_success:
|
|
118
|
+
try:
|
|
119
|
+
error_data = response.json()
|
|
120
|
+
error_message = error_data.get("error", response.text)
|
|
121
|
+
except ValueError:
|
|
122
|
+
error_message = response.text
|
|
123
|
+
raise httpx.HTTPStatusError(
|
|
124
|
+
f"HTTP error occurred: {response.status_code} - {error_message}",
|
|
125
|
+
request=response.request,
|
|
126
|
+
response=response
|
|
127
|
+
)
|
|
128
|
+
# 返回请求结果
|
|
129
|
+
return response_model(**response.json())
|
|
130
|
+
except httpx.HTTPStatusError:
|
|
131
|
+
raise
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self._logger.error(f"An error occurred: {e}")
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
# 回调方式发送请求
|
|
137
|
+
def _make_request_callback(
|
|
138
|
+
self,
|
|
139
|
+
method_name: str,
|
|
140
|
+
request_data: BaseModel,
|
|
141
|
+
response_model: Type[R],
|
|
142
|
+
on_success: Optional[Callable[[R], None]] = None,
|
|
143
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
回调方式发送请求,使用共享的 event loop
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
method_name: API方法名
|
|
150
|
+
request_data: 请求数据
|
|
151
|
+
response_model: 响应模型
|
|
152
|
+
on_success: 成功回调,接收响应对象,可选
|
|
153
|
+
on_error: 错误回调,接收异常对象,可选
|
|
154
|
+
"""
|
|
155
|
+
# 确保共享 loop 已启动
|
|
156
|
+
self._ensure_shared_loop()
|
|
157
|
+
|
|
158
|
+
async def _async_task():
|
|
159
|
+
"""异步任务"""
|
|
160
|
+
try:
|
|
161
|
+
result = await self._make_request_async(method_name, request_data, response_model)
|
|
162
|
+
# 调用成功回调(如果提供)
|
|
163
|
+
if on_success:
|
|
164
|
+
on_success(result)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
# 调用错误回调
|
|
167
|
+
if on_error:
|
|
168
|
+
on_error(e)
|
|
169
|
+
else:
|
|
170
|
+
self._logger.error(f"Callback request failed: {e}")
|
|
171
|
+
|
|
172
|
+
# 将任务提交到共享的 event loop
|
|
173
|
+
asyncio.run_coroutine_threadsafe(_async_task(), self._shared_loop)
|
|
174
|
+
|
|
175
|
+
async def close_async(self):
|
|
176
|
+
"""关闭异步客户端连接"""
|
|
177
|
+
if self._async_client is not None:
|
|
178
|
+
await self._async_client.aclose()
|
|
179
|
+
self._async_client = None
|
|
180
|
+
|
|
181
|
+
async def __aenter__(self):
|
|
182
|
+
"""异步上下文管理器入口"""
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
186
|
+
"""异步上下文管理器出口"""
|
|
187
|
+
await self.close_async()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from kaq_quant_common.utils import logger_utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommissionHelper:
|
|
9
|
+
"""手续费助手,用于从Redis加载和获取交易对的手续费率"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, ins_server):
|
|
12
|
+
# 必须放在这里 延迟引入,否则会有循环引用问题
|
|
13
|
+
from kaq_quant_common.api.rest.instruction.instruction_server_base import (
|
|
14
|
+
InstructionServerBase,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
self._server: InstructionServerBase = ins_server
|
|
18
|
+
self._logger = logger_utils.get_logger(self)
|
|
19
|
+
|
|
20
|
+
# 手续费数据缓存
|
|
21
|
+
self._commission_data: Optional[pd.DataFrame] = None
|
|
22
|
+
# 是否已加载
|
|
23
|
+
self._loaded = False
|
|
24
|
+
|
|
25
|
+
def _load_commission_rates(self):
|
|
26
|
+
"""从Redis加载手续费率数据"""
|
|
27
|
+
if self._loaded:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
redis = self._server._redis
|
|
31
|
+
if redis is None:
|
|
32
|
+
self._logger.warning("Redis未配置,无法加载手续费率")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
exchange = self._server._exchange
|
|
36
|
+
# 组装Redis key,格式: kaq_{exchange}_futures_commission_rate
|
|
37
|
+
redis_key = f"kaq_{exchange}_futures_commission_rate"
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
self._logger.info(f"Loading commission rates from Redis key: {redis_key}")
|
|
41
|
+
# 从Redis读取list数据
|
|
42
|
+
data_list = redis.lrange(redis_key)
|
|
43
|
+
|
|
44
|
+
if data_list is not None and not data_list.empty:
|
|
45
|
+
self._commission_data = data_list
|
|
46
|
+
self._loaded = True
|
|
47
|
+
self._logger.info(
|
|
48
|
+
f"Successfully loaded {len(self._commission_data)} commission rates"
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
self._logger.warning(f"No commission rate data found in Redis key: {redis_key}")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self._logger.error(f"Failed to load commission rates from Redis: {e}")
|
|
54
|
+
|
|
55
|
+
def get_taker_commission_rate(self, symbol: str, default_rate: float = 0.0005) -> float:
|
|
56
|
+
"""
|
|
57
|
+
获取指定交易对的taker手续费率
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
symbol: 交易对,如 BTCUSDT
|
|
61
|
+
default_rate: 默认手续费率,如果获取失败则使用该值
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
float: taker手续费率
|
|
65
|
+
"""
|
|
66
|
+
# 如果还没加载,先加载
|
|
67
|
+
if not self._loaded:
|
|
68
|
+
self._load_commission_rates()
|
|
69
|
+
|
|
70
|
+
# 如果还是没有数据,返回默认值
|
|
71
|
+
if self._commission_data is None or self._commission_data.empty:
|
|
72
|
+
self._logger.warning(
|
|
73
|
+
f"Commission data not available, using default rate: {default_rate}"
|
|
74
|
+
)
|
|
75
|
+
return default_rate
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# 从DataFrame中查找对应的symbol
|
|
79
|
+
matched = self._commission_data[self._commission_data["symbol"] == symbol]
|
|
80
|
+
|
|
81
|
+
if not matched.empty:
|
|
82
|
+
rate = float(matched.iloc[0]["takerCommissionRate"])
|
|
83
|
+
self._logger.debug(f"Found taker commission rate for {symbol}: {rate}")
|
|
84
|
+
return rate
|
|
85
|
+
else:
|
|
86
|
+
self._logger.warning(
|
|
87
|
+
f"Symbol {symbol} not found in commission data, using default rate: {default_rate}"
|
|
88
|
+
)
|
|
89
|
+
return default_rate
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self._logger.error(
|
|
92
|
+
f"Error getting commission rate for {symbol}: {e}, using default rate: {default_rate}"
|
|
93
|
+
)
|
|
94
|
+
return default_rate
|
|
95
|
+
|
|
96
|
+
def get_maker_commission_rate(self, symbol: str, default_rate: float = 0.0002) -> float:
|
|
97
|
+
"""
|
|
98
|
+
获取指定交易对的maker手续费率
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
symbol: 交易对,如 BTCUSDT
|
|
102
|
+
default_rate: 默认手续费率,如果获取失败则使用该值
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
float: maker手续费率
|
|
106
|
+
"""
|
|
107
|
+
# 如果还没加载,先加载
|
|
108
|
+
if not self._loaded:
|
|
109
|
+
self._load_commission_rates()
|
|
110
|
+
|
|
111
|
+
# 如果还是没有数据,返回默认值
|
|
112
|
+
if self._commission_data is None or self._commission_data.empty:
|
|
113
|
+
self._logger.warning(
|
|
114
|
+
f"Commission data not available, using default rate: {default_rate}"
|
|
115
|
+
)
|
|
116
|
+
return default_rate
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# 从DataFrame中查找对应的symbol
|
|
120
|
+
matched = self._commission_data[self._commission_data["symbol"] == symbol]
|
|
121
|
+
|
|
122
|
+
if not matched.empty:
|
|
123
|
+
rate = float(matched.iloc[0]["makerCommissionRate"])
|
|
124
|
+
self._logger.debug(f"Found maker commission rate for {symbol}: {rate}")
|
|
125
|
+
return rate
|
|
126
|
+
else:
|
|
127
|
+
self._logger.warning(
|
|
128
|
+
f"Symbol {symbol} not found in commission data, using default rate: {default_rate}"
|
|
129
|
+
)
|
|
130
|
+
return default_rate
|
|
131
|
+
except Exception as e:
|
|
132
|
+
self._logger.error(
|
|
133
|
+
f"Error getting commission rate for {symbol}: {e}, using default rate: {default_rate}"
|
|
134
|
+
)
|
|
135
|
+
return default_rate
|
|
136
|
+
|
|
137
|
+
def reload(self):
|
|
138
|
+
"""重新加载手续费率数据"""
|
|
139
|
+
self._loaded = False
|
|
140
|
+
self._commission_data = None
|
|
141
|
+
self._load_commission_rates()
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from kaq_quant_common.api.rest.instruction.models.order import (
|
|
6
|
+
OrderInfo,
|
|
7
|
+
OrderSide,
|
|
8
|
+
OrderStatus,
|
|
9
|
+
PositionStatus,
|
|
10
|
+
)
|
|
11
|
+
from kaq_quant_common.api.rest.instruction.models.position import PositionSide
|
|
12
|
+
from kaq_quant_common.utils import logger_utils, uuid_utils
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MockOrderHelper:
|
|
16
|
+
"""模拟订单助手,用于模拟下单流程,不调用真实交易所API"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, ins_server):
|
|
19
|
+
# 必须放在这里 延迟引入,否则会有循环引用问题
|
|
20
|
+
from kaq_quant_common.api.rest.instruction.instruction_server_base import (
|
|
21
|
+
InstructionServerBase,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
self._server: InstructionServerBase = ins_server
|
|
25
|
+
self._logger = logger_utils.get_logger(self)
|
|
26
|
+
|
|
27
|
+
self._mysql_table_name_order = "kaq_futures_instruction_order"
|
|
28
|
+
self._mysql_table_name_position = "kaq_futures_instruction_position"
|
|
29
|
+
# 当前持仓
|
|
30
|
+
self._redis_key_position = "kaq_futures_instruction_position"
|
|
31
|
+
# 持仓历史
|
|
32
|
+
self._redis_key_position_history = "kaq_futures_instruction_position_history"
|
|
33
|
+
|
|
34
|
+
def _write_position_open_to_redis(
|
|
35
|
+
self,
|
|
36
|
+
position_id: str,
|
|
37
|
+
exchange: str,
|
|
38
|
+
symbol: str,
|
|
39
|
+
position_side,
|
|
40
|
+
coin_quantity: float,
|
|
41
|
+
usdt_quantity: float,
|
|
42
|
+
open_ins_id: str,
|
|
43
|
+
open_price: float,
|
|
44
|
+
open_fee: float,
|
|
45
|
+
open_fee_rate: float,
|
|
46
|
+
open_time: int,
|
|
47
|
+
):
|
|
48
|
+
redis = self._server._redis
|
|
49
|
+
if redis is None:
|
|
50
|
+
return
|
|
51
|
+
data = {
|
|
52
|
+
"id": position_id,
|
|
53
|
+
"exchange": exchange,
|
|
54
|
+
"symbol": symbol,
|
|
55
|
+
"position_side": position_side.value,
|
|
56
|
+
"coin_quantity": coin_quantity,
|
|
57
|
+
"usdt_quantity": usdt_quantity,
|
|
58
|
+
"open_ins_id": open_ins_id,
|
|
59
|
+
"open_price": open_price,
|
|
60
|
+
"open_fee": open_fee,
|
|
61
|
+
"open_fee_rate": open_fee_rate,
|
|
62
|
+
"open_time": open_time,
|
|
63
|
+
"close_ins_id": None,
|
|
64
|
+
"close_price": 0,
|
|
65
|
+
"close_time": 0,
|
|
66
|
+
"status": PositionStatus.OPEN.value,
|
|
67
|
+
# 标识模拟
|
|
68
|
+
"is_mock": True,
|
|
69
|
+
}
|
|
70
|
+
redis.client.hset(self._redis_key_position, position_id, json.dumps(data))
|
|
71
|
+
|
|
72
|
+
def _write_position_close_to_redis(
|
|
73
|
+
self,
|
|
74
|
+
position_id: str,
|
|
75
|
+
exchange: str,
|
|
76
|
+
symbol: str,
|
|
77
|
+
position_side,
|
|
78
|
+
coin_quantity: float,
|
|
79
|
+
usdt_quantity: float,
|
|
80
|
+
open_ins_id: str,
|
|
81
|
+
open_price: float,
|
|
82
|
+
open_fee: float,
|
|
83
|
+
open_fee_rate: float,
|
|
84
|
+
open_time: int,
|
|
85
|
+
close_ins_id: str,
|
|
86
|
+
close_price: float,
|
|
87
|
+
close_fee: float,
|
|
88
|
+
close_fee_rate: float,
|
|
89
|
+
close_time: int,
|
|
90
|
+
):
|
|
91
|
+
redis = self._server._redis
|
|
92
|
+
if redis is None:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# 先从 redis 读取现有的 position 数据,获取 funding_rate_records 字段
|
|
96
|
+
funding_rate_records = None
|
|
97
|
+
try:
|
|
98
|
+
existing_position_json = redis.client.hget(
|
|
99
|
+
self._redis_key_position, position_id
|
|
100
|
+
)
|
|
101
|
+
if existing_position_json:
|
|
102
|
+
existing_position = json.loads(existing_position_json)
|
|
103
|
+
if existing_position and "funding_rate_records" in existing_position:
|
|
104
|
+
funding_rate_records = existing_position.get(
|
|
105
|
+
"funding_rate_records"
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
# 读取失败不影响后续流程,记录日志
|
|
109
|
+
self._logger.warning(
|
|
110
|
+
f"Failed to get funding_rate_records for position {position_id}: {e}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
data = {
|
|
114
|
+
"id": position_id,
|
|
115
|
+
"exchange": exchange,
|
|
116
|
+
"symbol": symbol,
|
|
117
|
+
"position_side": position_side.value,
|
|
118
|
+
"coin_quantity": coin_quantity,
|
|
119
|
+
"usdt_quantity": usdt_quantity,
|
|
120
|
+
"open_ins_id": open_ins_id,
|
|
121
|
+
"open_price": open_price,
|
|
122
|
+
"open_fee": open_fee,
|
|
123
|
+
"open_fee_rate": open_fee_rate,
|
|
124
|
+
"open_time": open_time,
|
|
125
|
+
"close_ins_id": close_ins_id,
|
|
126
|
+
"close_price": close_price,
|
|
127
|
+
"close_fee": close_fee,
|
|
128
|
+
"close_fee_rate": close_fee_rate,
|
|
129
|
+
"close_time": close_time,
|
|
130
|
+
"status": PositionStatus.CLOSE.value,
|
|
131
|
+
# 标识模拟
|
|
132
|
+
"is_mock": True,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# 如果存在 funding_rate_records,添加到 data 中
|
|
136
|
+
if funding_rate_records is not None:
|
|
137
|
+
data["funding_rate_records"] = funding_rate_records
|
|
138
|
+
|
|
139
|
+
redis.client.hdel(self._redis_key_position, position_id)
|
|
140
|
+
redis.client.rpush(self._redis_key_position_history, json.dumps(data))
|
|
141
|
+
|
|
142
|
+
def process_order(
|
|
143
|
+
self,
|
|
144
|
+
order: OrderInfo,
|
|
145
|
+
mock_fill_price: Optional[float] = None,
|
|
146
|
+
mock_fee_rate: float = 0.0005,
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
处理模拟订单
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
order: 订单信息
|
|
153
|
+
mock_fill_price: 模拟成交价格,如果为None则使用订单的target_price
|
|
154
|
+
mock_fee_rate: 模拟手续费率,默认0.05%
|
|
155
|
+
"""
|
|
156
|
+
# 获取交易所
|
|
157
|
+
exchange = self._server._exchange
|
|
158
|
+
|
|
159
|
+
# 记录开始时间
|
|
160
|
+
start_time = time.time()
|
|
161
|
+
|
|
162
|
+
# 执行模拟订单处理
|
|
163
|
+
self._do_process_mock_order(exchange, order, mock_fill_price, mock_fee_rate, start_time)
|
|
164
|
+
|
|
165
|
+
def _do_process_mock_order(
|
|
166
|
+
self,
|
|
167
|
+
exchange: str,
|
|
168
|
+
order: OrderInfo,
|
|
169
|
+
mock_fill_price: Optional[float],
|
|
170
|
+
mock_fee_rate: float,
|
|
171
|
+
start_time: float,
|
|
172
|
+
):
|
|
173
|
+
# 获取mysql
|
|
174
|
+
mysql = self._server._mysql
|
|
175
|
+
|
|
176
|
+
ins_id = order.instruction_id
|
|
177
|
+
order_id = order.order_id
|
|
178
|
+
symbol = order.symbol
|
|
179
|
+
side = order.side
|
|
180
|
+
position_side = order.position_side
|
|
181
|
+
|
|
182
|
+
is_open = True
|
|
183
|
+
side_str = "开仓"
|
|
184
|
+
if position_side == PositionSide.LONG:
|
|
185
|
+
# 多单是正向理解的
|
|
186
|
+
if side == OrderSide.SELL:
|
|
187
|
+
side_str = "平仓"
|
|
188
|
+
is_open = False
|
|
189
|
+
else:
|
|
190
|
+
side_str = "开仓"
|
|
191
|
+
is_open = True
|
|
192
|
+
else:
|
|
193
|
+
# 空单是反向理解的
|
|
194
|
+
if side == OrderSide.SELL:
|
|
195
|
+
side_str = "开仓"
|
|
196
|
+
is_open = True
|
|
197
|
+
else:
|
|
198
|
+
side_str = "平仓"
|
|
199
|
+
is_open = False
|
|
200
|
+
|
|
201
|
+
self._logger.info(
|
|
202
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 1. {side_str}模拟挂单 {order_id}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# 步骤1.挂单成功 插入到订单记录
|
|
206
|
+
current_time = int(time.time() * 1000)
|
|
207
|
+
|
|
208
|
+
if mysql is not None:
|
|
209
|
+
status = OrderStatus.CREATE
|
|
210
|
+
sql = f"""
|
|
211
|
+
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time, is_mock)
|
|
212
|
+
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time}, 1 );
|
|
213
|
+
"""
|
|
214
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
215
|
+
|
|
216
|
+
# 步骤2.模拟成交 - 直接使用模拟价格
|
|
217
|
+
# 如果没有指定模拟成交价,使用订单的当前价格
|
|
218
|
+
avg_price = mock_fill_price if mock_fill_price is not None else order.current_price or order.target_price
|
|
219
|
+
# 成交数量就是订单数量
|
|
220
|
+
executed_qty = order.quantity
|
|
221
|
+
# 计算出usdt数量
|
|
222
|
+
executed_usdt = avg_price * executed_qty
|
|
223
|
+
# 计算手续费
|
|
224
|
+
fee = executed_usdt * mock_fee_rate
|
|
225
|
+
# 费率
|
|
226
|
+
fee_rate = mock_fee_rate
|
|
227
|
+
|
|
228
|
+
# 模拟处理时间(可以立即完成)
|
|
229
|
+
end_time = time.time()
|
|
230
|
+
cost_time = end_time - start_time
|
|
231
|
+
|
|
232
|
+
self._logger.info(
|
|
233
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. {side_str}模拟订单 {order_id} 成交 耗时 {int(cost_time * 1000)}ms"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# 步骤3.把最终持仓写进去
|
|
237
|
+
current_time = int(time.time() * 1000)
|
|
238
|
+
|
|
239
|
+
if mysql is None:
|
|
240
|
+
self._logger.warning(
|
|
241
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} 仅操作,没有入库,请设置 mysql!!"
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
status = OrderStatus.FINISH
|
|
246
|
+
# 更新写入最终信息
|
|
247
|
+
sql = f"""
|
|
248
|
+
UPDATE {self._mysql_table_name_order}
|
|
249
|
+
SET price = {avg_price}, coin_quantity = {executed_qty}, usdt_quantity = {executed_usdt}, fee = {fee}, fee_rate = {fee_rate}, status = '{status.value}', last_update_time = {current_time}
|
|
250
|
+
WHERE ins_id = '{ins_id}' AND exchange = '{exchange}' AND symbol = '{symbol}';
|
|
251
|
+
"""
|
|
252
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
253
|
+
|
|
254
|
+
self._logger.info(
|
|
255
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 2. 模拟订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if is_open:
|
|
259
|
+
# 同时插入持仓表
|
|
260
|
+
position_id = uuid_utils.generate_uuid()
|
|
261
|
+
sql = f"""
|
|
262
|
+
INSERT INTO {self._mysql_table_name_position} (id, exchange, symbol, position_side, coin_quantity, usdt_quantity, open_ins_id, open_price, open_fee, open_fee_rate, open_time, status, is_mock)
|
|
263
|
+
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}', 1 );
|
|
264
|
+
"""
|
|
265
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
266
|
+
|
|
267
|
+
self._logger.info(
|
|
268
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 创建持仓记录 {position_id}"
|
|
269
|
+
)
|
|
270
|
+
try:
|
|
271
|
+
self._write_position_open_to_redis(
|
|
272
|
+
position_id,
|
|
273
|
+
exchange,
|
|
274
|
+
symbol,
|
|
275
|
+
position_side,
|
|
276
|
+
executed_qty,
|
|
277
|
+
executed_usdt,
|
|
278
|
+
ins_id,
|
|
279
|
+
avg_price,
|
|
280
|
+
fee,
|
|
281
|
+
fee_rate,
|
|
282
|
+
current_time,
|
|
283
|
+
)
|
|
284
|
+
except:
|
|
285
|
+
pass
|
|
286
|
+
else:
|
|
287
|
+
# 需要找到对应的持仓记录
|
|
288
|
+
sql = f"""
|
|
289
|
+
SELECT * FROM {self._mysql_table_name_position}
|
|
290
|
+
WHERE exchange = '{exchange}' AND symbol = '{symbol}' AND position_side = '{position_side.value}' AND status = '{PositionStatus.OPEN.value}' AND is_mock = 1
|
|
291
|
+
ORDER BY open_time ASC;
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
# 如果有指定仓位id,就用指定的
|
|
295
|
+
if hasattr(order, "position_id") and order.position_id:
|
|
296
|
+
sql = f"""
|
|
297
|
+
SELECT * FROM {self._mysql_table_name_position}
|
|
298
|
+
WHERE id = '{order.position_id}' AND status = '{PositionStatus.OPEN.value}' AND is_mock = 1
|
|
299
|
+
"""
|
|
300
|
+
self._logger.info(
|
|
301
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} get position by id {order.position_id}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
execute_ret = mysql.execute_sql(sql)
|
|
305
|
+
try:
|
|
306
|
+
row = execute_ret.fetchone()
|
|
307
|
+
position_id = row.id
|
|
308
|
+
if position_id is not None:
|
|
309
|
+
# 更新持仓信息
|
|
310
|
+
sql = f"""
|
|
311
|
+
UPDATE {self._mysql_table_name_position}
|
|
312
|
+
SET close_ins_id = '{ins_id}', close_price = {avg_price}, close_fee = '{fee}', close_fee_rate = '{fee_rate}', close_time = {current_time}, status = '{PositionStatus.CLOSE.value}'
|
|
313
|
+
WHERE id = '{position_id}';
|
|
314
|
+
"""
|
|
315
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
316
|
+
|
|
317
|
+
self._logger.info(
|
|
318
|
+
f"[MOCK] {ins_id}_{exchange}_{symbol} step 3. 更新持仓记录 {position_id}"
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
self._write_position_close_to_redis(
|
|
322
|
+
position_id,
|
|
323
|
+
exchange,
|
|
324
|
+
symbol,
|
|
325
|
+
position_side,
|
|
326
|
+
float(row.coin_quantity),
|
|
327
|
+
float(row.usdt_quantity),
|
|
328
|
+
row.open_ins_id,
|
|
329
|
+
float(row.open_price),
|
|
330
|
+
float(row.open_fee),
|
|
331
|
+
float(row.open_fee_rate),
|
|
332
|
+
int(row.open_time),
|
|
333
|
+
ins_id,
|
|
334
|
+
avg_price,
|
|
335
|
+
fee,
|
|
336
|
+
fee_rate,
|
|
337
|
+
current_time,
|
|
338
|
+
)
|
|
339
|
+
except:
|
|
340
|
+
pass
|
|
341
|
+
except:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
return True
|
|
@@ -194,7 +194,7 @@ class OrderHelper:
|
|
|
194
194
|
status = OrderStatus.CREATE
|
|
195
195
|
sql = f"""
|
|
196
196
|
INSERT INTO {self._mysql_table_name_order} (ins_id, exchange, symbol, side, position_side, orig_price, orig_coin_quantity, order_id, status, create_time, last_update_time)
|
|
197
|
-
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', {order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time} );
|
|
197
|
+
VALUES ( '{ins_id}', '{exchange}', '{symbol}', '{side.value}', '{order.position_side.value}', {order.current_price or order.target_price}, {order.quantity}, '{order_id}', '{status.value}', {current_time}, {current_time} );
|
|
198
198
|
"""
|
|
199
199
|
execute_ret = mysql.execute_sql(sql, True)
|
|
200
200
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 定义 客户端
|
|
2
2
|
import time
|
|
3
|
-
from typing import Type, TypeVar
|
|
3
|
+
from typing import Type, TypeVar, Callable, Optional
|
|
4
4
|
|
|
5
5
|
from kaq_quant_common.api.rest.api_client_base import ApiClientBase
|
|
6
6
|
from kaq_quant_common.api.rest.instruction.models import (
|
|
@@ -41,6 +41,10 @@ class InstructionClient(ApiClientBase):
|
|
|
41
41
|
def order(self, request: OrderRequest) -> OrderResponse:
|
|
42
42
|
return self._make_request("order", request, OrderResponse)
|
|
43
43
|
|
|
44
|
+
# 模拟下单
|
|
45
|
+
def mock_order(self, request: OrderRequest) -> OrderResponse:
|
|
46
|
+
return self._make_request("mock_order", request, OrderResponse)
|
|
47
|
+
|
|
44
48
|
# 修改订单
|
|
45
49
|
def modify_order(self, request: ModifyOrderRequest) -> ModifyOrderResponse:
|
|
46
50
|
return self._make_request("modify_order", request, ModifyOrderResponse)
|
|
@@ -83,4 +87,202 @@ class InstructionClient(ApiClientBase):
|
|
|
83
87
|
|
|
84
88
|
# 查询未平仓合约数量
|
|
85
89
|
def query_open_interest(self, request: QueryOpenInterestRequest) -> QueryOpenInterestResponse:
|
|
86
|
-
return self._make_request("query_open_interest", request, QueryOpenInterestResponse)
|
|
90
|
+
return self._make_request("query_open_interest", request, QueryOpenInterestResponse)
|
|
91
|
+
|
|
92
|
+
# ==================== 异步方法 ====================
|
|
93
|
+
|
|
94
|
+
async def _make_request_async(self, method: str, request: InstructionRequestBase, response_model: Type[R]) -> R:
|
|
95
|
+
"""异步版本的_make_request,处理公用字段"""
|
|
96
|
+
# 处理公用字段
|
|
97
|
+
if request.event_time is None:
|
|
98
|
+
request.event_time = int(time.time() * 1000)
|
|
99
|
+
if request.task_id is None:
|
|
100
|
+
request.task_id = f"t_{uuid_utils.generate_uuid()}"
|
|
101
|
+
return await super()._make_request_async(method, request, response_model)
|
|
102
|
+
|
|
103
|
+
# 异步下单
|
|
104
|
+
async def order_async(self, request: OrderRequest) -> OrderResponse:
|
|
105
|
+
return await self._make_request_async("order", request, OrderResponse)
|
|
106
|
+
|
|
107
|
+
# 异步模拟下单
|
|
108
|
+
async def mock_order_async(self, request: OrderRequest) -> OrderResponse:
|
|
109
|
+
return await self._make_request_async("mock_order", request, OrderResponse)
|
|
110
|
+
|
|
111
|
+
# 异步修改订单
|
|
112
|
+
async def modify_order_async(self, request: ModifyOrderRequest) -> ModifyOrderResponse:
|
|
113
|
+
return await self._make_request_async("modify_order", request, ModifyOrderResponse)
|
|
114
|
+
|
|
115
|
+
# 异步取消订单
|
|
116
|
+
async def cancel_order_async(self, request: CancelOrderRequest) -> CancelOrderResponse:
|
|
117
|
+
return await self._make_request_async("cancel_order", request, CancelOrderResponse)
|
|
118
|
+
|
|
119
|
+
# 异步查询当前全部挂单
|
|
120
|
+
async def all_open_orders_async(self, request: AllOpenOrdersRequest) -> AllOpenOrdersResponse:
|
|
121
|
+
return await self._make_request_async("all_open_orders", request, AllOpenOrdersResponse)
|
|
122
|
+
|
|
123
|
+
# 异步查询交易对设置
|
|
124
|
+
async def query_symbol_config_async(self, request: QuerySymbolConfigRequest) -> QuerySymbolConfigResponse:
|
|
125
|
+
return await self._make_request_async("query_symbol_config", request, QuerySymbolConfigResponse)
|
|
126
|
+
|
|
127
|
+
# 异步调整杠杆
|
|
128
|
+
async def change_leverage_async(self, request: ChangeLeverageRequest) -> ChangeLeverageResponse:
|
|
129
|
+
return await self._make_request_async("change_leverage", request, ChangeLeverageResponse)
|
|
130
|
+
|
|
131
|
+
# 异步查询持仓
|
|
132
|
+
async def query_position_async(self, request: QueryPositionRequest) -> QueryPositionResponse:
|
|
133
|
+
return await self._make_request_async("query_position", request, QueryPositionResponse)
|
|
134
|
+
|
|
135
|
+
# 异步划转
|
|
136
|
+
async def transfer_async(self, request: TransferRequest) -> TransferResponse:
|
|
137
|
+
return await self._make_request_async("transfer", request, TransferResponse)
|
|
138
|
+
|
|
139
|
+
# 异步查询合约账户余额
|
|
140
|
+
async def contract_balance_async(self, request: ContractBalanceRequest) -> ContractBalanceResponse:
|
|
141
|
+
return await self._make_request_async("contract_balance", request, ContractBalanceResponse)
|
|
142
|
+
|
|
143
|
+
# 异步查询5档深度
|
|
144
|
+
async def get_limit_order_async(self, request: LimitOrderBookRequest) -> LimitOrderBookResponse:
|
|
145
|
+
return await self._make_request_async("get_limit_order", request, LimitOrderBookResponse)
|
|
146
|
+
|
|
147
|
+
# 异步查询账户损益资金流水
|
|
148
|
+
async def get_account_income_async(self, request: AccountIncomeRequest) -> AccountIncomeResponse:
|
|
149
|
+
return await self._make_request_async("get_account_income", request, AccountIncomeResponse)
|
|
150
|
+
|
|
151
|
+
# 异步查询未平仓合约数量
|
|
152
|
+
async def query_open_interest_async(self, request: QueryOpenInterestRequest) -> QueryOpenInterestResponse:
|
|
153
|
+
return await self._make_request_async("query_open_interest", request, QueryOpenInterestResponse)
|
|
154
|
+
|
|
155
|
+
# ==================== 回调方法 ====================
|
|
156
|
+
|
|
157
|
+
def _make_request_callback(
|
|
158
|
+
self,
|
|
159
|
+
method: str,
|
|
160
|
+
request: InstructionRequestBase,
|
|
161
|
+
response_model: Type[R],
|
|
162
|
+
on_success: Optional[Callable[[R], None]] = None,
|
|
163
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
164
|
+
) -> None:
|
|
165
|
+
"""回调版本的_make_request,处理公用字段"""
|
|
166
|
+
# 处理公用字段
|
|
167
|
+
if request.event_time is None:
|
|
168
|
+
request.event_time = int(time.time() * 1000)
|
|
169
|
+
if request.task_id is None:
|
|
170
|
+
request.task_id = f"t_{uuid_utils.generate_uuid()}"
|
|
171
|
+
super()._make_request_callback(method, request, response_model, on_success, on_error)
|
|
172
|
+
|
|
173
|
+
# 回调下单
|
|
174
|
+
def order_callback(
|
|
175
|
+
self,
|
|
176
|
+
request: OrderRequest,
|
|
177
|
+
on_success: Optional[Callable[[OrderResponse], None]] = None,
|
|
178
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
179
|
+
) -> None:
|
|
180
|
+
self._make_request_callback("order", request, OrderResponse, on_success, on_error)
|
|
181
|
+
|
|
182
|
+
# 回调模拟下单
|
|
183
|
+
def mock_order_callback(
|
|
184
|
+
self,
|
|
185
|
+
request: OrderRequest,
|
|
186
|
+
on_success: Optional[Callable[[OrderResponse], None]] = None,
|
|
187
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
188
|
+
) -> None:
|
|
189
|
+
self._make_request_callback("mock_order", request, OrderResponse, on_success, on_error)
|
|
190
|
+
|
|
191
|
+
# 回调修改订单
|
|
192
|
+
def modify_order_callback(
|
|
193
|
+
self,
|
|
194
|
+
request: ModifyOrderRequest,
|
|
195
|
+
on_success: Optional[Callable[[ModifyOrderResponse], None]] = None,
|
|
196
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
197
|
+
) -> None:
|
|
198
|
+
self._make_request_callback("modify_order", request, ModifyOrderResponse, on_success, on_error)
|
|
199
|
+
|
|
200
|
+
# 回调取消订单
|
|
201
|
+
def cancel_order_callback(
|
|
202
|
+
self,
|
|
203
|
+
request: CancelOrderRequest,
|
|
204
|
+
on_success: Optional[Callable[[CancelOrderResponse], None]] = None,
|
|
205
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
206
|
+
) -> None:
|
|
207
|
+
self._make_request_callback("cancel_order", request, CancelOrderResponse, on_success, on_error)
|
|
208
|
+
|
|
209
|
+
# 回调查询当前全部挂单
|
|
210
|
+
def all_open_orders_callback(
|
|
211
|
+
self,
|
|
212
|
+
request: AllOpenOrdersRequest,
|
|
213
|
+
on_success: Optional[Callable[[AllOpenOrdersResponse], None]] = None,
|
|
214
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
215
|
+
) -> None:
|
|
216
|
+
self._make_request_callback("all_open_orders", request, AllOpenOrdersResponse, on_success, on_error)
|
|
217
|
+
|
|
218
|
+
# 回调查询交易对设置
|
|
219
|
+
def query_symbol_config_callback(
|
|
220
|
+
self,
|
|
221
|
+
request: QuerySymbolConfigRequest,
|
|
222
|
+
on_success: Optional[Callable[[QuerySymbolConfigResponse], None]] = None,
|
|
223
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
224
|
+
) -> None:
|
|
225
|
+
self._make_request_callback("query_symbol_config", request, QuerySymbolConfigResponse, on_success, on_error)
|
|
226
|
+
|
|
227
|
+
# 回调调整杠杆
|
|
228
|
+
def change_leverage_callback(
|
|
229
|
+
self,
|
|
230
|
+
request: ChangeLeverageRequest,
|
|
231
|
+
on_success: Optional[Callable[[ChangeLeverageResponse], None]] = None,
|
|
232
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
233
|
+
) -> None:
|
|
234
|
+
self._make_request_callback("change_leverage", request, ChangeLeverageResponse, on_success, on_error)
|
|
235
|
+
|
|
236
|
+
# 回调查询持仓
|
|
237
|
+
def query_position_callback(
|
|
238
|
+
self,
|
|
239
|
+
request: QueryPositionRequest,
|
|
240
|
+
on_success: Optional[Callable[[QueryPositionResponse], None]] = None,
|
|
241
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
242
|
+
) -> None:
|
|
243
|
+
self._make_request_callback("query_position", request, QueryPositionResponse, on_success, on_error)
|
|
244
|
+
|
|
245
|
+
# 回调划转
|
|
246
|
+
def transfer_callback(
|
|
247
|
+
self,
|
|
248
|
+
request: TransferRequest,
|
|
249
|
+
on_success: Optional[Callable[[TransferResponse], None]] = None,
|
|
250
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
251
|
+
) -> None:
|
|
252
|
+
self._make_request_callback("transfer", request, TransferResponse, on_success, on_error)
|
|
253
|
+
|
|
254
|
+
# 回调查询合约账户余额
|
|
255
|
+
def contract_balance_callback(
|
|
256
|
+
self,
|
|
257
|
+
request: ContractBalanceRequest,
|
|
258
|
+
on_success: Optional[Callable[[ContractBalanceResponse], None]] = None,
|
|
259
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
260
|
+
) -> None:
|
|
261
|
+
self._make_request_callback("contract_balance", request, ContractBalanceResponse, on_success, on_error)
|
|
262
|
+
|
|
263
|
+
# 回调查询5档深度
|
|
264
|
+
def get_limit_order_callback(
|
|
265
|
+
self,
|
|
266
|
+
request: LimitOrderBookRequest,
|
|
267
|
+
on_success: Optional[Callable[[LimitOrderBookResponse], None]] = None,
|
|
268
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
269
|
+
) -> None:
|
|
270
|
+
self._make_request_callback("get_limit_order", request, LimitOrderBookResponse, on_success, on_error)
|
|
271
|
+
|
|
272
|
+
# 回调查询账户损益资金流水
|
|
273
|
+
def get_account_income_callback(
|
|
274
|
+
self,
|
|
275
|
+
request: AccountIncomeRequest,
|
|
276
|
+
on_success: Optional[Callable[[AccountIncomeResponse], None]] = None,
|
|
277
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
278
|
+
) -> None:
|
|
279
|
+
self._make_request_callback("get_account_income", request, AccountIncomeResponse, on_success, on_error)
|
|
280
|
+
|
|
281
|
+
# 回调查询未平仓合约数量
|
|
282
|
+
def query_open_interest_callback(
|
|
283
|
+
self,
|
|
284
|
+
request: QueryOpenInterestRequest,
|
|
285
|
+
on_success: Optional[Callable[[QueryOpenInterestResponse], None]] = None,
|
|
286
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
287
|
+
) -> None:
|
|
288
|
+
self._make_request_callback("query_open_interest", request, QueryOpenInterestResponse, on_success, on_error)
|
|
@@ -6,6 +6,10 @@ from kaq_quant_common.api.common.api_interface import ApiInterface, api_method
|
|
|
6
6
|
from kaq_quant_common.api.rest.api_server_base import ApiServerBase
|
|
7
7
|
from kaq_quant_common.api.rest.instruction.helper.order_helper import \
|
|
8
8
|
OrderHelper
|
|
9
|
+
from kaq_quant_common.api.rest.instruction.helper.mock_order_helper import \
|
|
10
|
+
MockOrderHelper
|
|
11
|
+
from kaq_quant_common.api.rest.instruction.helper.commission_helper import \
|
|
12
|
+
CommissionHelper
|
|
9
13
|
from kaq_quant_common.api.rest.instruction.models import \
|
|
10
14
|
InstructionResponseBase
|
|
11
15
|
from kaq_quant_common.api.rest.instruction.models.account import (
|
|
@@ -39,6 +43,8 @@ class InstructionServerBase(ApiServerBase, ApiInterface, ABC):
|
|
|
39
43
|
|
|
40
44
|
# helper
|
|
41
45
|
self._order_helper = OrderHelper(self)
|
|
46
|
+
self._mock_order_helper = MockOrderHelper(self)
|
|
47
|
+
self._commission_helper = CommissionHelper(self)
|
|
42
48
|
|
|
43
49
|
# 统一处理返回数据
|
|
44
50
|
def _wrap_response(self, rsp: InstructionResponseBase):
|
|
@@ -57,6 +63,54 @@ class InstructionServerBase(ApiServerBase, ApiInterface, ABC):
|
|
|
57
63
|
raise Exception(f"order failed: {ret.orders[0].message}")
|
|
58
64
|
return ret
|
|
59
65
|
|
|
66
|
+
# 模拟下单
|
|
67
|
+
@api_method(OrderRequest, OrderResponse)
|
|
68
|
+
def mock_order(self, request: OrderRequest) -> OrderResponse:
|
|
69
|
+
"""
|
|
70
|
+
模拟下单,不调用真实交易所API
|
|
71
|
+
直接在父类处理,子类无需重写
|
|
72
|
+
"""
|
|
73
|
+
ret = OrderResponse(orders=[])
|
|
74
|
+
|
|
75
|
+
# 遍历所有订单
|
|
76
|
+
for order in request.orders:
|
|
77
|
+
# 获取模拟成交价
|
|
78
|
+
mock_fill_price = order.current_price
|
|
79
|
+
|
|
80
|
+
# 从commission_helper获取手续费率,如果获取失败使用默认值0.0005
|
|
81
|
+
mock_fee_rate = self._commission_helper.get_taker_commission_rate(
|
|
82
|
+
symbol=order.symbol,
|
|
83
|
+
default_rate=0.0005
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 使用mock_order_helper处理
|
|
87
|
+
try:
|
|
88
|
+
self._mock_order_helper.process_order(
|
|
89
|
+
order=order,
|
|
90
|
+
mock_fill_price=mock_fill_price,
|
|
91
|
+
mock_fee_rate=mock_fee_rate
|
|
92
|
+
)
|
|
93
|
+
# 成功的订单
|
|
94
|
+
from kaq_quant_common.api.rest.instruction.models.order import OpenedOrderInfo
|
|
95
|
+
ret.orders.append(
|
|
96
|
+
OpenedOrderInfo(
|
|
97
|
+
order_id=order.order_id,
|
|
98
|
+
symbol=order.symbol
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self._logger.error(f"Mock order failed: {e}")
|
|
103
|
+
from kaq_quant_common.api.rest.instruction.models.order import OpenedOrderInfo
|
|
104
|
+
ret.orders.append(
|
|
105
|
+
OpenedOrderInfo(
|
|
106
|
+
order_id=order.order_id,
|
|
107
|
+
symbol=order.symbol,
|
|
108
|
+
message=str(e)
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return ret
|
|
113
|
+
|
|
60
114
|
# 修改订单
|
|
61
115
|
@api_method(ModifyOrderRequest, ModifyOrderResponse)
|
|
62
116
|
def modify_order(self, request: ModifyOrderRequest) -> ModifyOrderResponse:
|
|
@@ -132,6 +132,8 @@ class FundingRateHelper:
|
|
|
132
132
|
# 避免刷库异常导致线程退出
|
|
133
133
|
self._logger.error(f"批量写入list失败: {e}")
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
if self._isMtwDdb:
|
|
135
|
+
if not self._isMtwDdb:
|
|
137
136
|
time.sleep(self._flush_interval_ms / 1000.0)
|
|
137
|
+
else:
|
|
138
|
+
# mtw交由ddb自己控制节奏
|
|
139
|
+
time.sleep(50 / 1000.0)
|
|
@@ -153,6 +153,8 @@ class LimitOrderHelper:
|
|
|
153
153
|
# f"批量写入{len(to_process)}条数据耗时{total_use_time}ms(avg {cum_total_use_time / cum_count:.2f}ms) 转换耗时{convert_use}ms(avg {cum_convert_time / cum_count:.2f}ms) 写入ddb耗时{write_ddb_use}ms(avg {cum_write_ddb_time / cum_count:.2f}ms)"
|
|
154
154
|
# )
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
if self._isMtwDdb:
|
|
156
|
+
if not self._isMtwDdb:
|
|
158
157
|
time.sleep(self._flush_interval_ms / 1000.0)
|
|
158
|
+
else:
|
|
159
|
+
# mtw交由ddb自己控制节奏
|
|
160
|
+
time.sleep(50 / 1000.0)
|
|
@@ -4,14 +4,16 @@ kaq_quant_common/api/common/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY7
|
|
|
4
4
|
kaq_quant_common/api/common/api_interface.py,sha256=E59C2Gh51wmy9NpD9y_SnCh_J-ZbZhT7rUsaORWzXHI,962
|
|
5
5
|
kaq_quant_common/api/common/auth.py,sha256=XqirJRL4D01YfSrBY4hyugw-Op6OJveNE--AnaqhYTQ,3987
|
|
6
6
|
kaq_quant_common/api/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
kaq_quant_common/api/rest/api_client_base.py,sha256=
|
|
7
|
+
kaq_quant_common/api/rest/api_client_base.py,sha256=BtJbN8tB-HqLiH_usDq3I6ZWkFtMh9yizWWOe2HzfD4,6945
|
|
8
8
|
kaq_quant_common/api/rest/api_server_base.py,sha256=URrvzerHIE6XQLERYFcFH1ftLbCYTz3sENAxFD0HWY0,4653
|
|
9
|
-
kaq_quant_common/api/rest/instruction/helper/
|
|
10
|
-
kaq_quant_common/api/rest/instruction/
|
|
11
|
-
kaq_quant_common/api/rest/instruction/
|
|
9
|
+
kaq_quant_common/api/rest/instruction/helper/commission_helper.py,sha256=lVthj5SENMylH2VH90b77jeCM1sTN9JJXYkSyfsLUgA,5234
|
|
10
|
+
kaq_quant_common/api/rest/instruction/helper/mock_order_helper.py,sha256=VgmQiXSM4Cd7M4CZSiY_szODvLJ3Nwv4998Kn6OGb7E,12968
|
|
11
|
+
kaq_quant_common/api/rest/instruction/helper/order_helper.py,sha256=rQ2R0noKecfCX7CXnHEtGQLx0qTs19VRq2uTTW-yWiE,13218
|
|
12
|
+
kaq_quant_common/api/rest/instruction/instruction_client.py,sha256=JlsHg_A9HBuAfqPFpINtWWL-mvPxUUMsODMdIMyeg3Q,13400
|
|
13
|
+
kaq_quant_common/api/rest/instruction/instruction_server_base.py,sha256=KWOKynlZzAkMUKa8gRMLtD2t9bR2iziu5x0wuTRZAo4,8342
|
|
12
14
|
kaq_quant_common/api/rest/instruction/models/__init__.py,sha256=fx5pnfcf9L5KvAqhsQBZkl9fUf9oABuroLGZqDNycpc,312
|
|
13
15
|
kaq_quant_common/api/rest/instruction/models/account.py,sha256=Lj12EvWNxEt7k9dAKSsFhTJDmLX553duMRa5NroJW30,1375
|
|
14
|
-
kaq_quant_common/api/rest/instruction/models/order.py,sha256=
|
|
16
|
+
kaq_quant_common/api/rest/instruction/models/order.py,sha256=TO_1Z4TYNQSHW7dCe_M7F4lxQJq33ymPtDJV8Qohz2w,6849
|
|
15
17
|
kaq_quant_common/api/rest/instruction/models/position.py,sha256=OqtfWWcpGhbijJbwJqERkeFxPiIkzdBnhPx5CfXj8W0,1744
|
|
16
18
|
kaq_quant_common/api/rest/instruction/models/transfer.py,sha256=htjk4hb9THUZP4REW5gtyPdo850jHPtHPWFLPA2ERzo,775
|
|
17
19
|
kaq_quant_common/api/ws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -27,8 +29,8 @@ kaq_quant_common/api/ws/ws_server_base.py,sha256=-JFA5fnYHXPYBZ09aZmhYuhgDHFfJbk
|
|
|
27
29
|
kaq_quant_common/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
30
|
kaq_quant_common/common/ddb_table_monitor.py,sha256=7Yihz_uGGujo_QqqPl45Gp8fwUMMw1auXx5egbzyYlE,3662
|
|
29
31
|
kaq_quant_common/common/http_monitor.py,sha256=_yChiwfVv1c5g_lKgYUjWY40fX61BWVK8SL4kXwRfwk,2375
|
|
30
|
-
kaq_quant_common/common/modules/funding_rate_helper.py,sha256=
|
|
31
|
-
kaq_quant_common/common/modules/limit_order_helper.py,sha256=
|
|
32
|
+
kaq_quant_common/common/modules/funding_rate_helper.py,sha256=IHBOMZuTpCqvLlPzrB0KquqEWVeIOEEE1wBsNM4VS9g,5800
|
|
33
|
+
kaq_quant_common/common/modules/limit_order_helper.py,sha256=TB7aXOaQBvWJfJD58qTDEtSgrzExD1zEAc0h8jSiVio,6872
|
|
32
34
|
kaq_quant_common/common/modules/limit_order_symbol_monitor.py,sha256=TBK48qyeCSQvkfDMv3J_0UM7f3OuBRKRFYDcL9kG6Cs,2876
|
|
33
35
|
kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py,sha256=oEqHIwxhqAzckmluHJHZHiHUNmAyaS2JyK2nXO58UhY,2394
|
|
34
36
|
kaq_quant_common/common/monitor_base.py,sha256=E4EUMsO3adNltCDNRgxkvUSbTTfKOL9S1zzN3WkZvpU,2467
|
|
@@ -62,6 +64,6 @@ kaq_quant_common/utils/signal_utils.py,sha256=zBSyEltNTKqkQCsrETd47kEBb3Q_OWUBUn
|
|
|
62
64
|
kaq_quant_common/utils/sqlite_utils.py,sha256=UDDFKfwL0N-jFifl40HdyOCENh2YQfW5so6hRaSJpv0,5722
|
|
63
65
|
kaq_quant_common/utils/uuid_utils.py,sha256=pm_pnXpd8n9CI66x3A20cOEUiriJyqHaKGCeLrgkBxU,71
|
|
64
66
|
kaq_quant_common/utils/yml_utils.py,sha256=gcKjb_-uuUajBGAl5QBPIZTg2wXm7qeeJvtHflj_zOE,4513
|
|
65
|
-
kaq_quant_common-0.2.
|
|
66
|
-
kaq_quant_common-0.2.
|
|
67
|
-
kaq_quant_common-0.2.
|
|
67
|
+
kaq_quant_common-0.2.13.dist-info/METADATA,sha256=jQj78kc6NnnGGye5J6eJ-uYG4yTIL77B3s2eoKCKLj4,1971
|
|
68
|
+
kaq_quant_common-0.2.13.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
69
|
+
kaq_quant_common-0.2.13.dist-info/RECORD,,
|
|
File without changes
|