kaq-quant-common 0.2.12__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/__init__.py +0 -0
- kaq_quant_common/api/__init__.py +0 -0
- kaq_quant_common/api/common/__init__.py +1 -0
- kaq_quant_common/api/common/api_interface.py +38 -0
- kaq_quant_common/api/common/auth.py +118 -0
- kaq_quant_common/api/rest/__init__.py +0 -0
- kaq_quant_common/api/rest/api_client_base.py +42 -0
- kaq_quant_common/api/rest/api_server_base.py +135 -0
- kaq_quant_common/api/rest/instruction/helper/order_helper.py +342 -0
- kaq_quant_common/api/rest/instruction/instruction_client.py +86 -0
- kaq_quant_common/api/rest/instruction/instruction_server_base.py +154 -0
- kaq_quant_common/api/rest/instruction/models/__init__.py +17 -0
- kaq_quant_common/api/rest/instruction/models/account.py +49 -0
- kaq_quant_common/api/rest/instruction/models/order.py +248 -0
- kaq_quant_common/api/rest/instruction/models/position.py +70 -0
- kaq_quant_common/api/rest/instruction/models/transfer.py +32 -0
- kaq_quant_common/api/ws/__init__.py +0 -0
- kaq_quant_common/api/ws/exchange/models.py +23 -0
- kaq_quant_common/api/ws/exchange/ws_exchange_client.py +31 -0
- kaq_quant_common/api/ws/exchange/ws_exchange_server.py +440 -0
- kaq_quant_common/api/ws/instruction/__init__.py +0 -0
- kaq_quant_common/api/ws/instruction/ws_instruction_client.py +82 -0
- kaq_quant_common/api/ws/instruction/ws_instruction_server_base.py +139 -0
- kaq_quant_common/api/ws/models.py +46 -0
- kaq_quant_common/api/ws/ws_client_base.py +235 -0
- kaq_quant_common/api/ws/ws_server_base.py +288 -0
- kaq_quant_common/common/__init__.py +0 -0
- kaq_quant_common/common/ddb_table_monitor.py +106 -0
- kaq_quant_common/common/http_monitor.py +69 -0
- kaq_quant_common/common/modules/funding_rate_helper.py +137 -0
- kaq_quant_common/common/modules/limit_order_helper.py +158 -0
- kaq_quant_common/common/modules/limit_order_symbol_monitor.py +76 -0
- kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py +69 -0
- kaq_quant_common/common/monitor_base.py +84 -0
- kaq_quant_common/common/monitor_group.py +97 -0
- kaq_quant_common/common/redis_table_monitor.py +123 -0
- kaq_quant_common/common/statistics/funding_rate_history_statistics.py +208 -0
- kaq_quant_common/common/statistics/kline_history_statistics.py +211 -0
- kaq_quant_common/common/ws_wrapper.py +21 -0
- kaq_quant_common/config/config.yaml +5 -0
- kaq_quant_common/resources/__init__.py +0 -0
- kaq_quant_common/resources/kaq_ddb_pool_stream_read_resources.py +56 -0
- kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +88 -0
- kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +81 -0
- kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +359 -0
- kaq_quant_common/resources/kaq_mysql_init_resources.py +23 -0
- kaq_quant_common/resources/kaq_mysql_resources.py +341 -0
- kaq_quant_common/resources/kaq_postgresql_resources.py +58 -0
- kaq_quant_common/resources/kaq_quant_hive_resources.py +107 -0
- kaq_quant_common/resources/kaq_redis_resources.py +117 -0
- kaq_quant_common/utils/__init__.py +0 -0
- kaq_quant_common/utils/dagster_job_check_utils.py +29 -0
- kaq_quant_common/utils/dagster_utils.py +19 -0
- kaq_quant_common/utils/date_util.py +204 -0
- kaq_quant_common/utils/enums_utils.py +79 -0
- kaq_quant_common/utils/error_utils.py +22 -0
- kaq_quant_common/utils/hash_utils.py +48 -0
- kaq_quant_common/utils/log_time_utils.py +32 -0
- kaq_quant_common/utils/logger_utils.py +97 -0
- kaq_quant_common/utils/mytt_utils.py +372 -0
- kaq_quant_common/utils/signal_utils.py +23 -0
- kaq_quant_common/utils/sqlite_utils.py +169 -0
- kaq_quant_common/utils/uuid_utils.py +5 -0
- kaq_quant_common/utils/yml_utils.py +148 -0
- kaq_quant_common-0.2.12.dist-info/METADATA +66 -0
- kaq_quant_common-0.2.12.dist-info/RECORD +67 -0
- kaq_quant_common-0.2.12.dist-info/WHEEL +4 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, Type
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def api_method(request_model: Type[BaseModel], response_model: Type[BaseModel]):
|
|
9
|
+
"""
|
|
10
|
+
api 方法注解
|
|
11
|
+
:param request_model: 请求模型
|
|
12
|
+
:param response_model: 响应模型
|
|
13
|
+
:return:
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def decorator(func: Callable):
|
|
17
|
+
# 将注解信息绑定到原始函数
|
|
18
|
+
func._is_api_method = True
|
|
19
|
+
func._request_model = request_model
|
|
20
|
+
func._response_model = response_model
|
|
21
|
+
|
|
22
|
+
@wraps(func)
|
|
23
|
+
def wrapper(*args, **kwargs):
|
|
24
|
+
return func(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
# 同步注解信息到包装函数,便于通过inspect发现
|
|
27
|
+
wrapper._is_api_method = True
|
|
28
|
+
wrapper._request_model = request_model
|
|
29
|
+
wrapper._response_model = response_model
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# 定义 api 接口,暂时没啥用
|
|
37
|
+
class ApiInterface(ABC):
|
|
38
|
+
pass
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from kaq_quant_common.utils import yml_utils
|
|
5
|
+
|
|
6
|
+
# 统一的简单鉴权:基于一个共享的 token
|
|
7
|
+
# - 来源优先级:环境变量 KAQ_API_TOKEN > 配置文件 kaq.api_token
|
|
8
|
+
# - 如果未配置 token,则认为鉴权关闭(通过)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_expected_token(pkg_name: Optional[str] = None) -> Optional[str]:
|
|
12
|
+
# 环境变量优先
|
|
13
|
+
token = os.getenv("KAQ_API_TOKEN")
|
|
14
|
+
if token:
|
|
15
|
+
return token.strip()
|
|
16
|
+
# 配置文件兜底
|
|
17
|
+
try:
|
|
18
|
+
token_cfg = yml_utils.get(pkg_name or "kaq_quant_common", "api_token")
|
|
19
|
+
if isinstance(token_cfg, str) and token_cfg.strip():
|
|
20
|
+
return token_cfg.strip()
|
|
21
|
+
except Exception as e:
|
|
22
|
+
# 配置读取失败则视为未配置
|
|
23
|
+
pass
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _extract_token_from_authorization(auth_header: Optional[str]) -> Optional[str]:
|
|
28
|
+
if not auth_header:
|
|
29
|
+
return None
|
|
30
|
+
# 支持 Bearer/Token 两种前缀
|
|
31
|
+
val = auth_header.strip()
|
|
32
|
+
lower = val.lower()
|
|
33
|
+
if lower.startswith("bearer "):
|
|
34
|
+
return val[7:].strip()
|
|
35
|
+
if lower.startswith("token "):
|
|
36
|
+
return val[6:].strip()
|
|
37
|
+
# 若为纯 token 也接受
|
|
38
|
+
return val
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _extract_token_from_headers(headers: Any) -> Optional[str]:
|
|
42
|
+
# 兼容 Flask/Werkzeug 和 websockets.Headers
|
|
43
|
+
try:
|
|
44
|
+
# Flask/requests:大小写不敏感
|
|
45
|
+
auth = headers.get("Authorization") if hasattr(headers, "get") else None
|
|
46
|
+
if not auth and hasattr(headers, "get"):
|
|
47
|
+
auth = headers.get("authorization")
|
|
48
|
+
token = _extract_token_from_authorization(auth)
|
|
49
|
+
if token:
|
|
50
|
+
return token
|
|
51
|
+
# 备用头
|
|
52
|
+
x_token = headers.get("X-API-Token") if hasattr(headers, "get") else None
|
|
53
|
+
if not x_token and hasattr(headers, "get"):
|
|
54
|
+
x_token = headers.get("x-api-token")
|
|
55
|
+
return x_token.strip() if isinstance(x_token, str) and x_token.strip() else None
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _extract_token_from_path_query(path: Optional[str]) -> Optional[str]:
|
|
61
|
+
# 解析 ?token=xxx 简单查询参数
|
|
62
|
+
if not path or "?" not in path:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
query = path.split("?", 1)[1]
|
|
66
|
+
for pair in query.split("&"):
|
|
67
|
+
if not pair:
|
|
68
|
+
continue
|
|
69
|
+
k, _, v = pair.partition("=")
|
|
70
|
+
if k.lower() == "token" and v:
|
|
71
|
+
return v
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ----------------- 对外校验方法 -----------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def verify_http_request(flask_request: Any, pkg_name: Optional[str] = None) -> Tuple[bool, Optional[str]]:
|
|
81
|
+
"""校验 HTTP 请求头中的 token。
|
|
82
|
+
返回 (是否通过, 错误信息)。未配置 token 时默认放行。
|
|
83
|
+
"""
|
|
84
|
+
expected = _get_expected_token(pkg_name)
|
|
85
|
+
if not expected:
|
|
86
|
+
return True, None
|
|
87
|
+
# 从 Header 提取
|
|
88
|
+
token = _extract_token_from_headers(getattr(flask_request, "headers", None))
|
|
89
|
+
if token and token == expected:
|
|
90
|
+
return True, None
|
|
91
|
+
return False, "Unauthorized"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def verify_ws_handshake(path: Optional[str], headers: Any, pkg_name: Optional[str] = None) -> Tuple[bool, Optional[str]]:
|
|
95
|
+
"""校验 WebSocket 握手阶段的路径与请求头。
|
|
96
|
+
支持从 Header 与 ?token=xx 解析。未配置 token 时默认放行。
|
|
97
|
+
"""
|
|
98
|
+
expected = _get_expected_token(pkg_name)
|
|
99
|
+
if not expected:
|
|
100
|
+
return True, None
|
|
101
|
+
# Header 优先
|
|
102
|
+
token = _extract_token_from_headers(headers)
|
|
103
|
+
if not token:
|
|
104
|
+
# 再从路径查询参数解析
|
|
105
|
+
token = _extract_token_from_path_query(path)
|
|
106
|
+
if token and token == expected:
|
|
107
|
+
return True, None
|
|
108
|
+
return False, "Unauthorized"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ----------------- 客户端复用:获取默认 token -----------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_auth_token(pkg_name: Optional[str] = None) -> Optional[str]:
|
|
115
|
+
"""供客户端复用的获取默认 token 的方法。
|
|
116
|
+
与服务器端校验使用相同的来源规则。
|
|
117
|
+
"""
|
|
118
|
+
return _get_expected_token(pkg_name)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional, Type, TypeVar
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from kaq_quant_common.api.common.auth import get_auth_token
|
|
5
|
+
from kaq_quant_common.utils import logger_utils
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
R = TypeVar("R", bound=BaseModel)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiClientBase:
|
|
12
|
+
"""
|
|
13
|
+
api 客户端
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
17
|
+
self._base_url = base_url.rstrip("/")
|
|
18
|
+
self._token = token if token is not None else get_auth_token()
|
|
19
|
+
self._logger = logger_utils.get_logger(self)
|
|
20
|
+
|
|
21
|
+
# 发送请求
|
|
22
|
+
def _make_request(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
23
|
+
url = f"{self._base_url}/api/{method_name}"
|
|
24
|
+
headers = {}
|
|
25
|
+
if self._token:
|
|
26
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
27
|
+
try:
|
|
28
|
+
# 发送post请求
|
|
29
|
+
response = requests.post(url, json=request_data.model_dump(), headers=headers or None)
|
|
30
|
+
# 检查响应状态码,如果不成功,则尝试解析错误信息并抛出异常
|
|
31
|
+
if not response.ok:
|
|
32
|
+
try:
|
|
33
|
+
error_data = response.json()
|
|
34
|
+
error_message = error_data.get("error", response.text)
|
|
35
|
+
except ValueError:
|
|
36
|
+
error_message = response.text
|
|
37
|
+
raise requests.exceptions.HTTPError(f"HTTP error occurred: {response.status_code} - {error_message}", response=response)
|
|
38
|
+
# 返回请求结果
|
|
39
|
+
return response_model(**response.json())
|
|
40
|
+
except requests.exceptions.RequestException as e:
|
|
41
|
+
self._logger.error(f"An error occurred: {e}")
|
|
42
|
+
raise
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from flask import Flask, jsonify, request
|
|
6
|
+
from werkzeug.serving import make_server
|
|
7
|
+
|
|
8
|
+
from kaq_quant_common.api.common.api_interface import ApiInterface
|
|
9
|
+
from kaq_quant_common.api.common.auth import verify_http_request
|
|
10
|
+
from kaq_quant_common.utils import logger_utils, signal_utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiServerBase:
|
|
14
|
+
"""
|
|
15
|
+
API服务器基类,用于动态发现和分派API方法。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api: ApiInterface, host="0.0.0.0", port=5000):
|
|
19
|
+
"""
|
|
20
|
+
初始化API服务器。
|
|
21
|
+
|
|
22
|
+
:param api: 实现了ApiInterface的API实例。
|
|
23
|
+
:param host: 服务器主机地址。
|
|
24
|
+
:param port: 服务器端口。
|
|
25
|
+
"""
|
|
26
|
+
self._app = Flask(__name__)
|
|
27
|
+
self._api = api
|
|
28
|
+
self._host = host
|
|
29
|
+
self._port = port
|
|
30
|
+
self._server = make_server(self._host, self._port, self._app)
|
|
31
|
+
self._api_methods = self._discover_api_methods()
|
|
32
|
+
self._logger = logger_utils.get_logger(self)
|
|
33
|
+
|
|
34
|
+
@self._app.route("/api/<method_name>", methods=["POST"])
|
|
35
|
+
def handle_request(method_name: str):
|
|
36
|
+
"""
|
|
37
|
+
处理API请求。
|
|
38
|
+
"""
|
|
39
|
+
# 简单鉴权(若未配置 token 则默认放行)
|
|
40
|
+
ok, err = verify_http_request(request)
|
|
41
|
+
if not ok:
|
|
42
|
+
return jsonify({"error": err or "Unauthorized"}), 401
|
|
43
|
+
|
|
44
|
+
if method_name not in self._api_methods:
|
|
45
|
+
return jsonify({"error": f"Method '{method_name}' not found"}), 404
|
|
46
|
+
|
|
47
|
+
method, request_model, response_model = self._api_methods[method_name]
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# 使用请求模型验证和解析JSON数据
|
|
51
|
+
request_data = request_model(**request.json)
|
|
52
|
+
# 调用API方法
|
|
53
|
+
response_data = method(request_data)
|
|
54
|
+
# 验证响应类型
|
|
55
|
+
if not isinstance(response_data, response_model):
|
|
56
|
+
return jsonify({"error": "Invalid response type from method"}), 500
|
|
57
|
+
# 允许子类包装响应
|
|
58
|
+
response_data = self._wrap_response(response_data)
|
|
59
|
+
# 返回JSON响应
|
|
60
|
+
return jsonify(response_data.model_dump())
|
|
61
|
+
except Exception as e:
|
|
62
|
+
# 捕获请求处理过程中的异常
|
|
63
|
+
return jsonify({"error": str(e)}), 400
|
|
64
|
+
|
|
65
|
+
def _discover_api_methods(self):
|
|
66
|
+
"""
|
|
67
|
+
发现并注册所有使用@api_method装饰器标记的API方法。
|
|
68
|
+
"""
|
|
69
|
+
methods = {}
|
|
70
|
+
for name, func in inspect.getmembers(self._api, predicate=inspect.ismethod):
|
|
71
|
+
if hasattr(func, "_is_api_method") and func._is_api_method:
|
|
72
|
+
methods[name] = (func, func._request_model, func._response_model)
|
|
73
|
+
return methods
|
|
74
|
+
|
|
75
|
+
# 子类用来包装响应,例如添加时间戳
|
|
76
|
+
def _wrap_response(self, rsp: any):
|
|
77
|
+
return rsp
|
|
78
|
+
|
|
79
|
+
def run(self):
|
|
80
|
+
"""
|
|
81
|
+
启动API服务器。
|
|
82
|
+
"""
|
|
83
|
+
self._logger.info(f"Starting server on {self._host}:{self._port}")
|
|
84
|
+
self._server.serve_forever()
|
|
85
|
+
|
|
86
|
+
def shutdown(self):
|
|
87
|
+
"""
|
|
88
|
+
关闭API服务器。
|
|
89
|
+
"""
|
|
90
|
+
self._logger.info(f"Shutting down server on {self._host}:{self._port}")
|
|
91
|
+
self._server.shutdown()
|
|
92
|
+
|
|
93
|
+
def run_with_thread(self, block=True):
|
|
94
|
+
"""
|
|
95
|
+
启动API服务器在一个新线程中。
|
|
96
|
+
"""
|
|
97
|
+
self._server_thread = threading.Thread(target=self.run)
|
|
98
|
+
self._server_thread.name = "ApiServerThread"
|
|
99
|
+
self._server_thread.daemon = True
|
|
100
|
+
self._server_thread.start()
|
|
101
|
+
time.sleep(1)
|
|
102
|
+
|
|
103
|
+
if block:
|
|
104
|
+
self.wait_for_termination()
|
|
105
|
+
|
|
106
|
+
def shutdown_with_thread(self):
|
|
107
|
+
"""
|
|
108
|
+
关闭服务器并等待线程退出
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
self.shutdown()
|
|
112
|
+
finally:
|
|
113
|
+
if hasattr(self, "_server_thread") and self._server_thread.is_alive():
|
|
114
|
+
self._server_thread.join(timeout=3)
|
|
115
|
+
|
|
116
|
+
#
|
|
117
|
+
def wait_for_termination(self):
|
|
118
|
+
# 全局退出事件,用于传递终止信号
|
|
119
|
+
exit_event = threading.Event()
|
|
120
|
+
|
|
121
|
+
def handle_terminate_signal(signum, frame=None):
|
|
122
|
+
"""信号处理函数:捕获终止信号并触发退出事件"""
|
|
123
|
+
self._logger.info(f"收到终止信号 {signum}")
|
|
124
|
+
exit_event.set()
|
|
125
|
+
# 优雅地停止服务器
|
|
126
|
+
self.shutdown()
|
|
127
|
+
|
|
128
|
+
# 监听信号
|
|
129
|
+
signal_utils.register_signal_handler(handle_terminate_signal)
|
|
130
|
+
|
|
131
|
+
# 监听退出事件
|
|
132
|
+
while not exit_event.is_set():
|
|
133
|
+
time.sleep(1)
|
|
134
|
+
|
|
135
|
+
self._logger.warning("ApiServer 线程退出")
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from kaq_quant_common.api.rest.instruction.models.order import (
|
|
7
|
+
OrderInfo,
|
|
8
|
+
OrderSide,
|
|
9
|
+
OrderStatus,
|
|
10
|
+
PositionStatus,
|
|
11
|
+
)
|
|
12
|
+
from kaq_quant_common.api.rest.instruction.models.position import PositionSide
|
|
13
|
+
from kaq_quant_common.utils import logger_utils, uuid_utils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrderHelper:
|
|
17
|
+
def __init__(self, ins_server):
|
|
18
|
+
# 必须放在这里 延迟引入,否则会有循环引用问题
|
|
19
|
+
from kaq_quant_common.api.rest.instruction.instruction_server_base import (
|
|
20
|
+
InstructionServerBase,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
self._server: InstructionServerBase = ins_server
|
|
24
|
+
self._logger = logger_utils.get_logger(self)
|
|
25
|
+
|
|
26
|
+
self._mysql_table_name_order = "kaq_futures_instruction_order"
|
|
27
|
+
self._mysql_table_name_position = "kaq_futures_instruction_position"
|
|
28
|
+
# 当前持仓
|
|
29
|
+
self._redis_key_position = "kaq_futures_instruction_position"
|
|
30
|
+
# 持仓历史
|
|
31
|
+
self._redis_key_position_history = "kaq_futures_instruction_position_history"
|
|
32
|
+
|
|
33
|
+
def _write_position_open_to_redis(
|
|
34
|
+
self,
|
|
35
|
+
position_id: str,
|
|
36
|
+
exchange: str,
|
|
37
|
+
symbol: str,
|
|
38
|
+
position_side,
|
|
39
|
+
coin_quantity: float,
|
|
40
|
+
usdt_quantity: float,
|
|
41
|
+
open_ins_id: str,
|
|
42
|
+
open_price: float,
|
|
43
|
+
open_fee: float,
|
|
44
|
+
open_fee_rate: float,
|
|
45
|
+
open_time: int,
|
|
46
|
+
):
|
|
47
|
+
redis = self._server._redis
|
|
48
|
+
if redis is None:
|
|
49
|
+
return
|
|
50
|
+
data = {
|
|
51
|
+
"id": position_id,
|
|
52
|
+
"exchange": exchange,
|
|
53
|
+
"symbol": symbol,
|
|
54
|
+
"position_side": position_side.value,
|
|
55
|
+
"coin_quantity": coin_quantity,
|
|
56
|
+
"usdt_quantity": usdt_quantity,
|
|
57
|
+
"open_ins_id": open_ins_id,
|
|
58
|
+
"open_price": open_price,
|
|
59
|
+
"open_fee": open_fee,
|
|
60
|
+
"open_fee_rate": open_fee_rate,
|
|
61
|
+
"open_time": open_time,
|
|
62
|
+
"close_ins_id": None,
|
|
63
|
+
"close_price": 0,
|
|
64
|
+
"close_time": 0,
|
|
65
|
+
"status": PositionStatus.OPEN.value,
|
|
66
|
+
}
|
|
67
|
+
redis.client.hset(self._redis_key_position, position_id, json.dumps(data))
|
|
68
|
+
|
|
69
|
+
def _write_position_close_to_redis(
|
|
70
|
+
self,
|
|
71
|
+
position_id: str,
|
|
72
|
+
exchange: str,
|
|
73
|
+
symbol: str,
|
|
74
|
+
position_side,
|
|
75
|
+
coin_quantity: float,
|
|
76
|
+
usdt_quantity: float,
|
|
77
|
+
open_ins_id: str,
|
|
78
|
+
open_price: float,
|
|
79
|
+
open_fee: float,
|
|
80
|
+
open_fee_rate: float,
|
|
81
|
+
open_time: int,
|
|
82
|
+
close_ins_id: str,
|
|
83
|
+
close_price: float,
|
|
84
|
+
close_fee: float,
|
|
85
|
+
close_fee_rate: float,
|
|
86
|
+
close_time: int,
|
|
87
|
+
):
|
|
88
|
+
redis = self._server._redis
|
|
89
|
+
if redis is None:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# 先从 redis 读取现有的 position 数据,获取 funding_rate_records 字段
|
|
93
|
+
funding_rate_records = None
|
|
94
|
+
try:
|
|
95
|
+
existing_position_json = redis.client.hget(self._redis_key_position, position_id)
|
|
96
|
+
if existing_position_json:
|
|
97
|
+
existing_position = json.loads(existing_position_json)
|
|
98
|
+
if existing_position and "funding_rate_records" in existing_position:
|
|
99
|
+
funding_rate_records = existing_position.get("funding_rate_records")
|
|
100
|
+
except Exception as e:
|
|
101
|
+
# 读取失败不影响后续流程,记录日志
|
|
102
|
+
self._logger.warning(f"Failed to get funding_rate_records for position {position_id}: {e}")
|
|
103
|
+
|
|
104
|
+
data = {
|
|
105
|
+
"id": position_id,
|
|
106
|
+
"exchange": exchange,
|
|
107
|
+
"symbol": symbol,
|
|
108
|
+
"position_side": position_side.value,
|
|
109
|
+
"coin_quantity": coin_quantity,
|
|
110
|
+
"usdt_quantity": usdt_quantity,
|
|
111
|
+
"open_ins_id": open_ins_id,
|
|
112
|
+
"open_price": open_price,
|
|
113
|
+
"open_fee": open_fee,
|
|
114
|
+
"open_fee_rate": open_fee_rate,
|
|
115
|
+
"open_time": open_time,
|
|
116
|
+
"close_ins_id": close_ins_id,
|
|
117
|
+
"close_price": close_price,
|
|
118
|
+
"close_fee": close_fee,
|
|
119
|
+
"close_fee_rate": close_fee_rate,
|
|
120
|
+
"close_time": close_time,
|
|
121
|
+
"status": PositionStatus.CLOSE.value,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# 如果存在 funding_rate_records,添加到 data 中
|
|
125
|
+
if funding_rate_records is not None:
|
|
126
|
+
data["funding_rate_records"] = funding_rate_records
|
|
127
|
+
|
|
128
|
+
redis.client.hdel(self._redis_key_position, position_id)
|
|
129
|
+
redis.client.rpush(self._redis_key_position_history, json.dumps(data))
|
|
130
|
+
|
|
131
|
+
def process_order(self, order: OrderInfo, get_order_result: Callable):
|
|
132
|
+
# 获取交易所
|
|
133
|
+
exchange = self._server._exchange
|
|
134
|
+
|
|
135
|
+
# 记录时间,统计成交耗时
|
|
136
|
+
start_time = time.time()
|
|
137
|
+
|
|
138
|
+
#
|
|
139
|
+
if not self._do_process_order(exchange, order, get_order_result, True, start_time):
|
|
140
|
+
# 马上执行,没有成功,开启线程执行
|
|
141
|
+
thread = threading.Thread(
|
|
142
|
+
target=self._do_process_order,
|
|
143
|
+
args=(exchange, order, get_order_result, False, start_time),
|
|
144
|
+
)
|
|
145
|
+
thread.name = f"process_order_{order.instruction_id}_{exchange}_{order.symbol}_{order.order_id}"
|
|
146
|
+
thread.daemon = True
|
|
147
|
+
thread.start()
|
|
148
|
+
|
|
149
|
+
def _do_process_order(
|
|
150
|
+
self,
|
|
151
|
+
exchange: str,
|
|
152
|
+
order: OrderInfo,
|
|
153
|
+
get_order_result: Callable,
|
|
154
|
+
first=True,
|
|
155
|
+
start_time: float = 0,
|
|
156
|
+
):
|
|
157
|
+
# 获取mysql
|
|
158
|
+
mysql = self._server._mysql
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
ins_id = order.instruction_id
|
|
162
|
+
order_id = order.order_id
|
|
163
|
+
symbol = order.symbol
|
|
164
|
+
side = order.side
|
|
165
|
+
position_side = order.position_side
|
|
166
|
+
|
|
167
|
+
is_open = True
|
|
168
|
+
side_str = "开仓"
|
|
169
|
+
if position_side == PositionSide.LONG:
|
|
170
|
+
# 多单是正向理解的
|
|
171
|
+
if side == OrderSide.SELL:
|
|
172
|
+
side_str = "平仓"
|
|
173
|
+
is_open = False
|
|
174
|
+
else:
|
|
175
|
+
side_str = "开仓"
|
|
176
|
+
is_open = True
|
|
177
|
+
else:
|
|
178
|
+
# 空单是反向理解的
|
|
179
|
+
if side == OrderSide.SELL:
|
|
180
|
+
side_str = "开仓"
|
|
181
|
+
is_open = True
|
|
182
|
+
else:
|
|
183
|
+
side_str = "平仓"
|
|
184
|
+
is_open = False
|
|
185
|
+
|
|
186
|
+
if first:
|
|
187
|
+
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 1. {side_str}挂单成功 {order_id}")
|
|
188
|
+
|
|
189
|
+
# 步骤1.挂单成功 插入到订单记录
|
|
190
|
+
# 获取当前时间-ms
|
|
191
|
+
current_time = int(time.time() * 1000)
|
|
192
|
+
|
|
193
|
+
if mysql is not None:
|
|
194
|
+
status = OrderStatus.CREATE
|
|
195
|
+
sql = f"""
|
|
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} );
|
|
198
|
+
"""
|
|
199
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
200
|
+
|
|
201
|
+
# 步骤2.查询订单状态 直到订单成交后
|
|
202
|
+
# 统计查询次数
|
|
203
|
+
query_counter = 0
|
|
204
|
+
while True:
|
|
205
|
+
query_counter += 1
|
|
206
|
+
# 获取订单结果
|
|
207
|
+
order_info = None
|
|
208
|
+
try:
|
|
209
|
+
order_info = get_order_result()
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self._logger.error(f"{ins_id}_{exchange}_{symbol} step 2. {side_str}订单 查询状态失败 {e}")
|
|
212
|
+
|
|
213
|
+
if order_info is not None:
|
|
214
|
+
break
|
|
215
|
+
# 只查询一次
|
|
216
|
+
if first:
|
|
217
|
+
return False
|
|
218
|
+
# 等待
|
|
219
|
+
time.sleep(1)
|
|
220
|
+
|
|
221
|
+
if not first:
|
|
222
|
+
# 需要加上第一查询
|
|
223
|
+
query_counter += 1
|
|
224
|
+
|
|
225
|
+
# 记录时间,统计成交耗时
|
|
226
|
+
end_time = time.time()
|
|
227
|
+
cost_time = end_time - start_time
|
|
228
|
+
self._logger.info(
|
|
229
|
+
f"{ins_id}_{exchange}_{symbol} step 2. {side_str}订单 {order_id} 成交 耗时 {int(cost_time * 1000)}ms, 查询次数 {query_counter}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# 步骤3.把最终持仓写进去
|
|
233
|
+
# 平均成交价格 转float
|
|
234
|
+
avg_price = float(order_info["avg_price"])
|
|
235
|
+
# 最终成交数量 转float
|
|
236
|
+
executed_qty = float(order_info["executed_qty"])
|
|
237
|
+
# 计算出usdt数量
|
|
238
|
+
executed_usdt = avg_price * executed_qty
|
|
239
|
+
# 手续费,不一定有
|
|
240
|
+
fee = float(order_info["fee"]) if "fee" in order_info else 0.0
|
|
241
|
+
# 费率
|
|
242
|
+
fee_rate = fee / executed_usdt
|
|
243
|
+
|
|
244
|
+
current_time = int(time.time() * 1000)
|
|
245
|
+
|
|
246
|
+
if mysql is None:
|
|
247
|
+
self._logger.warning(f"{ins_id}_{exchange}_{symbol} 仅操作,没有入库,请设置 mysql!!")
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
status = OrderStatus.FINISH
|
|
251
|
+
# 更新写入最终信息
|
|
252
|
+
sql = f"""
|
|
253
|
+
UPDATE {self._mysql_table_name_order}
|
|
254
|
+
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}
|
|
255
|
+
WHERE ins_id = '{ins_id}' AND exchange = '{exchange}' AND symbol = '{symbol}';
|
|
256
|
+
"""
|
|
257
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
258
|
+
|
|
259
|
+
self._logger.info(
|
|
260
|
+
f"{ins_id}_{exchange}_{symbol} step 2. 订单成交 {order_id}, {side_str}价格 {avg_price}, {side_str}数量 {executed_qty}, {side_str}usdt {executed_usdt}"
|
|
261
|
+
)
|
|
262
|
+
if is_open:
|
|
263
|
+
# 同时插入持仓表
|
|
264
|
+
position_id = uuid_utils.generate_uuid()
|
|
265
|
+
sql = f"""
|
|
266
|
+
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)
|
|
267
|
+
VALUES ( '{position_id}', '{exchange}', '{symbol}', '{position_side.value}', '{executed_qty}', '{executed_usdt}', '{ins_id}', '{avg_price}', '{fee}', '{fee_rate}', {current_time}, '{PositionStatus.OPEN.value}' );
|
|
268
|
+
"""
|
|
269
|
+
execute_ret = mysql.execute_sql(sql, True)
|
|
270
|
+
|
|
271
|
+
self._logger.info(f"{ins_id}_{exchange}_{symbol} step 3. 创建持仓记录 {position_id}")
|
|
272
|
+
try:
|
|
273
|
+
self._write_position_open_to_redis(
|
|
274
|
+
position_id,
|
|
275
|
+
exchange,
|
|
276
|
+
symbol,
|
|
277
|
+
position_side,
|
|
278
|
+
executed_qty,
|
|
279
|
+
executed_usdt,
|
|
280
|
+
ins_id,
|
|
281
|
+
avg_price,
|
|
282
|
+
fee,
|
|
283
|
+
fee_rate,
|
|
284
|
+
current_time,
|
|
285
|
+
)
|
|
286
|
+
except:
|
|
287
|
+
pass
|
|
288
|
+
else:
|
|
289
|
+
# 需要找到对应的持仓记录
|
|
290
|
+
sql = f"""
|
|
291
|
+
SELECT * FROM {self._mysql_table_name_position}
|
|
292
|
+
WHERE exchange = '{exchange}' AND symbol = '{symbol}' AND position_side = '{position_side.value}' AND status = '{PositionStatus.OPEN.value}'
|
|
293
|
+
ORDER BY open_time ASC;
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
# 如果有指定仓位id,就用指定的
|
|
297
|
+
if hasattr(order, "position_id") and order.position_id:
|
|
298
|
+
sql = f"""
|
|
299
|
+
SELECT * FROM {self._mysql_table_name_position}
|
|
300
|
+
WHERE id = '{order.position_id}' AND status = '{PositionStatus.OPEN.value}'
|
|
301
|
+
"""
|
|
302
|
+
self._logger.info(f"{ins_id}_{exchange}_{symbol} get position by id {order.position_id}")
|
|
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(f"{ins_id}_{exchange}_{symbol} step 3. 更新持仓记录 {position_id}")
|
|
318
|
+
try:
|
|
319
|
+
self._write_position_close_to_redis(
|
|
320
|
+
position_id,
|
|
321
|
+
exchange,
|
|
322
|
+
symbol,
|
|
323
|
+
position_side,
|
|
324
|
+
float(row.coin_quantity),
|
|
325
|
+
float(row.usdt_quantity),
|
|
326
|
+
row.open_ins_id,
|
|
327
|
+
float(row.open_price),
|
|
328
|
+
float(row.open_fee),
|
|
329
|
+
float(row.open_fee_rate),
|
|
330
|
+
int(row.open_time),
|
|
331
|
+
ins_id,
|
|
332
|
+
avg_price,
|
|
333
|
+
fee,
|
|
334
|
+
fee_rate,
|
|
335
|
+
current_time,
|
|
336
|
+
)
|
|
337
|
+
except:
|
|
338
|
+
pass
|
|
339
|
+
except:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
return True
|