sahiapi 0.1.0__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.
- sahiapi/__init__.py +88 -0
- sahiapi/py.typed +0 -0
- sahiapi/sahi/__init__.py +0 -0
- sahiapi/sahi/account.py +28 -0
- sahiapi/sahi/auth.py +163 -0
- sahiapi/sahi/base.py +32 -0
- sahiapi/sahi/client.py +306 -0
- sahiapi/sahi/constants.py +115 -0
- sahiapi/sahi/env.py +39 -0
- sahiapi/sahi/exceptions.py +412 -0
- sahiapi/sahi/feed.py +668 -0
- sahiapi/sahi/funds.py +134 -0
- sahiapi/sahi/http.py +301 -0
- sahiapi/sahi/instruments.py +315 -0
- sahiapi/sahi/marketdata.py +268 -0
- sahiapi/sahi/orders.py +225 -0
- sahiapi/sahi/portfolio.py +49 -0
- sahiapi/sahi/proto/__init__.py +0 -0
- sahiapi/sahi/proto/codec.py +126 -0
- sahiapi/sahi/proto/marketdata.proto +138 -0
- sahiapi/sahi/proto/marketdata_pb2.py +55 -0
- sahiapi/sahi/ws_client.py +355 -0
- sahiapi-0.1.0.dist-info/METADATA +97 -0
- sahiapi-0.1.0.dist-info/RECORD +26 -0
- sahiapi-0.1.0.dist-info/WHEEL +4 -0
- sahiapi-0.1.0.dist-info/licenses/LICENSE +7 -0
sahiapi/sahi/orders.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Order endpoints: place, modify, cancel, and read the order book and trades.
|
|
2
|
+
|
|
3
|
+
These mirror the Orders page of the API reference. Placing, modifying and
|
|
4
|
+
cancelling need the ``orders.write`` scope and must originate from a registered
|
|
5
|
+
static IP; the reads need ``orders.read``.
|
|
6
|
+
|
|
7
|
+
As everywhere in the SDK, arguments are the API's own field names and values,
|
|
8
|
+
prices are integer paise, quantities are integer units, and each method returns
|
|
9
|
+
the parsed response body unchanged.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .base import EndpointGroup
|
|
17
|
+
from .exceptions import SahiConfigurationException
|
|
18
|
+
|
|
19
|
+
__all__ = ["_Orders"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_order_body(
|
|
23
|
+
exchange_segment: str,
|
|
24
|
+
symbol: str,
|
|
25
|
+
txn_type: str,
|
|
26
|
+
qty: int,
|
|
27
|
+
order_type: str,
|
|
28
|
+
product: str,
|
|
29
|
+
validity: str,
|
|
30
|
+
price: int,
|
|
31
|
+
*,
|
|
32
|
+
trigger_price: int | None = None,
|
|
33
|
+
disclosed_qty: int | None = None,
|
|
34
|
+
market_protection: float | None = None,
|
|
35
|
+
variant: str | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""Build the order request body shared by place-order and the margin check.
|
|
38
|
+
|
|
39
|
+
An order and its pre-trade margin quote take the identical body, so both
|
|
40
|
+
:meth:`_Orders.place_order` and ``get_order_margin`` build it here to stay in
|
|
41
|
+
lockstep. Optional fields are included only when set, so the server sees
|
|
42
|
+
exactly what the caller specified.
|
|
43
|
+
"""
|
|
44
|
+
body: dict[str, Any] = {
|
|
45
|
+
"exchange_segment": exchange_segment,
|
|
46
|
+
"symbol": symbol,
|
|
47
|
+
"txn_type": txn_type,
|
|
48
|
+
"qty": qty,
|
|
49
|
+
"order_type": order_type,
|
|
50
|
+
"product": product,
|
|
51
|
+
"validity": validity,
|
|
52
|
+
"price": price,
|
|
53
|
+
}
|
|
54
|
+
if trigger_price is not None:
|
|
55
|
+
body["trigger_price"] = trigger_price
|
|
56
|
+
if disclosed_qty is not None:
|
|
57
|
+
body["disclosed_qty"] = disclosed_qty
|
|
58
|
+
if market_protection is not None:
|
|
59
|
+
body["market_protection"] = market_protection
|
|
60
|
+
if variant is not None:
|
|
61
|
+
body["variant"] = variant
|
|
62
|
+
return body
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _Orders(EndpointGroup):
|
|
66
|
+
"""Order methods mixed into :class:`SahiAPI`."""
|
|
67
|
+
|
|
68
|
+
def place_order(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
exchange_segment: str,
|
|
72
|
+
symbol: str,
|
|
73
|
+
txn_type: str,
|
|
74
|
+
qty: int,
|
|
75
|
+
order_type: str,
|
|
76
|
+
product: str,
|
|
77
|
+
validity: str,
|
|
78
|
+
price: int,
|
|
79
|
+
trigger_price: int | None = None,
|
|
80
|
+
disclosed_qty: int | None = None,
|
|
81
|
+
market_protection: float | None = None,
|
|
82
|
+
variant: str | None = None,
|
|
83
|
+
timeout: float | None = None,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""Place an order (``POST /v1/orders``).
|
|
86
|
+
|
|
87
|
+
Every argument is keyword-only: an order is a run of codes, units and
|
|
88
|
+
paise, and ``price=249955`` says what a bare ``249955`` cannot.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
exchange_segment: ``NSE`` / ``NFO`` / ``BSE`` / ``BFO``.
|
|
92
|
+
symbol: The scrip master's ``symbol`` (e.g. ``RELIANCE``).
|
|
93
|
+
txn_type: ``B`` to buy, ``S`` to sell.
|
|
94
|
+
qty: Total quantity in units. Must not exceed the instrument's freeze
|
|
95
|
+
quantity.
|
|
96
|
+
order_type: ``LMT`` / ``MKT`` / ``SL-LMT`` / ``SL-MKT``.
|
|
97
|
+
product: ``C`` (delivery) / ``I`` (intraday) / ``M`` (margin, F&O).
|
|
98
|
+
validity: ``DAY`` or ``IOC``.
|
|
99
|
+
price: Limit price in paise. Use ``0`` for ``MKT`` / ``SL-MKT``; must
|
|
100
|
+
be a multiple of tick size for ``LMT`` / ``SL-LMT``.
|
|
101
|
+
trigger_price: Paise. Required for ``SL-LMT`` / ``SL-MKT``.
|
|
102
|
+
disclosed_qty: Disclosed quantity, in units.
|
|
103
|
+
market_protection: ``MKT`` / ``SL-MKT`` only. ``-1`` (or omitting it)
|
|
104
|
+
applies auto protection; otherwise a percentage from ``0.01`` to
|
|
105
|
+
``100``. ``0`` is rejected. Ignored for limit orders.
|
|
106
|
+
variant: ``REGULAR`` (default) or ``AMO`` for an after-market order,
|
|
107
|
+
accepted while the market is closed. ``LMT`` / ``SL-LMT`` only.
|
|
108
|
+
timeout: Per-call timeout override.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
``{"order_ids": [...]}``. Always a list, and it may hold more than one
|
|
112
|
+
id — read it as a list.
|
|
113
|
+
"""
|
|
114
|
+
body = build_order_body(
|
|
115
|
+
exchange_segment,
|
|
116
|
+
symbol,
|
|
117
|
+
txn_type,
|
|
118
|
+
qty,
|
|
119
|
+
order_type,
|
|
120
|
+
product,
|
|
121
|
+
validity,
|
|
122
|
+
price,
|
|
123
|
+
trigger_price=trigger_price,
|
|
124
|
+
disclosed_qty=disclosed_qty,
|
|
125
|
+
market_protection=market_protection,
|
|
126
|
+
variant=variant,
|
|
127
|
+
)
|
|
128
|
+
return self._request("POST", "/v1/orders", json=body, timeout=timeout) # type: ignore[no-any-return]
|
|
129
|
+
|
|
130
|
+
def modify_order(
|
|
131
|
+
self,
|
|
132
|
+
order_id: str,
|
|
133
|
+
*,
|
|
134
|
+
qty: int | None = None,
|
|
135
|
+
price: int | None = None,
|
|
136
|
+
order_type: str | None = None,
|
|
137
|
+
validity: str | None = None,
|
|
138
|
+
trigger_price: int | None = None,
|
|
139
|
+
disclosed_qty: int | None = None,
|
|
140
|
+
market_protection: float | None = None,
|
|
141
|
+
timeout: float | None = None,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
"""Modify an open order (``PUT /v1/orders/{order_id}``).
|
|
144
|
+
|
|
145
|
+
Only orders in ``Open`` or ``OpenPartial`` can be modified. Pass only the
|
|
146
|
+
fields you are changing; at least one of ``qty``, ``price``,
|
|
147
|
+
``order_type``, ``validity``, ``trigger_price`` or ``disclosed_qty`` is
|
|
148
|
+
required. ``market_protection`` is not a modification by itself: it only
|
|
149
|
+
sets the protection used when the modify targets ``MKT`` / ``SL-MKT``.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
``{"order_id": ...}`` — the modified order id, echoed back.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
SahiConfigurationException: If no field to change is given. Raised
|
|
156
|
+
before any request is made.
|
|
157
|
+
"""
|
|
158
|
+
body: dict[str, Any] = {}
|
|
159
|
+
if qty is not None:
|
|
160
|
+
body["qty"] = qty
|
|
161
|
+
if price is not None:
|
|
162
|
+
body["price"] = price
|
|
163
|
+
if order_type is not None:
|
|
164
|
+
body["order_type"] = order_type
|
|
165
|
+
if validity is not None:
|
|
166
|
+
body["validity"] = validity
|
|
167
|
+
if trigger_price is not None:
|
|
168
|
+
body["trigger_price"] = trigger_price
|
|
169
|
+
if disclosed_qty is not None:
|
|
170
|
+
body["disclosed_qty"] = disclosed_qty
|
|
171
|
+
if not body:
|
|
172
|
+
# market_protection alone is not a change (the API answers 400), so
|
|
173
|
+
# it is checked after the guard and does not satisfy it.
|
|
174
|
+
raise SahiConfigurationException(
|
|
175
|
+
"modify_order requires at least one field to change: qty, price, "
|
|
176
|
+
"order_type, validity, trigger_price or disclosed_qty. "
|
|
177
|
+
"market_protection alone does not modify an order."
|
|
178
|
+
)
|
|
179
|
+
if market_protection is not None:
|
|
180
|
+
body["market_protection"] = market_protection
|
|
181
|
+
return self._request( # type: ignore[no-any-return]
|
|
182
|
+
"PUT", f"/v1/orders/{order_id}", json=body, timeout=timeout
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def cancel_order(self, order_id: str, *, timeout: float | None = None) -> None:
|
|
186
|
+
"""Cancel an open order (``DELETE /v1/orders/{order_id}``).
|
|
187
|
+
|
|
188
|
+
Cancellable states are ``Open`` and ``OpenPartial``. Returns ``None`` on
|
|
189
|
+
success (``204``).
|
|
190
|
+
"""
|
|
191
|
+
self._request("DELETE", f"/v1/orders/{order_id}", timeout=timeout)
|
|
192
|
+
|
|
193
|
+
def get_order_list(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
194
|
+
"""Return today's order book (``GET /v1/orders``).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
``{"orders": [...]}`` — every order on the account for the current
|
|
198
|
+
trading day, in any status.
|
|
199
|
+
"""
|
|
200
|
+
return self._request( # type: ignore[no-any-return]
|
|
201
|
+
"GET", "/v1/orders", timeout=timeout, retry_on_rate_limit=True
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def get_order_detail(self, order_id: str, *, timeout: float | None = None) -> dict[str, Any]:
|
|
205
|
+
"""Return one order by id (``GET /v1/orders/{order_id}``).
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The order object itself, not a wrapper: the same fields as one entry
|
|
209
|
+
of the ``orders`` list from :meth:`get_order_list`.
|
|
210
|
+
"""
|
|
211
|
+
return self._request( # type: ignore[no-any-return]
|
|
212
|
+
"GET", f"/v1/orders/{order_id}", timeout=timeout, retry_on_rate_limit=True
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def get_trade_list_for_order(
|
|
216
|
+
self, order_id: str, *, timeout: float | None = None
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""Return the trades (fills) for one order (``GET /v1/orders/{order_id}/trades``).
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
``{"trades": [...]}``.
|
|
222
|
+
"""
|
|
223
|
+
return self._request( # type: ignore[no-any-return]
|
|
224
|
+
"GET", f"/v1/orders/{order_id}/trades", timeout=timeout, retry_on_rate_limit=True
|
|
225
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Portfolio endpoints: today's positions and demat holdings.
|
|
2
|
+
|
|
3
|
+
Mirrors the Portfolio page of the API reference. ``get_positions`` needs the
|
|
4
|
+
``positions.read`` scope and ``get_holdings`` needs ``holdings.read``. Both
|
|
5
|
+
return the parsed body unchanged.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .base import EndpointGroup
|
|
13
|
+
|
|
14
|
+
__all__ = ["_Portfolio"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Portfolio(EndpointGroup):
|
|
18
|
+
"""Portfolio methods mixed into :class:`SahiAPI`."""
|
|
19
|
+
|
|
20
|
+
def get_positions(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
21
|
+
"""Return today's net positions across all products (``GET /v1/positions``).
|
|
22
|
+
|
|
23
|
+
A position squared off today is still listed, with ``net_qty`` at ``0``
|
|
24
|
+
and the ``realised_pnl`` it booked: the day's book, not only what is
|
|
25
|
+
still open. Filter on ``net_qty`` before acting on a row — an exit built
|
|
26
|
+
from every entry would send zero-quantity orders, which are rejected.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
``{"positions": [...]}``.
|
|
30
|
+
"""
|
|
31
|
+
return self._request( # type: ignore[no-any-return]
|
|
32
|
+
"GET", "/v1/positions", timeout=timeout, retry_on_rate_limit=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def get_holdings(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
36
|
+
"""Return demat holdings (``GET /v1/holdings``).
|
|
37
|
+
|
|
38
|
+
One entry per ISIN, with today's delivery-product activity folded in:
|
|
39
|
+
``net_qty`` adjusts the settled holding for today's delivery buys and
|
|
40
|
+
sells, and the nested ``position`` carries the day's totals. There are
|
|
41
|
+
no LTP or valuation fields; fetch prices with ``get_ltp`` using each
|
|
42
|
+
entry's ``instrument_ids``.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
``{"holdings": [...]}``; an empty list when you hold nothing.
|
|
46
|
+
"""
|
|
47
|
+
return self._request( # type: ignore[no-any-return]
|
|
48
|
+
"GET", "/v1/holdings", timeout=timeout, retry_on_rate_limit=True
|
|
49
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Encode and decode the market-data tick stream's protobuf frames.
|
|
2
|
+
|
|
3
|
+
The tick stream speaks binary protobuf, not JSON: each frame is one
|
|
4
|
+
``ClientMessage`` (client to server) or ``ServerMessage`` (server to client).
|
|
5
|
+
This module is the only place that touches the generated protobuf classes. It
|
|
6
|
+
turns the SDK's plain arguments into ``ClientMessage`` bytes, and turns incoming
|
|
7
|
+
``ServerMessage`` bytes into a ``(type, data)`` pair where ``data`` is a plain
|
|
8
|
+
dict, so nothing above this layer depends on protobuf.
|
|
9
|
+
|
|
10
|
+
The decoded dicts keep the proto field names verbatim (``instrument_id``,
|
|
11
|
+
``last_trade_price``), keep integers as integers (unlike protobuf's JSON mapping,
|
|
12
|
+
which renders 64-bit ints as strings), and give enum values as their string names
|
|
13
|
+
with the enum prefix stripped (``NSE``, ``LTP``), matching the constants
|
|
14
|
+
elsewhere in the SDK. Only fields the frame actually carries appear, so an absent
|
|
15
|
+
optional such as an index's ``last_trade_time`` is simply missing rather than a
|
|
16
|
+
zero.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from google.protobuf.descriptor import FieldDescriptor
|
|
24
|
+
from google.protobuf.message import Message
|
|
25
|
+
|
|
26
|
+
from .. import constants
|
|
27
|
+
from . import marketdata_pb2 as pb
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"decode_server_message",
|
|
31
|
+
"encode_subscribe",
|
|
32
|
+
"encode_unsubscribe",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# The tick stream carries enum numbers, not the SDK's mode strings; encode via
|
|
36
|
+
# this table.
|
|
37
|
+
_MODE_TO_PROTO = {
|
|
38
|
+
constants.MODE_LTP: pb.MODE_LTP,
|
|
39
|
+
constants.MODE_QUOTE: pb.MODE_QUOTE,
|
|
40
|
+
constants.MODE_DEPTH: pb.MODE_DEPTH,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _proto_mode(mode: str) -> int:
|
|
45
|
+
try:
|
|
46
|
+
return _MODE_TO_PROTO[mode]
|
|
47
|
+
except KeyError:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"unknown mode {mode!r}; use one of "
|
|
50
|
+
f"{constants.MODE_LTP}, {constants.MODE_QUOTE}, {constants.MODE_DEPTH}"
|
|
51
|
+
) from None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def encode_subscribe(mode: str, instrument_ids: list[int]) -> bytes:
|
|
55
|
+
"""Build a ``subscribe`` ClientMessage frame."""
|
|
56
|
+
msg = pb.ClientMessage(
|
|
57
|
+
subscribe=pb.Subscribe(mode=_proto_mode(mode), instrument_ids=instrument_ids)
|
|
58
|
+
)
|
|
59
|
+
return msg.SerializeToString()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def encode_unsubscribe(instrument_ids: list[int]) -> bytes:
|
|
63
|
+
"""Build an ``unsubscribe`` ClientMessage frame."""
|
|
64
|
+
msg = pb.ClientMessage(unsubscribe=pb.Unsubscribe(instrument_ids=instrument_ids))
|
|
65
|
+
return msg.SerializeToString()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def decode_server_message(raw: bytes) -> tuple[str, dict[str, Any]]:
|
|
69
|
+
"""Decode a ServerMessage frame into a ``(type, data)`` pair.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
raw: The binary protobuf frame.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
``(type, data)``. ``type`` is one of ``ack``, ``ltp_tick``, ``quote_tick``,
|
|
76
|
+
``depth_tick``, ``error`` or ``unknown``. ``data`` is the decoded payload
|
|
77
|
+
as a plain dict; for a tick it includes ``instrument_id``.
|
|
78
|
+
"""
|
|
79
|
+
message = pb.ServerMessage()
|
|
80
|
+
message.ParseFromString(raw)
|
|
81
|
+
which = message.WhichOneof("payload")
|
|
82
|
+
if which is None:
|
|
83
|
+
return "unknown", {}
|
|
84
|
+
payload = getattr(message, which)
|
|
85
|
+
return which, _message_to_dict(payload)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _strip_enum_prefix(enum_type_name: str, value_name: str) -> str:
|
|
89
|
+
"""Turn ``EXCHANGE_SEGMENT_NSE`` into ``NSE`` given the enum type name."""
|
|
90
|
+
prefix = "".join(f"_{c}" if c.isupper() else c.upper() for c in enum_type_name).lstrip("_")
|
|
91
|
+
prefix += "_"
|
|
92
|
+
if value_name.startswith(prefix):
|
|
93
|
+
return value_name[len(prefix) :]
|
|
94
|
+
return value_name
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_repeated(field: FieldDescriptor) -> bool:
|
|
98
|
+
# protobuf >= 5.27 exposes is_repeated; older releases only have label.
|
|
99
|
+
is_repeated = getattr(field, "is_repeated", None)
|
|
100
|
+
if is_repeated is not None:
|
|
101
|
+
return bool(is_repeated)
|
|
102
|
+
return field.label == FieldDescriptor.LABEL_REPEATED # type: ignore[attr-defined]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _convert_value(field: FieldDescriptor, value: Any) -> Any:
|
|
106
|
+
if field.type == FieldDescriptor.TYPE_ENUM:
|
|
107
|
+
name = field.enum_type.values_by_number[value].name
|
|
108
|
+
return _strip_enum_prefix(field.enum_type.name, name)
|
|
109
|
+
if field.type == FieldDescriptor.TYPE_MESSAGE:
|
|
110
|
+
return _message_to_dict(value)
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _message_to_dict(message: Message) -> dict[str, Any]:
|
|
115
|
+
"""Convert a protobuf message to a plain dict, set fields only.
|
|
116
|
+
|
|
117
|
+
Integers stay integers, enums become their stripped string names, nested
|
|
118
|
+
messages recurse, and repeated fields become lists.
|
|
119
|
+
"""
|
|
120
|
+
result: dict[str, Any] = {}
|
|
121
|
+
for field, value in message.ListFields():
|
|
122
|
+
if _is_repeated(field):
|
|
123
|
+
result[field.name] = [_convert_value(field, item) for item in value]
|
|
124
|
+
else:
|
|
125
|
+
result[field.name] = _convert_value(field, value)
|
|
126
|
+
return result
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Sahi market-data tick stream schema.
|
|
2
|
+
//
|
|
3
|
+
// Vendored verbatim from the public API reference:
|
|
4
|
+
// https://docs.sahi.com/api/v1/reference/market-data/
|
|
5
|
+
// (section "WSS /v1/stream/marketdata — Tick stream" → "Schema")
|
|
6
|
+
// Fetched 2026-09-13.
|
|
7
|
+
//
|
|
8
|
+
// Regenerate marketdata_pb2.py with tools/regen_proto.sh after editing this file.
|
|
9
|
+
// Do not edit the generated file by hand.
|
|
10
|
+
|
|
11
|
+
syntax = "proto3";
|
|
12
|
+
|
|
13
|
+
package sahi.direct.marketdata.v1;
|
|
14
|
+
|
|
15
|
+
// Enums
|
|
16
|
+
|
|
17
|
+
enum ExchangeSegment {
|
|
18
|
+
EXCHANGE_SEGMENT_INVALID = 0;
|
|
19
|
+
EXCHANGE_SEGMENT_NSE = 1;
|
|
20
|
+
EXCHANGE_SEGMENT_BSE = 2;
|
|
21
|
+
EXCHANGE_SEGMENT_NFO = 3;
|
|
22
|
+
EXCHANGE_SEGMENT_BFO = 4;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
enum InstrumentType {
|
|
26
|
+
INSTRUMENT_TYPE_INVALID = 0;
|
|
27
|
+
INSTRUMENT_TYPE_INDEX = 1;
|
|
28
|
+
INSTRUMENT_TYPE_STOCK = 2;
|
|
29
|
+
INSTRUMENT_TYPE_ETF = 3;
|
|
30
|
+
INSTRUMENT_TYPE_INDEX_FUT = 4;
|
|
31
|
+
INSTRUMENT_TYPE_INDEX_OPT_CE = 5;
|
|
32
|
+
INSTRUMENT_TYPE_INDEX_OPT_PE = 6;
|
|
33
|
+
INSTRUMENT_TYPE_STOCK_FUT = 7;
|
|
34
|
+
INSTRUMENT_TYPE_STOCK_OPT_CE = 8;
|
|
35
|
+
INSTRUMENT_TYPE_STOCK_OPT_PE = 9;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
enum Mode {
|
|
39
|
+
MODE_INVALID = 0;
|
|
40
|
+
MODE_LTP = 1;
|
|
41
|
+
MODE_QUOTE = 2;
|
|
42
|
+
MODE_DEPTH = 3;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Client -> server
|
|
46
|
+
|
|
47
|
+
message ClientMessage {
|
|
48
|
+
oneof payload {
|
|
49
|
+
Subscribe subscribe = 1;
|
|
50
|
+
Unsubscribe unsubscribe = 2;
|
|
51
|
+
ModeChange mode_change = 3;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
message Subscribe {
|
|
56
|
+
Mode mode = 1;
|
|
57
|
+
repeated int64 instrument_ids = 2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
message Unsubscribe {
|
|
61
|
+
repeated int64 instrument_ids = 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
message ModeChange {
|
|
65
|
+
Mode mode = 1;
|
|
66
|
+
repeated int64 instrument_ids = 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Server -> client
|
|
70
|
+
|
|
71
|
+
message ServerMessage {
|
|
72
|
+
oneof payload {
|
|
73
|
+
Ack ack = 1;
|
|
74
|
+
LtpTick ltp_tick = 2;
|
|
75
|
+
QuoteTick quote_tick = 3;
|
|
76
|
+
DepthTick depth_tick = 4;
|
|
77
|
+
StreamError error = 5;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
message Ack {
|
|
82
|
+
enum Action {
|
|
83
|
+
ACTION_INVALID = 0;
|
|
84
|
+
ACTION_SUBSCRIBE = 1;
|
|
85
|
+
ACTION_UNSUBSCRIBE = 2;
|
|
86
|
+
ACTION_MODE_CHANGE = 3;
|
|
87
|
+
}
|
|
88
|
+
Action action = 1;
|
|
89
|
+
repeated int64 instrument_ids = 2;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
message StreamError {
|
|
93
|
+
string code = 1;
|
|
94
|
+
string message = 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
message LtpTick {
|
|
98
|
+
int64 instrument_id = 1;
|
|
99
|
+
ExchangeSegment exchange_segment = 2;
|
|
100
|
+
InstrumentType instrument_type = 3;
|
|
101
|
+
int64 last_trade_price = 4; // paise
|
|
102
|
+
optional int64 last_trade_time = 5; // epoch ms; absent for indices
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
message QuoteTick {
|
|
106
|
+
int64 instrument_id = 1;
|
|
107
|
+
ExchangeSegment exchange_segment = 2;
|
|
108
|
+
InstrumentType instrument_type = 3;
|
|
109
|
+
int64 last_trade_price = 4;
|
|
110
|
+
optional int64 last_trade_time = 5;
|
|
111
|
+
int64 volume_today = 6;
|
|
112
|
+
int64 dpr_upper = 7;
|
|
113
|
+
int64 dpr_lower = 8;
|
|
114
|
+
optional int64 prev_open_interest = 9; // derivative-only
|
|
115
|
+
optional int64 open_interest = 10; // derivative-only
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
message DepthTick {
|
|
119
|
+
int64 instrument_id = 1;
|
|
120
|
+
ExchangeSegment exchange_segment = 2;
|
|
121
|
+
InstrumentType instrument_type = 3;
|
|
122
|
+
int64 last_trade_price = 4;
|
|
123
|
+
optional int64 last_trade_time = 5;
|
|
124
|
+
int64 volume_today = 6;
|
|
125
|
+
int64 dpr_upper = 7;
|
|
126
|
+
int64 dpr_lower = 8;
|
|
127
|
+
optional int64 prev_open_interest = 9;
|
|
128
|
+
optional int64 open_interest = 10;
|
|
129
|
+
// 5 or 20 levels per side depending on the instrument's feed type (L2 vs L3).
|
|
130
|
+
repeated DepthLevel best_buys = 11;
|
|
131
|
+
repeated DepthLevel best_sells = 12;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
message DepthLevel {
|
|
135
|
+
int64 price = 1; // paise
|
|
136
|
+
int64 qty = 2;
|
|
137
|
+
int64 num_orders = 3;
|
|
138
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: marketdata.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.1
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
10
|
+
from google.protobuf.internal import builder as _builder
|
|
11
|
+
# @@protoc_insertion_point(imports)
|
|
12
|
+
|
|
13
|
+
_sym_db = _symbol_database.Default()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
17
|
+
b'\n\x10marketdata.proto\x12\x19sahi.direct.marketdata.v1"\xd2\x01\n\rClientMessage\x12\x39\n\tsubscribe\x18\x01 \x01(\x0b\x32$.sahi.direct.marketdata.v1.SubscribeH\x00\x12=\n\x0bunsubscribe\x18\x02 \x01(\x0b\x32&.sahi.direct.marketdata.v1.UnsubscribeH\x00\x12<\n\x0bmode_change\x18\x03 \x01(\x0b\x32%.sahi.direct.marketdata.v1.ModeChangeH\x00\x42\t\n\x07payload"R\n\tSubscribe\x12-\n\x04mode\x18\x01 \x01(\x0e\x32\x1f.sahi.direct.marketdata.v1.Mode\x12\x16\n\x0einstrument_ids\x18\x02 \x03(\x03"%\n\x0bUnsubscribe\x12\x16\n\x0einstrument_ids\x18\x01 \x03(\x03"S\n\nModeChange\x12-\n\x04mode\x18\x01 \x01(\x0e\x32\x1f.sahi.direct.marketdata.v1.Mode\x12\x16\n\x0einstrument_ids\x18\x02 \x03(\x03"\xb2\x02\n\rServerMessage\x12-\n\x03\x61\x63k\x18\x01 \x01(\x0b\x32\x1e.sahi.direct.marketdata.v1.AckH\x00\x12\x36\n\x08ltp_tick\x18\x02 \x01(\x0b\x32".sahi.direct.marketdata.v1.LtpTickH\x00\x12:\n\nquote_tick\x18\x03 \x01(\x0b\x32$.sahi.direct.marketdata.v1.QuoteTickH\x00\x12:\n\ndepth_tick\x18\x04 \x01(\x0b\x32$.sahi.direct.marketdata.v1.DepthTickH\x00\x12\x37\n\x05\x65rror\x18\x05 \x01(\x0b\x32&.sahi.direct.marketdata.v1.StreamErrorH\x00\x42\t\n\x07payload"\xb8\x01\n\x03\x41\x63k\x12\x35\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32%.sahi.direct.marketdata.v1.Ack.Action\x12\x16\n\x0einstrument_ids\x18\x02 \x03(\x03"b\n\x06\x41\x63tion\x12\x12\n\x0e\x41\x43TION_INVALID\x10\x00\x12\x14\n\x10\x41\x43TION_SUBSCRIBE\x10\x01\x12\x16\n\x12\x41\x43TION_UNSUBSCRIBE\x10\x02\x12\x16\n\x12\x41\x43TION_MODE_CHANGE\x10\x03",\n\x0bStreamError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x0f\n\x07message\x18\x02 \x01(\t"\xf6\x01\n\x07LtpTick\x12\x15\n\rinstrument_id\x18\x01 \x01(\x03\x12\x44\n\x10\x65xchange_segment\x18\x02 \x01(\x0e\x32*.sahi.direct.marketdata.v1.ExchangeSegment\x12\x42\n\x0finstrument_type\x18\x03 \x01(\x0e\x32).sahi.direct.marketdata.v1.InstrumentType\x12\x18\n\x10last_trade_price\x18\x04 \x01(\x03\x12\x1c\n\x0flast_trade_time\x18\x05 \x01(\x03H\x00\x88\x01\x01\x42\x12\n\x10_last_trade_time"\x9a\x03\n\tQuoteTick\x12\x15\n\rinstrument_id\x18\x01 \x01(\x03\x12\x44\n\x10\x65xchange_segment\x18\x02 \x01(\x0e\x32*.sahi.direct.marketdata.v1.ExchangeSegment\x12\x42\n\x0finstrument_type\x18\x03 \x01(\x0e\x32).sahi.direct.marketdata.v1.InstrumentType\x12\x18\n\x10last_trade_price\x18\x04 \x01(\x03\x12\x1c\n\x0flast_trade_time\x18\x05 \x01(\x03H\x00\x88\x01\x01\x12\x14\n\x0cvolume_today\x18\x06 \x01(\x03\x12\x11\n\tdpr_upper\x18\x07 \x01(\x03\x12\x11\n\tdpr_lower\x18\x08 \x01(\x03\x12\x1f\n\x12prev_open_interest\x18\t \x01(\x03H\x01\x88\x01\x01\x12\x1a\n\ropen_interest\x18\n \x01(\x03H\x02\x88\x01\x01\x42\x12\n\x10_last_trade_timeB\x15\n\x13_prev_open_interestB\x10\n\x0e_open_interest"\x8f\x04\n\tDepthTick\x12\x15\n\rinstrument_id\x18\x01 \x01(\x03\x12\x44\n\x10\x65xchange_segment\x18\x02 \x01(\x0e\x32*.sahi.direct.marketdata.v1.ExchangeSegment\x12\x42\n\x0finstrument_type\x18\x03 \x01(\x0e\x32).sahi.direct.marketdata.v1.InstrumentType\x12\x18\n\x10last_trade_price\x18\x04 \x01(\x03\x12\x1c\n\x0flast_trade_time\x18\x05 \x01(\x03H\x00\x88\x01\x01\x12\x14\n\x0cvolume_today\x18\x06 \x01(\x03\x12\x11\n\tdpr_upper\x18\x07 \x01(\x03\x12\x11\n\tdpr_lower\x18\x08 \x01(\x03\x12\x1f\n\x12prev_open_interest\x18\t \x01(\x03H\x01\x88\x01\x01\x12\x1a\n\ropen_interest\x18\n \x01(\x03H\x02\x88\x01\x01\x12\x38\n\tbest_buys\x18\x0b \x03(\x0b\x32%.sahi.direct.marketdata.v1.DepthLevel\x12\x39\n\nbest_sells\x18\x0c \x03(\x0b\x32%.sahi.direct.marketdata.v1.DepthLevelB\x12\n\x10_last_trade_timeB\x15\n\x13_prev_open_interestB\x10\n\x0e_open_interest"<\n\nDepthLevel\x12\r\n\x05price\x18\x01 \x01(\x03\x12\x0b\n\x03qty\x18\x02 \x01(\x03\x12\x12\n\nnum_orders\x18\x03 \x01(\x03*\x97\x01\n\x0f\x45xchangeSegment\x12\x1c\n\x18\x45XCHANGE_SEGMENT_INVALID\x10\x00\x12\x18\n\x14\x45XCHANGE_SEGMENT_NSE\x10\x01\x12\x18\n\x14\x45XCHANGE_SEGMENT_BSE\x10\x02\x12\x18\n\x14\x45XCHANGE_SEGMENT_NFO\x10\x03\x12\x18\n\x14\x45XCHANGE_SEGMENT_BFO\x10\x04*\xc2\x02\n\x0eInstrumentType\x12\x1b\n\x17INSTRUMENT_TYPE_INVALID\x10\x00\x12\x19\n\x15INSTRUMENT_TYPE_INDEX\x10\x01\x12\x19\n\x15INSTRUMENT_TYPE_STOCK\x10\x02\x12\x17\n\x13INSTRUMENT_TYPE_ETF\x10\x03\x12\x1d\n\x19INSTRUMENT_TYPE_INDEX_FUT\x10\x04\x12 \n\x1cINSTRUMENT_TYPE_INDEX_OPT_CE\x10\x05\x12 \n\x1cINSTRUMENT_TYPE_INDEX_OPT_PE\x10\x06\x12\x1d\n\x19INSTRUMENT_TYPE_STOCK_FUT\x10\x07\x12 \n\x1cINSTRUMENT_TYPE_STOCK_OPT_CE\x10\x08\x12 \n\x1cINSTRUMENT_TYPE_STOCK_OPT_PE\x10\t*F\n\x04Mode\x12\x10\n\x0cMODE_INVALID\x10\x00\x12\x0c\n\x08MODE_LTP\x10\x01\x12\x0e\n\nMODE_QUOTE\x10\x02\x12\x0e\n\nMODE_DEPTH\x10\x03\x62\x06proto3'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_globals = globals()
|
|
21
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
22
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, "marketdata_pb2", _globals)
|
|
23
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
24
|
+
DESCRIPTOR._options = None
|
|
25
|
+
_globals["_EXCHANGESEGMENT"]._serialized_start = 2265
|
|
26
|
+
_globals["_EXCHANGESEGMENT"]._serialized_end = 2416
|
|
27
|
+
_globals["_INSTRUMENTTYPE"]._serialized_start = 2419
|
|
28
|
+
_globals["_INSTRUMENTTYPE"]._serialized_end = 2741
|
|
29
|
+
_globals["_MODE"]._serialized_start = 2743
|
|
30
|
+
_globals["_MODE"]._serialized_end = 2813
|
|
31
|
+
_globals["_CLIENTMESSAGE"]._serialized_start = 48
|
|
32
|
+
_globals["_CLIENTMESSAGE"]._serialized_end = 258
|
|
33
|
+
_globals["_SUBSCRIBE"]._serialized_start = 260
|
|
34
|
+
_globals["_SUBSCRIBE"]._serialized_end = 342
|
|
35
|
+
_globals["_UNSUBSCRIBE"]._serialized_start = 344
|
|
36
|
+
_globals["_UNSUBSCRIBE"]._serialized_end = 381
|
|
37
|
+
_globals["_MODECHANGE"]._serialized_start = 383
|
|
38
|
+
_globals["_MODECHANGE"]._serialized_end = 466
|
|
39
|
+
_globals["_SERVERMESSAGE"]._serialized_start = 469
|
|
40
|
+
_globals["_SERVERMESSAGE"]._serialized_end = 775
|
|
41
|
+
_globals["_ACK"]._serialized_start = 778
|
|
42
|
+
_globals["_ACK"]._serialized_end = 962
|
|
43
|
+
_globals["_ACK_ACTION"]._serialized_start = 864
|
|
44
|
+
_globals["_ACK_ACTION"]._serialized_end = 962
|
|
45
|
+
_globals["_STREAMERROR"]._serialized_start = 964
|
|
46
|
+
_globals["_STREAMERROR"]._serialized_end = 1008
|
|
47
|
+
_globals["_LTPTICK"]._serialized_start = 1011
|
|
48
|
+
_globals["_LTPTICK"]._serialized_end = 1257
|
|
49
|
+
_globals["_QUOTETICK"]._serialized_start = 1260
|
|
50
|
+
_globals["_QUOTETICK"]._serialized_end = 1670
|
|
51
|
+
_globals["_DEPTHTICK"]._serialized_start = 1673
|
|
52
|
+
_globals["_DEPTHTICK"]._serialized_end = 2200
|
|
53
|
+
_globals["_DEPTHLEVEL"]._serialized_start = 2202
|
|
54
|
+
_globals["_DEPTHLEVEL"]._serialized_end = 2262
|
|
55
|
+
# @@protoc_insertion_point(module_scope)
|