quantplay 2.1.136__tar.gz → 2.1.138__tar.gz

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.
Files changed (99) hide show
  1. {quantplay-2.1.136 → quantplay-2.1.138}/PKG-INFO +1 -1
  2. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/broker_factory.py +4 -3
  3. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/generics/broker.py +11 -0
  4. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/jainam_xts.py +11 -0
  5. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/ConnectV2.py +3 -2
  6. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/PKG-INFO +1 -1
  7. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/SOURCES.txt +2 -1
  8. {quantplay-2.1.136 → quantplay-2.1.138}/setup.py +1 -1
  9. {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_jainam_xts.py +2 -0
  10. quantplay-2.1.138/tests/test_sub_client_all_types.py +151 -0
  11. quantplay-2.1.138/tests/test_xts_v2_modify_order.py +37 -0
  12. quantplay-2.1.136/tests/test_instrument_freshness.py +0 -347
  13. {quantplay-2.1.136 → quantplay-2.1.138}/README.md +0 -0
  14. {quantplay-2.1.136 → quantplay-2.1.138}/pyproject.toml +0 -0
  15. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/__init__.py +0 -0
  16. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/__init__.py +0 -0
  17. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue.py +0 -0
  18. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_utils/__init__.py +0 -0
  19. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_utils/aliceblue_oauth.py +0 -0
  20. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_v2.py +0 -0
  21. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone.py +0 -0
  22. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone_utils/__init__.py +0 -0
  23. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone_utils/angeloneWS.py +0 -0
  24. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/auto_login/__init__.py +0 -0
  25. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/auto_login/aliceblue.py +0 -0
  26. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/breeze/__init__.py +0 -0
  27. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/breeze/breeze_utils.py +0 -0
  28. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/dhan.py +0 -0
  29. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/__init__.py +0 -0
  30. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/fa_noren.py +0 -0
  31. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/finvasia_oauth.py +0 -0
  32. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/five_paisa.py +0 -0
  33. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/flattrade.py +0 -0
  34. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/__init__.py +0 -0
  35. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/flattrade_utils.py +0 -0
  36. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/ft_noren.py +0 -0
  37. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/generics/__init__.py +0 -0
  38. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/icici_direct.py +0 -0
  39. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/iifl_xts.py +0 -0
  40. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kite_utils.py +0 -0
  41. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak.py +0 -0
  42. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/__init__.py +0 -0
  43. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/kotak_ws.py +0 -0
  44. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/kotak_ws_lib.py +0 -0
  45. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/motilal.py +0 -0
  46. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/noren.py +0 -0
  47. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/shoonya.py +0 -0
  48. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/uplink/__init__.py +0 -0
  49. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/uplink/uplink_utils.py +0 -0
  50. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/upstox.py +0 -0
  51. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts.py +0 -0
  52. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/Connect.py +0 -0
  53. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/Exception.py +0 -0
  54. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/InteractiveSocketClient.py +0 -0
  55. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/__init__.py +0 -0
  56. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/InteractiveSocketClientV2.py +0 -0
  57. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/__init__.py +0 -0
  58. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/zerodha.py +0 -0
  59. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/core/__init__.py +0 -0
  60. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/core/strategy.py +0 -0
  61. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/exception/__init__.py +0 -0
  62. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/exception/exceptions.py +0 -0
  63. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/indicator/__init__.py +0 -0
  64. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/indicator/iv.py +0 -0
  65. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/__init__.py +0 -0
  66. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/broker.py +0 -0
  67. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/broker_response.py +0 -0
  68. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/generics.py +0 -0
  69. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/instrument_data.py +0 -0
  70. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/order_event.py +0 -0
  71. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/py.typed +0 -0
  72. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/__init__.py +0 -0
  73. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/iv_spike.py +0 -0
  74. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/obuy.py +0 -0
  75. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/__init__.py +0 -0
  76. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/caching.py +0 -0
  77. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/constant.py +0 -0
  78. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/exchange.py +0 -0
  79. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/number_utils.py +0 -0
  80. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/pickle_utils.py +0 -0
  81. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/redaction.py +0 -0
  82. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/selenium_utils.py +0 -0
  83. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/__init__.py +0 -0
  84. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/aws/__init__.py +0 -0
  85. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/aws/s3.py +0 -0
  86. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/redis.py +0 -0
  87. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/dependency_links.txt +0 -0
  88. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/requires.txt +0 -0
  89. {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/top_level.txt +0 -0
  90. {quantplay-2.1.136 → quantplay-2.1.138}/setup.cfg +0 -0
  91. {quantplay-2.1.136 → quantplay-2.1.138}/tests/__init__.py +0 -0
  92. {quantplay-2.1.136 → quantplay-2.1.138}/tests/conftest.py +0 -0
  93. {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_deltax_positions.py +0 -0
  94. {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_deltax_root_url.py +0 -0
  95. {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_kotak_order_feed_url.py +0 -0
  96. {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_log_redaction.py +0 -0
  97. {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/__init__.py +0 -0
  98. {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/aws/__init__.py +0 -0
  99. {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/aws/s3_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantplay
3
- Version: 2.1.136
3
+ Version: 2.1.138
4
4
  Summary: This python package will be stored in AWS CodeArtifact
5
5
  Home-page:
6
6
  Author:
@@ -497,12 +497,13 @@ class BrokerFactory:
497
497
  )
498
498
  broker_data["user_id"] = broker_client.wrapper.userID
499
499
 
500
- # Type C dealers must place orders as this sub-client. The login
500
+ # Type C dealers must place orders as this sub-client. Captured for
501
+ # every XTS type so reclassifying an account to C later works. The login
501
502
  # response is its only reliable source, so persist it for sessions
502
503
  # later restored from the stored wrapper (the dealer orderbook
503
504
  # fallback is empty every morning until the first order exists).
504
- if broker_client.dealer_client_id:
505
- broker_data["sub_client_id"] = broker_client.dealer_client_id
505
+ if broker_client.resolved_sub_client_id:
506
+ broker_data["sub_client_id"] = broker_client.resolved_sub_client_id
506
507
 
507
508
  broker_data["wrapper"] = codecs.encode(
508
509
  pickle.dumps(broker_client.wrapper), "base64"
@@ -910,6 +910,17 @@ class Broker(ABC):
910
910
  sleep_time: float = 10,
911
911
  max_modification_count: int = 10,
912
912
  ) -> None:
913
+ # TODO: think hard about how an exit that won't fill should end. Today:
914
+ # - skip_first_sleep is only cleared inside the branch it guards, so the
915
+ # loop never sleeps and its re-prices run back to back.
916
+ # - Once the count exceeds max_modification_count it modifies to MARKET,
917
+ # which brokers refuse from the API.
918
+ # - The "> 20" cancel only runs when max_modification_count is above 20.
919
+ # Every caller passes 10 or less, so in practice the loop has no exit
920
+ # if the order never completes.
921
+ # Widening the LIMIT buffer instead isn't a fix: exchange price bands
922
+ # reject prices too far from the LTP. In practice exits fill within the
923
+ # first few re-prices, so this is left as is for now.
913
924
  modification_count = {}
914
925
  skip_first_sleep = True
915
926
  while 1:
@@ -131,6 +131,17 @@ class Jainam(XTS):
131
131
 
132
132
  self.ORDER_POLLING_INTERVAL = 5
133
133
 
134
+ @property
135
+ def resolved_sub_client_id(self) -> str | None:
136
+ """The sub-client code discovered for this account, if any.
137
+
138
+ The login response carries `clientCodes` for every XTS type, but only
139
+ type C dealers resolve it into `dealer_client_id`. Exposing both means
140
+ generate_token can persist the code for A and B accounts too, so an
141
+ account later reclassified as type C already has what it needs.
142
+ """
143
+ return self.dealer_client_id or self._login_client_code
144
+
134
145
  @property
135
146
  def order_client_id(self) -> str:
136
147
  """Client ID for write operations.
@@ -406,7 +406,7 @@ class XTSConnectV2(XTSCommon):
406
406
 
407
407
  def modify_order(
408
408
  self,
409
- appOrderID: int,
409
+ appOrderID: int | str,
410
410
  modifiedProductType: XTSTypes.ProductType,
411
411
  modifiedOrderType: XTSTypes.OrderType,
412
412
  modifiedOrderQuantity: int,
@@ -420,7 +420,8 @@ class XTSConnectV2(XTSCommon):
420
420
  """The facility to modify your open orders by allowing you to change limit order to market or vice versa,
421
421
  change Price or Quantity of the limit open order, change disclosed quantity or stop-loss of any
422
422
  open stop loss order."""
423
- appOrderID = int(appOrderID)
423
+ # order.modify rejects a numeric appOrderID ("must be a string")
424
+ appOrderID = str(appOrderID)
424
425
  params: dict[str, Any] = {
425
426
  "appOrderID": appOrderID,
426
427
  "modifiedProductType": modifiedProductType,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantplay
3
- Version: 2.1.136
3
+ Version: 2.1.138
4
4
  Summary: This python package will be stored in AWS CodeArtifact
5
5
  Home-page:
6
6
  Author:
@@ -86,10 +86,11 @@ tests/__init__.py
86
86
  tests/conftest.py
87
87
  tests/test_deltax_positions.py
88
88
  tests/test_deltax_root_url.py
89
- tests/test_instrument_freshness.py
90
89
  tests/test_jainam_xts.py
91
90
  tests/test_kotak_order_feed_url.py
92
91
  tests/test_log_redaction.py
92
+ tests/test_sub_client_all_types.py
93
+ tests/test_xts_v2_modify_order.py
93
94
  tests/wrapper/__init__.py
94
95
  tests/wrapper/aws/__init__.py
95
96
  tests/wrapper/aws/s3_test.py
@@ -21,7 +21,7 @@ requirements = [
21
21
  setup(
22
22
  name="quantplay",
23
23
  long_description=Path("README.md").read_text(),
24
- version="2.1.136",
24
+ version="2.1.138",
25
25
  install_requires=requirements,
26
26
  tests_require=[],
27
27
  packages=find_packages(),
@@ -175,6 +175,7 @@ def test_generate_token_persists_sub_client_id_in_broker_data(
175
175
  wrapper=types.SimpleNamespace(userID=DEALER_ID),
176
176
  md_wrapper=types.SimpleNamespace(),
177
177
  dealer_client_id=SUB_CLIENT_ID,
178
+ resolved_sub_client_id=SUB_CLIENT_ID,
178
179
  )
179
180
  monkeypatch.setattr(broker_factory, "Jainam", lambda **kwargs: stub)
180
181
 
@@ -209,6 +210,7 @@ def test_generate_token_omits_sub_client_id_when_unresolved(
209
210
  wrapper=types.SimpleNamespace(userID=DEALER_ID),
210
211
  md_wrapper=types.SimpleNamespace(),
211
212
  dealer_client_id=None,
213
+ resolved_sub_client_id=None,
212
214
  )
213
215
  monkeypatch.setattr(broker_factory, "Jainam", lambda **kwargs: stub)
214
216
 
@@ -0,0 +1,151 @@
1
+ """The sub-client code must be captured for every XTS type.
2
+
3
+ Jainam resolves it into dealer_client_id only for type C dealers, but the
4
+ login response carries clientCodes for A and B too. generate_token used to
5
+ persist only dealer_client_id, so an account added as A or B had no
6
+ sub_client_id stored - and reclassifying it to C later (which is exactly
7
+ what a mis-answered "Account Type" leads to) left it unable to place an
8
+ order until someone hand-wrote the field.
9
+ """
10
+
11
+ import types
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from quantplay.broker import broker_factory, jainam_xts
17
+ from quantplay.broker.jainam_xts import Jainam
18
+
19
+ SUB = "JSR129"
20
+
21
+
22
+ class FakeConnect:
23
+ """Stands in for XTSConnectV2 so __init__ runs the real login path."""
24
+
25
+ def __init__(self, apiKey: str, secretKey: str, root: str) -> None:
26
+ self.root = root
27
+ self.token = None
28
+ self.userID = "JSR129A77"
29
+
30
+ def session_login(self, root: str) -> dict[str, Any]:
31
+ return {
32
+ "type": "success",
33
+ "result": {
34
+ "token": "t",
35
+ "userID": "JSR129A77",
36
+ "isInvestorClient": False,
37
+ "clientCodes": [SUB],
38
+ },
39
+ }
40
+
41
+ def marketdata_login(self) -> dict[str, Any]:
42
+ return {"type": "success", "result": {"token": "m"}}
43
+
44
+
45
+ def login_jainam(monkeypatch: Any, **kwargs: Any) -> Jainam:
46
+ monkeypatch.setattr(jainam_xts, "XTSConnectV2", FakeConnect)
47
+ return Jainam(
48
+ api_key="k",
49
+ api_secret="s",
50
+ md_api_key="mk",
51
+ md_api_secret="ms",
52
+ load_instrument=False,
53
+ **kwargs,
54
+ )
55
+
56
+
57
+ @pytest.mark.parametrize("xts_type", ["A", "B", "C"])
58
+ def test_login_client_code_is_exposed_for_every_xts_type(
59
+ monkeypatch: Any, xts_type: str
60
+ ):
61
+ broker = login_jainam(monkeypatch, XTS_type=xts_type, is_dealer=True)
62
+
63
+ assert broker.resolved_sub_client_id == SUB
64
+
65
+
66
+ def test_type_c_still_prefers_the_resolved_dealer_client(monkeypatch: Any):
67
+ broker = login_jainam(monkeypatch, XTS_type="C", is_dealer=True)
68
+
69
+ assert broker.dealer_client_id == SUB
70
+ assert broker.resolved_sub_client_id == SUB
71
+ assert broker.order_client_id == SUB
72
+
73
+
74
+ def test_resolved_sub_client_is_none_when_login_gives_no_client_codes(
75
+ monkeypatch: Any,
76
+ ):
77
+ class NoCodes(FakeConnect):
78
+ def session_login(self, root: str) -> dict[str, Any]:
79
+ r = super().session_login(root)
80
+ r["result"].pop("clientCodes")
81
+ return r
82
+
83
+ monkeypatch.setattr(jainam_xts, "XTSConnectV2", NoCodes)
84
+ broker = Jainam(
85
+ api_key="k",
86
+ api_secret="s",
87
+ md_api_key="mk",
88
+ md_api_secret="ms",
89
+ XTS_type="A",
90
+ is_dealer=True,
91
+ load_instrument=False,
92
+ )
93
+
94
+ assert broker.resolved_sub_client_id is None
95
+
96
+
97
+ @pytest.mark.parametrize("xts_type", ["A", "B", "C"])
98
+ def test_generate_token_persists_the_sub_client_for_every_type(
99
+ monkeypatch: Any, xts_type: str
100
+ ):
101
+ stub = types.SimpleNamespace(
102
+ wrapper=types.SimpleNamespace(userID="JSR129A77"),
103
+ md_wrapper=types.SimpleNamespace(),
104
+ dealer_client_id=SUB if xts_type == "C" else None,
105
+ resolved_sub_client_id=SUB,
106
+ )
107
+ monkeypatch.setattr(broker_factory, "Jainam", lambda **kw: stub)
108
+
109
+ result = broker_factory.BrokerFactory().generate_token(
110
+ {
111
+ "username": "SUNILYADAV",
112
+ "nickname": "JSR129A77",
113
+ "broker": "Jainam",
114
+ "broker_data": {
115
+ "api_key": "k",
116
+ "api_secret": "s",
117
+ "md_api_key": "mk",
118
+ "md_api_secret": "ms",
119
+ "is_dealer": True,
120
+ "XTS_type": xts_type,
121
+ },
122
+ }
123
+ )
124
+
125
+ assert result["broker_data"]["sub_client_id"] == SUB
126
+
127
+
128
+ def test_generate_token_omits_the_key_when_nothing_resolved(monkeypatch: Any):
129
+ stub = types.SimpleNamespace(
130
+ wrapper=types.SimpleNamespace(userID="X"),
131
+ md_wrapper=types.SimpleNamespace(),
132
+ dealer_client_id=None,
133
+ resolved_sub_client_id=None,
134
+ )
135
+ monkeypatch.setattr(broker_factory, "Jainam", lambda **kw: stub)
136
+
137
+ result = broker_factory.BrokerFactory().generate_token(
138
+ {
139
+ "username": "U",
140
+ "nickname": "N",
141
+ "broker": "Jainam",
142
+ "broker_data": {
143
+ "api_key": "k",
144
+ "api_secret": "s",
145
+ "md_api_key": "mk",
146
+ "md_api_secret": "ms",
147
+ },
148
+ }
149
+ )
150
+
151
+ assert "sub_client_id" not in result["broker_data"]
@@ -0,0 +1,37 @@
1
+ import json
2
+ from unittest.mock import MagicMock
3
+
4
+ from quantplay.broker.xts_utils_v2.ConnectV2 import XTSConnectV2
5
+
6
+ APP_ORDER_ID = "18001263010000042909"
7
+
8
+
9
+ def modify_body(app_order_id: int | str) -> dict:
10
+ connect = object.__new__(XTSConnectV2)
11
+ connect.isInvestorClient = False
12
+ connect._put = MagicMock(return_value={"type": "success"})
13
+
14
+ connect.modify_order(
15
+ appOrderID=app_order_id,
16
+ modifiedProductType="NRML",
17
+ modifiedOrderType="Limit",
18
+ modifiedOrderQuantity=90,
19
+ modifiedDisclosedQuantity=0,
20
+ modifiedLimitPrice=874.0,
21
+ modifiedStopPrice=0,
22
+ modifiedTimeInForce="DAY",
23
+ orderUniqueIdentifier="JSR129A43",
24
+ clientID="JSR129",
25
+ )
26
+
27
+ route, body = connect._put.call_args.args
28
+ assert route == "order.modify"
29
+ return json.loads(body)
30
+
31
+
32
+ def test_modify_order_sends_app_order_id_as_string():
33
+ assert modify_body(APP_ORDER_ID)["appOrderID"] == APP_ORDER_ID
34
+
35
+
36
+ def test_modify_order_stringifies_an_int_app_order_id():
37
+ assert modify_body(int(APP_ORDER_ID))["appOrderID"] == APP_ORDER_ID
@@ -1,347 +0,0 @@
1
- import os
2
- import time
3
- import uuid
4
- from datetime import datetime, timedelta, timezone
5
-
6
- import pandas as pd
7
- import pytest
8
-
9
- import quantplay.utils.pickle_utils as pickle_utils_module
10
- from quantplay.broker.broker_factory import (
11
- Broker as BrokerName,
12
- )
13
- from quantplay.broker.broker_factory import (
14
- BrokerFactory,
15
- broker_instruments_map,
16
- instrument_cache,
17
- )
18
- from quantplay.broker.generics.broker import Broker
19
- from quantplay.broker.zerodha import Zerodha
20
- from quantplay.exception.exceptions import StaleDataFound
21
- from quantplay.utils.pickle_utils import InstrumentData, PickleUtils
22
- from quantplay.wrapper.aws import s3 as s3_module
23
- from quantplay.wrapper.aws.s3 import S3Utils
24
-
25
-
26
- def unique_name(prefix: str) -> str:
27
- return f"{prefix}_{uuid.uuid4().hex[:10]}"
28
-
29
-
30
- class DummyBroker(Broker):
31
- def get_exchange(self, exchange):
32
- return exchange
33
-
34
-
35
- DummyBroker.__abstractmethods__ = frozenset()
36
-
37
-
38
- class FakeS3Client:
39
- def __init__(self, response=None, error=None):
40
- self.response = response
41
- self.error = error
42
-
43
- def head_object(self, Bucket, Key):
44
- if self.error is not None:
45
- raise self.error
46
- return self.response
47
-
48
-
49
- @pytest.fixture
50
- def cleanup_paths():
51
- paths = []
52
- yield paths
53
- for path in paths:
54
- if os.path.exists(path):
55
- os.remove(path)
56
-
57
-
58
- @pytest.fixture(autouse=True)
59
- def clear_instrument_cache():
60
- instrument_cache.clear()
61
- yield
62
- instrument_cache.clear()
63
-
64
-
65
- def make_pickle(file_name: str, data: dict) -> str:
66
- PickleUtils.save_data(data, file_name)
67
- return f"/tmp/{file_name}.pickle"
68
-
69
-
70
- OLD_SYMBOL_DATA = {
71
- "BFO:OLDSYMBOL": {
72
- "broker_symbol": "OLDSYMBOL",
73
- "tradingsymbol": "OLDSYMBOL",
74
- "lot_size": 20,
75
- }
76
- }
77
-
78
- NEW_SYMBOL_DATA = {
79
- "BFO:SENSEX2671678400CE": {
80
- "broker_symbol": "SENSEX2671678400CE",
81
- "tradingsymbol": "SENSEX2671678400CE",
82
- "lot_size": 20,
83
- }
84
- }
85
-
86
-
87
- class TestS3LastModified:
88
- def test_returns_datetime_on_success(self, monkeypatch):
89
- expected = datetime(2026, 7, 16, 3, 16, tzinfo=timezone.utc)
90
- monkeypatch.setattr(
91
- s3_module.boto3,
92
- "client",
93
- lambda service: FakeS3Client(response={"LastModified": expected}),
94
- )
95
- assert S3Utils.last_modified("bucket", "key") == expected
96
-
97
- def test_returns_none_on_failure(self, monkeypatch):
98
- monkeypatch.setattr(
99
- s3_module.boto3,
100
- "client",
101
- lambda service: FakeS3Client(error=Exception("404 Not Found")),
102
- )
103
- assert S3Utils.last_modified("bucket", "key") is None
104
-
105
-
106
- class TestPickleUtils:
107
- def test_save_data_is_atomic_on_failure(self, monkeypatch, cleanup_paths):
108
- file_name = unique_name("atomic")
109
- cleanup_paths.append(make_pickle(file_name, {"a": 1}))
110
-
111
- def boom(*args, **kwargs):
112
- raise RuntimeError("disk full")
113
-
114
- monkeypatch.setattr(pickle_utils_module.pickle, "dump", boom)
115
- with pytest.raises(Exception):
116
- PickleUtils.save_data({"b": 2}, file_name)
117
- monkeypatch.undo()
118
-
119
- assert PickleUtils.load_data(file_name) == {"a": 1}
120
-
121
- def test_instrument_data_reloads_when_pickle_changes(self, cleanup_paths):
122
- file_name = unique_name("reload")
123
- path = make_pickle(file_name, {"NSE:A": {"lot_size": 1}})
124
- cleanup_paths.append(path)
125
-
126
- instance = InstrumentData.get_instance()
127
- assert instance.load_data(file_name) == {"NSE:A": {"lot_size": 1}}
128
-
129
- make_pickle(file_name, {"NSE:B": {"lot_size": 2}})
130
- new_time = time.time() + 10
131
- os.utime(path, (new_time, new_time))
132
-
133
- assert instance.load_data(file_name) == {"NSE:B": {"lot_size": 2}}
134
-
135
- def test_instrument_data_serves_memory_copy_when_unchanged(self, cleanup_paths):
136
- file_name = unique_name("cached")
137
- path = make_pickle(file_name, {"NSE:A": {"lot_size": 1}})
138
- cleanup_paths.append(path)
139
-
140
- instance = InstrumentData.get_instance()
141
- first = instance.load_data(file_name)
142
- second = instance.load_data(file_name)
143
- assert first is second
144
-
145
-
146
- class TestGetLotSize:
147
- def test_missing_symbol_raises_stale_data_found(self):
148
- broker = DummyBroker()
149
- broker.symbol_data = {}
150
-
151
- with pytest.raises(StaleDataFound) as exc_info:
152
- broker.get_lot_size("BFO", "SENSEX2671678400CE")
153
-
154
- assert "BFO:SENSEX2671678400CE" in str(exc_info.value)
155
-
156
- def test_returns_lot_size_when_symbol_exists(self):
157
- broker = DummyBroker()
158
- broker.symbol_data = NEW_SYMBOL_DATA
159
-
160
- assert broker.get_lot_size("BFO", "SENSEX2671678400CE") == 20
161
-
162
-
163
- class TestPickleFreshness:
164
- def setup_method(self):
165
- self.factory = BrokerFactory()
166
-
167
- def test_missing_pickle_is_stale(self, monkeypatch):
168
- monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
169
- assert self.factory.is_instrument_pickle_fresh(unique_name("missing")) is False
170
-
171
- def test_pickle_from_today_with_no_s3_csv_is_fresh(
172
- self, monkeypatch, cleanup_paths
173
- ):
174
- file_name = unique_name("fresh")
175
- cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
176
- monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
177
-
178
- assert self.factory.is_instrument_pickle_fresh(file_name) is True
179
-
180
- def test_pickle_from_yesterday_is_stale(self, monkeypatch, cleanup_paths):
181
- file_name = unique_name("stale_date")
182
- path = make_pickle(file_name, OLD_SYMBOL_DATA)
183
- cleanup_paths.append(path)
184
- yesterday = time.time() - 86400
185
- os.utime(path, (yesterday, yesterday))
186
- monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
187
-
188
- assert self.factory.is_instrument_pickle_fresh(file_name) is False
189
-
190
- def test_pickle_older_than_s3_csv_is_stale(self, monkeypatch, cleanup_paths):
191
- file_name = unique_name("stale_s3")
192
- cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
193
- s3_time = datetime.now(timezone.utc) + timedelta(hours=1)
194
- monkeypatch.setattr(
195
- S3Utils, "last_modified", staticmethod(lambda b, k: s3_time)
196
- )
197
-
198
- assert self.factory.is_instrument_pickle_fresh(file_name) is False
199
-
200
- def test_pickle_newer_than_s3_csv_is_fresh(self, monkeypatch, cleanup_paths):
201
- file_name = unique_name("fresh_s3")
202
- cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
203
- s3_time = datetime.now(timezone.utc) - timedelta(hours=2)
204
- monkeypatch.setattr(
205
- S3Utils, "last_modified", staticmethod(lambda b, k: s3_time)
206
- )
207
-
208
- assert self.factory.is_instrument_pickle_fresh(file_name) is True
209
-
210
-
211
- class TestSetBrokerInstruments:
212
- def setup_method(self):
213
- self.factory = BrokerFactory()
214
-
215
- def _stale_pickle_setup(self, monkeypatch, cleanup_paths):
216
- file_name = unique_name("alicebluev2_test")
217
- monkeypatch.setitem(
218
- broker_instruments_map, BrokerName.ALICEBLUE_V2, file_name
219
- )
220
- path = make_pickle(file_name, OLD_SYMBOL_DATA)
221
- cleanup_paths.append(path)
222
- yesterday = time.time() - 86400
223
- os.utime(path, (yesterday, yesterday))
224
- monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
225
- return file_name
226
-
227
- def test_stale_pickle_triggers_refresh(self, monkeypatch, cleanup_paths):
228
- file_name = self._stale_pickle_setup(monkeypatch, cleanup_paths)
229
- broker = DummyBroker()
230
- refresh_calls = []
231
-
232
- def fake_refresh(name):
233
- refresh_calls.append(name)
234
- broker.symbol_data = NEW_SYMBOL_DATA
235
- broker.initialize_broker_symbol_map()
236
-
237
- monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
238
-
239
- result = self.factory.set_broker_instruments(
240
- broker_name=BrokerName.ALICEBLUE_V2, broker=broker
241
- )
242
-
243
- assert refresh_calls == [file_name]
244
- assert "BFO:SENSEX2671678400CE" in result.symbol_data
245
- assert (
246
- instrument_cache.get(f"{BrokerName.ALICEBLUE_V2}_instruments")
247
- == NEW_SYMBOL_DATA
248
- )
249
-
250
- def test_refresh_failure_serves_stale_pickle(self, monkeypatch, cleanup_paths):
251
- self._stale_pickle_setup(monkeypatch, cleanup_paths)
252
- broker = DummyBroker()
253
-
254
- def fake_refresh(name):
255
- raise Exception("s3 is down")
256
-
257
- monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
258
-
259
- result = self.factory.set_broker_instruments(
260
- broker_name=BrokerName.ALICEBLUE_V2, broker=broker
261
- )
262
-
263
- assert "BFO:OLDSYMBOL" in result.symbol_data
264
-
265
- def test_fresh_pickle_skips_refresh(self, monkeypatch, cleanup_paths):
266
- file_name = unique_name("alicebluev2_test")
267
- monkeypatch.setitem(
268
- broker_instruments_map, BrokerName.ALICEBLUE_V2, file_name
269
- )
270
- cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
271
- monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
272
-
273
- broker = DummyBroker()
274
-
275
- def fake_refresh(name):
276
- raise AssertionError("refresh_instruments should not be called")
277
-
278
- monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
279
-
280
- result = self.factory.set_broker_instruments(
281
- broker_name=BrokerName.ALICEBLUE_V2, broker=broker
282
- )
283
-
284
- assert "BFO:OLDSYMBOL" in result.symbol_data
285
-
286
-
287
- class TestRefreshInstruments:
288
- def test_rebuilds_from_s3_and_updates_all_caches(self, monkeypatch, cleanup_paths):
289
- file_name = unique_name("refresh_base")
290
- cleanup_paths.append(f"/tmp/{file_name}.pickle")
291
-
292
- local_csv = f"/tmp/quantplay-market-data/symbol_data/{file_name}.csv"
293
- os.makedirs(os.path.dirname(local_csv), exist_ok=True)
294
- with open(local_csv, "w") as f:
295
- f.write("stale,csv\n")
296
- cleanup_paths.append(local_csv)
297
-
298
- df = pd.DataFrame(
299
- [
300
- {
301
- "exchange": "BFO",
302
- "broker_symbol": "SENSEX2671678400CE",
303
- "tradingsymbol": "SENSEX2671678400CE",
304
- "lot_size": 20,
305
- }
306
- ]
307
- )
308
-
309
- def fake_read_csv(bucket, key):
310
- assert bucket == "quantplay-market-data"
311
- assert key == f"symbol_data/{file_name}.csv"
312
- return df
313
-
314
- monkeypatch.setattr(S3Utils, "read_csv", staticmethod(fake_read_csv))
315
-
316
- broker = DummyBroker()
317
- broker.refresh_instruments(file_name)
318
-
319
- assert "BFO:SENSEX2671678400CE" in broker.symbol_data
320
- assert broker.symbol_data["BFO:SENSEX2671678400CE"]["lot_size"] == 20
321
- assert not os.path.exists(local_csv)
322
- assert "BFO:SENSEX2671678400CE" in PickleUtils.load_data(file_name)
323
- assert (
324
- InstrumentData.get_instance().load_data(file_name) == broker.symbol_data
325
- )
326
- assert broker.broker_symbol_map["SENSEX2671678400CE"] == "SENSEX2671678400CE"
327
-
328
- def test_zerodha_rebuilds_from_live_instruments(self, cleanup_paths):
329
- file_name = unique_name("zerodha_refresh")
330
- cleanup_paths.append(f"/tmp/{file_name}.pickle")
331
-
332
- class FakeWrapper:
333
- def instruments(self):
334
- return [
335
- {
336
- "exchange": "NFO",
337
- "tradingsymbol": "NIFTY26JUL25000CE",
338
- "lot_size": 75,
339
- }
340
- ]
341
-
342
- broker = object.__new__(Zerodha)
343
- broker.wrapper = FakeWrapper()
344
- broker.refresh_instruments(file_name)
345
-
346
- assert broker.symbol_data["NFO:NIFTY26JUL25000CE"]["lot_size"] == 75
347
- assert "NFO:NIFTY26JUL25000CE" in PickleUtils.load_data(file_name)
File without changes
File without changes
File without changes