dnse-sdk-openapi 0.0.1__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.
- broker-api/get_list_care_by.py +22 -0
- dnse/__init__.py +4 -0
- dnse/client.py +408 -0
- dnse/common.py +103 -0
- dnse_sdk_openapi-0.0.1.dist-info/METADATA +120 -0
- dnse_sdk_openapi-0.0.1.dist-info/RECORD +50 -0
- dnse_sdk_openapi-0.0.1.dist-info/WHEEL +5 -0
- dnse_sdk_openapi-0.0.1.dist-info/top_level.txt +5 -0
- marketdata-api/get_instruments.py +22 -0
- marketdata-api/get_latest_trade.py +22 -0
- marketdata-api/get_ohlc.py +31 -0
- marketdata-api/get_security_definition.py +22 -0
- marketdata-api/get_trades.py +22 -0
- marketdata-api/get_working_dates.py +22 -0
- trading-api/cancel_order.py +29 -0
- trading-api/close_position.py +27 -0
- trading-api/create_trading_token.py +26 -0
- trading-api/get_accounts.py +22 -0
- trading-api/get_balances.py +22 -0
- trading-api/get_close_price.py +22 -0
- trading-api/get_execution_detail.py +28 -0
- trading-api/get_loan_packages.py +27 -0
- trading-api/get_order_detail.py +28 -0
- trading-api/get_order_history.py +30 -0
- trading-api/get_orders.py +27 -0
- trading-api/get_position_by_id.py +26 -0
- trading-api/get_positions.py +26 -0
- trading-api/get_ppse.py +29 -0
- trading-api/post_order.py +38 -0
- trading-api/put_order.py +35 -0
- trading-api/send_email_otp.py +22 -0
- websocket-marketdata/expected_price.py +53 -0
- websocket-marketdata/foreign_investor.py +51 -0
- websocket-marketdata/market_index.py +52 -0
- websocket-marketdata/ohlc.py +55 -0
- websocket-marketdata/ohlc_closed.py +55 -0
- websocket-marketdata/order.py +51 -0
- websocket-marketdata/quote.py +50 -0
- websocket-marketdata/sec_def.py +52 -0
- websocket-marketdata/trade.py +52 -0
- websocket-marketdata/trade_extra.py +51 -0
- websocket-marketdata/trading_websocket/__init__.py +33 -0
- websocket-marketdata/trading_websocket/_version.py +3 -0
- websocket-marketdata/trading_websocket/auth.py +59 -0
- websocket-marketdata/trading_websocket/client.py +790 -0
- websocket-marketdata/trading_websocket/connection.py +151 -0
- websocket-marketdata/trading_websocket/encoding.py +78 -0
- websocket-marketdata/trading_websocket/exceptions.py +38 -0
- websocket-marketdata/trading_websocket/models.py +525 -0
- websocket-marketdata/trading_websocket/py.typed +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import ssl
|
|
4
|
+
from typing import Optional, AsyncIterator
|
|
5
|
+
|
|
6
|
+
import certifi
|
|
7
|
+
import websockets
|
|
8
|
+
from websockets import ClientConnection
|
|
9
|
+
from .exceptions import ConnectionError, ConnectionClosed
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
logger.setLevel(logging.INFO)
|
|
13
|
+
if not logger.handlers:
|
|
14
|
+
handler = logging.StreamHandler()
|
|
15
|
+
handler.setLevel(logging.INFO)
|
|
16
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
17
|
+
handler.setFormatter(formatter)
|
|
18
|
+
logger.addHandler(handler)
|
|
19
|
+
|
|
20
|
+
class WebSocketConnection:
|
|
21
|
+
"""
|
|
22
|
+
WebSocket connection manager with automatic reconnection.
|
|
23
|
+
|
|
24
|
+
Features:
|
|
25
|
+
- Exponential backoff reconnection
|
|
26
|
+
- Connection health monitoring
|
|
27
|
+
- Graceful shutdown
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
url: str,
|
|
33
|
+
timeout: float = 60.0,
|
|
34
|
+
heartbeat_interval: float = 25.0,
|
|
35
|
+
auto_reconnect: bool = True,
|
|
36
|
+
max_retries: int = 10,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Initialize connection manager.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
url: WebSocket URL
|
|
43
|
+
timeout: Connection timeout (seconds)
|
|
44
|
+
heartbeat_interval: Heartbeat interval (seconds)
|
|
45
|
+
auto_reconnect: Enable automatic reconnection
|
|
46
|
+
max_retries: Maximum reconnection attempts
|
|
47
|
+
"""
|
|
48
|
+
self.url = url
|
|
49
|
+
self.timeout = timeout
|
|
50
|
+
self.heartbeat_interval = heartbeat_interval
|
|
51
|
+
self.auto_reconnect = auto_reconnect
|
|
52
|
+
self.max_retries = max_retries
|
|
53
|
+
|
|
54
|
+
self._ws: Optional[ClientConnection] = None
|
|
55
|
+
self._retry_count = 0
|
|
56
|
+
self._is_connected = False
|
|
57
|
+
|
|
58
|
+
async def connect(self) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Establish WebSocket connection.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ConnectionError: Failed to connect after max retries
|
|
64
|
+
asyncio.TimeoutError: Connection timeout
|
|
65
|
+
"""
|
|
66
|
+
while self._retry_count < self.max_retries:
|
|
67
|
+
try:
|
|
68
|
+
logger.info(f"Connecting to {self.url} (attempt {self._retry_count + 1}/{self.max_retries})")
|
|
69
|
+
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
|
70
|
+
# ssl_context.check_hostname = False
|
|
71
|
+
# ssl_context.verify_mode = ssl.CERT_NONE
|
|
72
|
+
self._ws = await asyncio.wait_for(
|
|
73
|
+
websockets.connect(self.url,
|
|
74
|
+
ssl=ssl_context,
|
|
75
|
+
ping_interval=30,
|
|
76
|
+
ping_timeout=30,
|
|
77
|
+
close_timeout=10,
|
|
78
|
+
max_queue=512), timeout=self.timeout)
|
|
79
|
+
|
|
80
|
+
self._is_connected = True
|
|
81
|
+
self._retry_count = 0
|
|
82
|
+
logger.info("Connected successfully")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
except (websockets.exceptions.WebSocketException, OSError) as e:
|
|
86
|
+
self._retry_count += 1
|
|
87
|
+
|
|
88
|
+
if self._retry_count >= self.max_retries:
|
|
89
|
+
raise ConnectionError(f"Failed to connect after {self.max_retries} attempts: {e}")
|
|
90
|
+
|
|
91
|
+
# Exponential backoff: 1s, 2s, 4s, 8s, ... up to 60s
|
|
92
|
+
delay = min(2 ** (self._retry_count - 1), 60)
|
|
93
|
+
logger.warning(f"Connection failed: {e}. Retrying in {delay}s...")
|
|
94
|
+
await asyncio.sleep(delay)
|
|
95
|
+
|
|
96
|
+
async def send(self, message: bytes) -> None:
|
|
97
|
+
if not self._ws or not self._is_connected:
|
|
98
|
+
raise ConnectionError("Not connected")
|
|
99
|
+
|
|
100
|
+
await self._ws.send(message)
|
|
101
|
+
|
|
102
|
+
async def receive(self) -> bytes:
|
|
103
|
+
if not self._ws or not self._is_connected:
|
|
104
|
+
raise ConnectionError("Not connected")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
message = await self._ws.recv()
|
|
108
|
+
return message if isinstance(message, bytes) else message.encode()
|
|
109
|
+
except websockets.exceptions.ConnectionClosed as e:
|
|
110
|
+
self._is_connected = False
|
|
111
|
+
code = e.rcvd.code if e.rcvd else 1006
|
|
112
|
+
|
|
113
|
+
if code in (1000, 1001): # Normal closure, going away
|
|
114
|
+
logger.info(f"Connection closed normally: {code}")
|
|
115
|
+
raise ConnectionClosed(f"Connection closed normally: {code}")
|
|
116
|
+
elif code in (1006, 1011, 1012): # Abnormal, server error, restart
|
|
117
|
+
logger.warning(f"Connection closed abnormally: {code}")
|
|
118
|
+
if self.auto_reconnect:
|
|
119
|
+
raise ConnectionClosed(f"Connection closed abnormally: {code}", recoverable=True)
|
|
120
|
+
else:
|
|
121
|
+
raise ConnectionClosed(f"Connection closed abnormally: {code}")
|
|
122
|
+
else:
|
|
123
|
+
logger.error(f"Connection closed with unexpected code: {code}")
|
|
124
|
+
raise ConnectionClosed(f"Connection closed: {code}", recoverable=True)
|
|
125
|
+
|
|
126
|
+
async def close(self) -> None:
|
|
127
|
+
"""Close connection gracefully"""
|
|
128
|
+
if self._ws:
|
|
129
|
+
await self._ws.close()
|
|
130
|
+
|
|
131
|
+
self._is_connected = False
|
|
132
|
+
logger.info("Connection closed")
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_connected(self) -> bool:
|
|
136
|
+
"""Check if connection is active"""
|
|
137
|
+
return self._is_connected and self._ws is not None
|
|
138
|
+
|
|
139
|
+
def __aiter__(self) -> AsyncIterator[bytes]:
|
|
140
|
+
"""Allow async iteration over messages"""
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
async def __anext__(self) -> bytes:
|
|
144
|
+
"""Get next message"""
|
|
145
|
+
try:
|
|
146
|
+
return await self.receive()
|
|
147
|
+
except ConnectionClosed as e:
|
|
148
|
+
if e.recoverable:
|
|
149
|
+
# Re-raise so _message_handler can trigger reconnection
|
|
150
|
+
raise
|
|
151
|
+
raise StopAsyncIteration
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
import msgpack
|
|
4
|
+
from .exceptions import EncodingError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MessageEncoder:
|
|
8
|
+
"""Encode messages for WebSocket transmission"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, encoding: str = "msgpack"):
|
|
11
|
+
"""
|
|
12
|
+
Initialize encoder.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
encoding: "json" or "msgpack"
|
|
16
|
+
"""
|
|
17
|
+
if encoding not in ("json", "msgpack"):
|
|
18
|
+
raise ValueError(f"Invalid encoding: {encoding}. Must be 'json' or 'msgpack'")
|
|
19
|
+
|
|
20
|
+
self.encoding = encoding
|
|
21
|
+
|
|
22
|
+
def encode(self, data: Dict[str, Any]) -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
Encode message.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
data: Message dict
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Encoded bytes
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
EncodingError: Encoding failed
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
if self.encoding == "json":
|
|
37
|
+
return json.dumps(data).encode('utf-8')
|
|
38
|
+
else: # msgpack
|
|
39
|
+
return msgpack.packb(data)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
raise EncodingError(f"Failed to encode message: {e}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MessageDecoder:
|
|
45
|
+
"""Decode messages from WebSocket"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, encoding: str = "msgpack"):
|
|
48
|
+
"""
|
|
49
|
+
Initialize decoder.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
encoding: "json" or "msgpack"
|
|
53
|
+
"""
|
|
54
|
+
if encoding not in ("json", "msgpack"):
|
|
55
|
+
raise ValueError(f"Invalid encoding: {encoding}. Must be 'json' or 'msgpack'")
|
|
56
|
+
|
|
57
|
+
self.encoding = encoding
|
|
58
|
+
|
|
59
|
+
def decode(self, data: bytes) -> Dict[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Decode message.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data: Encoded bytes
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Decoded dict
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
EncodingError: Decoding failed
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
if self.encoding == "json":
|
|
74
|
+
return json.loads(data.decode('utf-8'))
|
|
75
|
+
else: # msgpack
|
|
76
|
+
return msgpack.unpackb(data, raw=False)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise EncodingError(f"Failed to decode message: {e}")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class TradingWebSocketError(Exception):
|
|
2
|
+
"""Base exception for all SDK errors"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConnectionError(TradingWebSocketError):
|
|
7
|
+
"""Failed to establish connection"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConnectionClosed(TradingWebSocketError):
|
|
12
|
+
"""Connection was closed"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message: str, recoverable: bool = False):
|
|
15
|
+
"""
|
|
16
|
+
Initialize ConnectionClosed exception.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
message: Error message
|
|
20
|
+
recoverable: Whether this is a recoverable error (should retry)
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.recoverable = recoverable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuthenticationError(TradingWebSocketError):
|
|
27
|
+
"""Authentication failed"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SubscriptionError(TradingWebSocketError):
|
|
32
|
+
"""Subscription failed"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EncodingError(TradingWebSocketError):
|
|
37
|
+
"""Message encoding/decoding failed"""
|
|
38
|
+
pass
|