tradingapi 0.1.2__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.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: tradingapi
3
+ Version: 0.1.2
4
+ Summary: Trade integration with brokers
5
+ Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://bitbucket.org/incurrency/tradingpapi2
8
+ Project-URL: repository, https://bitbucket.org/incurrency/tradingapi2
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: chameli
11
+ Requires-Dist: NorenRestApi==0.0.32
12
+ Requires-Dist: py5paisa==0.7.19
13
+ Requires-Dist: redis==5.0.4
14
+ Requires-Dist: pyotp==2.9.0
@@ -0,0 +1,47 @@
1
+ [tool.black]
2
+ line-length = 88
3
+
4
+ [tool.flake8]
5
+ max-line-length = 88
6
+
7
+ [tool.mypy]
8
+ ignore_missing_imports = true
9
+
10
+ [[tool.mypy.overrides]]
11
+ module = ["yaml", "pytz","requests"]
12
+ ignore_missing_imports = true
13
+
14
+ [[tool.mypy.overrides]]
15
+ module = ["numpy", "pyreadr", "rpy2", "pandas", "scipy","redis"]
16
+ ignore_missing_imports = true
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=42", "wheel"]
20
+ build-backend = "setuptools.build_meta"
21
+
22
+ [tool.setuptools]
23
+ include-package-data = true
24
+
25
+ [tool.setuptools.package-data]
26
+ "tradingapi2" = ["config/config_sample.yaml", "config/commissions_20241216.yaml"]
27
+
28
+ [project]
29
+ name = "tradingapi"
30
+ version = "0.1.2"
31
+ description = "Trade integration with brokers"
32
+ readme = "README.md"
33
+ license = "MIT"
34
+ authors = [
35
+ { name = "Pankaj Sharma", email = "sharma.pankaj.kumar@gmail.com" }
36
+ ]
37
+ dependencies = [
38
+ "chameli",
39
+ "NorenRestApi==0.0.32",
40
+ "py5paisa==0.7.19",
41
+ "redis==5.0.4",
42
+ "pyotp==2.9.0"
43
+ ]
44
+
45
+ [project.urls]
46
+ homepage = "https://bitbucket.org/incurrency/tradingpapi2"
47
+ repository = "https://bitbucket.org/incurrency/tradingapi2"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,83 @@
1
+ # tradingAPI/__init__.py
2
+ import logging
3
+ import os
4
+ import sys
5
+ from importlib.resources import files
6
+ from logging.handlers import TimedRotatingFileHandler
7
+
8
+ sys.path.append(os.path.dirname(os.path.realpath(__file__)))
9
+ from .config import is_config_loaded, load_config
10
+
11
+
12
+ def get_default_config_path():
13
+ """Returns the path to the default config file included in the package."""
14
+ return files("tradingapi").joinpath("config/config.yaml")
15
+
16
+
17
+ def configure_logging(
18
+ module_names=None,
19
+ level=logging.WARNING,
20
+ log_file=None,
21
+ clear_existing_handlers=False,
22
+ enable_console=True,
23
+ backup_count=7,
24
+ format_string="%(asctime)s:%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s",
25
+ ):
26
+ """
27
+ Configure logging for specific modules or all modules in the tradingAPI package.
28
+
29
+ Args:
30
+ module_names (list of str): List of module names to enable logging for. If None, configure logging for all modules.
31
+ level (int): Logging level (e.g., logging.DEBUG, logging.INFO).
32
+ log_file (str): Path to the log file. If None, logs will go to the console.
33
+ clear_existing_handlers (bool): Whether to clear existing handlers from the root logger.
34
+ enable_console (bool): Whether console logging is enabled
35
+ backup_count (int): number of log files to keep
36
+ """
37
+ if clear_existing_handlers:
38
+ for handler in logging.root.handlers[:]:
39
+ logging.root.removeHandler(handler)
40
+
41
+ # Create handlers
42
+ handlers = []
43
+ formatter = logging.Formatter(format_string)
44
+ if log_file:
45
+ file_handler = TimedRotatingFileHandler(log_file, when="midnight", backupCount=backup_count)
46
+ file_handler.suffix = "%Y%m%d"
47
+ file_handler.setLevel(level)
48
+ file_handler.setFormatter(formatter)
49
+ handlers.append(file_handler)
50
+
51
+ if enable_console:
52
+ console_handler = logging.StreamHandler()
53
+ console_handler.setLevel(level)
54
+ console_handler.setFormatter(formatter)
55
+ handlers.append(console_handler)
56
+
57
+ # Configure logging for specific modules or globally
58
+ if module_names:
59
+ for module_name in module_names:
60
+ logger = logging.getLogger(module_name)
61
+ logger.setLevel(level)
62
+ for handler in handlers:
63
+ logger.addHandler(handler)
64
+ else:
65
+ # Configure root logger if no specific modules are mentioned
66
+ root_logger = logging.getLogger()
67
+ root_logger.setLevel(level)
68
+ for handler in handlers:
69
+ root_logger.addHandler(handler)
70
+
71
+
72
+ # # Set default logging level to WARNING and log to console by default
73
+ configure_logging()
74
+
75
+
76
+ def initialize_config(config_file_path: str, force_reload=True):
77
+ if is_config_loaded() and not force_reload:
78
+ raise RuntimeError("Configuration is already loaded.")
79
+ else:
80
+ load_config(config_file_path)
81
+
82
+
83
+ initialize_config(get_default_config_path())
@@ -0,0 +1,432 @@
1
+ import datetime as dt
2
+ import json
3
+ import math
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import asdict, dataclass
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Union
8
+
9
+ import pandas as pd
10
+ import redis
11
+
12
+ NEXT_DAY_TIMESTAMP = int((dt.datetime.today() + dt.timedelta(days=1)).timestamp())
13
+
14
+
15
+ class Brokers(Enum):
16
+ UNDEFINED = 1
17
+ FIVEPAISA = 2
18
+ SHOONYA = 3
19
+ INTERACTIVEBROKERS = 4
20
+ DHAN = 5
21
+ ICICIDIRECT = 6
22
+
23
+
24
+ @dataclass
25
+ class HistoricalData:
26
+ date: dt.datetime
27
+ open: float
28
+ high: float
29
+ low: float
30
+ close: float
31
+ volume: int
32
+ intoi: int
33
+ oi: int
34
+
35
+ def to_dict(self) -> dict:
36
+ return {k: v for k, v in asdict(self).items() if v is not None and not (isinstance(v, float) and math.isnan(v))}
37
+ # return asdict(self)
38
+
39
+ def __repr__(self):
40
+ return f"HistoricalData({self.__dict__})"
41
+
42
+
43
+ class OrderStatus(Enum):
44
+ UNDEFINED = 1 # No information on status
45
+ HISTORICAL = 2 # Order is from earlier days, no broker status
46
+ PENDING = 3 # Pending with Broker
47
+ REJECTED = 4 # Rejected by Broker
48
+ OPEN = 5 # active with exchange
49
+ FILLED = 6 # Filled with exchange
50
+ CANCELLED = 7 # Cancelled by Exchange
51
+
52
+
53
+ class Order:
54
+ def __init__(
55
+ self,
56
+ long_symbol: str = "",
57
+ order_type: str = "",
58
+ price_type: float = 0.0,
59
+ quantity: int = 0,
60
+ exchange: str = "",
61
+ exchange_segment: str = "",
62
+ price: float = float("nan"),
63
+ is_intraday: bool = True,
64
+ internal_order_id: str = "",
65
+ remote_order_id: str = "",
66
+ scrip_code: int = 0,
67
+ exch_order_id: str = "0",
68
+ broker_order_id: str = "0",
69
+ stoploss_price: float = 0.0,
70
+ is_stoploss_order: bool = False,
71
+ ioc_order: bool = False,
72
+ scripdata: str = "",
73
+ orderRef: str = "",
74
+ order_id: int = 0,
75
+ local_order_id: int = 0,
76
+ disqty: int = 0,
77
+ message: str = "",
78
+ status: str = "UNDEFINED",
79
+ vtd: str = f"/Date({NEXT_DAY_TIMESTAMP})/",
80
+ ahplaced: str = "N",
81
+ IsGTCOrder: bool = False,
82
+ IsEOSOrder: bool = False,
83
+ paper: bool = True,
84
+ broker: str = "UNDEFINED",
85
+ additional_info: str = "",
86
+ **kwargs: Any,
87
+ ):
88
+ """
89
+ Initialize an Order object with various attributes related to trading orders.
90
+ """
91
+ self.long_symbol = long_symbol
92
+ self.price_type = price_type
93
+ self.order_type = order_type
94
+ self.quantity = self._convert_to_int(quantity)
95
+ self.price = self._convert_to_float(price)
96
+ self.exchange = exchange
97
+ self.exchange_segment = exchange_segment
98
+ self.internal_order_id = internal_order_id
99
+ self.remote_order_id = remote_order_id
100
+ self.scrip_code = self._convert_to_int(scrip_code)
101
+ self.exch_order_id = exch_order_id
102
+ self.broker_order_id = broker_order_id
103
+ self.stoploss_price = self._convert_to_float(stoploss_price)
104
+ self.is_stoploss_order = self._convert_to_bool(is_stoploss_order)
105
+ self.ioc_order = self._convert_to_bool(ioc_order)
106
+ self.scripdata = scripdata
107
+ self.orderRef = orderRef
108
+ self.order_id = self._convert_to_int(order_id)
109
+ self.local_order_id = self._convert_to_int(local_order_id)
110
+ self.disqty = self._convert_to_int(disqty)
111
+ self.message = message
112
+ self.vtd = vtd
113
+ self.ahplaced = ahplaced
114
+ self.IsGTCOrder = self._convert_to_bool(IsGTCOrder)
115
+ self.IsEOSOrder = self._convert_to_bool(IsEOSOrder)
116
+ self.paper = self._convert_to_bool(paper)
117
+ self.is_intraday = self._convert_to_bool(is_intraday)
118
+ self.additional_info = additional_info
119
+
120
+ # Setting status using enum
121
+ self.status = self._set_status(status)
122
+
123
+ # Setting broker using enum
124
+ self.broker = self._set_broker(broker)
125
+
126
+ # Handling any additional keyword arguments
127
+ for key, value in kwargs.items():
128
+ setattr(self, key, value)
129
+
130
+ def _set_status(self, status: str) -> OrderStatus:
131
+ """
132
+ Convert a string status to an OrderStatus enum. Default to UNDEFINED if invalid.
133
+ """
134
+ try:
135
+ return OrderStatus[status.upper()]
136
+ except KeyError:
137
+ return OrderStatus.UNDEFINED
138
+
139
+ def _set_broker(self, broker: str) -> Brokers:
140
+ """
141
+ Convert a string broker to a Brokers enum. Default to UNDEFINED if invalid.
142
+ """
143
+ try:
144
+ return Brokers[broker.upper()]
145
+ except KeyError:
146
+ return Brokers.UNDEFINED
147
+
148
+ def _convert_to_int(self, value: Any) -> int:
149
+ """
150
+ Convert a value to an integer if possible, otherwise return None.
151
+ """
152
+ try:
153
+ return int(float(value))
154
+ except (TypeError, ValueError):
155
+ return 0
156
+
157
+ def _convert_to_float(self, value: Any) -> float:
158
+ """
159
+ Convert a value to a float if possible, otherwise return NaN.
160
+ """
161
+ try:
162
+ return float(value)
163
+ except (TypeError, ValueError):
164
+ return float("nan")
165
+
166
+ def _convert_to_bool(self, value: Any) -> bool:
167
+ """
168
+ Convert a value to a boolean if possible.
169
+ """
170
+ if isinstance(value, str):
171
+ return value.lower() in ["true", "1", "yes"]
172
+ return bool(value)
173
+
174
+ def to_dict(self):
175
+ result = self.__dict__.copy()
176
+ # Convert enum to its name (string)
177
+ result["status"] = self.status.name
178
+ result["broker"] = self.broker.name
179
+ return result
180
+
181
+ def __repr__(self):
182
+ return json.dumps(self.to_dict(), indent=4, default=str)
183
+
184
+
185
+ class Price:
186
+ def __init__(
187
+ self,
188
+ bid: float = float("nan"),
189
+ ask: float = float("nan"),
190
+ bid_volume: int = 0,
191
+ ask_volume: int = 0,
192
+ prior_close: float = float("nan"),
193
+ last: float = float("nan"),
194
+ high: float = float("nan"),
195
+ low: float = float("nan"),
196
+ volume: int = 0,
197
+ symbol: str = "",
198
+ exchange: str = "",
199
+ src: str = "",
200
+ timestamp: str = "",
201
+ ):
202
+ self.bid = bid
203
+ self.ask = ask
204
+ self.bid_volume = bid_volume
205
+ self.ask_volume = ask_volume
206
+ self.prior_close = prior_close
207
+ self.last = last
208
+ self.high = high
209
+ self.low = low
210
+ self.volume = volume
211
+ self.symbol = symbol
212
+ self.exchange = exchange
213
+ self.src = src
214
+ self.timestamp = timestamp
215
+
216
+ def __add__(self, other):
217
+ def safe_add(a, b):
218
+ if math.isnan(a) or math.isnan(b):
219
+ return float("nan")
220
+ return a + b
221
+
222
+ return Price(
223
+ bid=safe_add(self.bid, other.bid),
224
+ ask=safe_add(self.ask, other.ask),
225
+ bid_volume=safe_add(self.bid_volume, other.bid_volume),
226
+ ask_volume=safe_add(self.ask_volume, other.ask_volume),
227
+ prior_close=safe_add(self.prior_close, other.prior_close),
228
+ last=safe_add(self.last, other.last),
229
+ high=safe_add(self.high, other.high),
230
+ low=safe_add(self.low, other.low),
231
+ volume=safe_add(self.volume, other.volume),
232
+ )
233
+ # dont change symbol
234
+
235
+ def update(self, other, size=1):
236
+ self.bid = other.bid * size if other.bid * size is not float("nan") else self.bid
237
+ self.ask = other.ask * size if other.ask * size is not float("nan") else self.ask
238
+ self.bid_volume = other.bid_volume if other.bid_volume is not float("nan") else self.bid_volume
239
+ self.ask_volume = other.ask_volume if other.ask_volume is not float("nan") else self.ask_volume
240
+ self.prior_close = other.prior_close * size if other.prior_close is not float("nan") else self.prior_close
241
+ self.last = other.last * size if other.last * size is not float("nan") else self.last
242
+ self.high = other.high * size if other.high * size is not float("nan") else self.high
243
+ self.low = other.low * size if other.low * size is not float("nan") else self.low
244
+ self.volume = other.volume if other.volume is not float("nan") else self.volume
245
+ self.symbol = other.symbol
246
+ self.exchange = other.exchange
247
+ self.src = other.src
248
+ self.timestamp = other.timestamp
249
+
250
+ def to_dict(self):
251
+ return {
252
+ "bid": self.bid,
253
+ "ask": self.ask,
254
+ "bid_volume": self.bid_volume,
255
+ "ask_volume": self.ask_volume,
256
+ "prior_close": self.prior_close,
257
+ "last": self.last,
258
+ "high": self.high,
259
+ "low": self.low,
260
+ "volume": self.volume,
261
+ "symbol": self.symbol,
262
+ "exchange": self.exchange,
263
+ "src": self.src,
264
+ "timestamp": self.timestamp,
265
+ }
266
+
267
+ @classmethod
268
+ def from_dict(cls, data):
269
+ return cls(
270
+ bid=data.get("bid", float("nan")),
271
+ ask=data.get("ask", float("nan")),
272
+ bid_volume=data.get("bid_volume", 0),
273
+ ask_volume=data.get("ask_volume", 0),
274
+ prior_close=data.get("prior_close", float("nan")),
275
+ last=data.get("last", float("nan")),
276
+ high=data.get("high", float("nan")),
277
+ low=data.get("low", float("nan")),
278
+ volume=data.get("volume", 0),
279
+ symbol=data.get("symbol", ""),
280
+ exchange=data.get("exchange", ""),
281
+ src=data.get("src", ""),
282
+ timestamp=data.get("timestamp", ""),
283
+ )
284
+
285
+ def __repr__(self):
286
+ return json.dumps(self.to_dict(), indent=4, default=str)
287
+
288
+
289
+ @dataclass
290
+ class Position:
291
+ symbol: str = ""
292
+ size: int = 0
293
+ price: float = 0
294
+ value: float = 0
295
+
296
+ def to_dict(self) -> dict:
297
+ return {k: v for k, v in asdict(self).items() if v is not None and not (isinstance(v, float) and math.isnan(v))}
298
+ # return asdict(self)
299
+
300
+ def __repr__(self):
301
+ return f"Position({self.__dict__})"
302
+
303
+
304
+ class OrderInfo:
305
+ def __init__(
306
+ self,
307
+ order_size: int = 0,
308
+ order_price: float = float("nan"),
309
+ fill_size: int = 0,
310
+ fill_price: float = 0,
311
+ status: OrderStatus = OrderStatus.UNDEFINED,
312
+ broker_order_id: str = "",
313
+ exchange_order_id: str = "",
314
+ broker=Brokers.UNDEFINED,
315
+ ):
316
+ self.order_size = order_size
317
+ self.order_price = order_price
318
+ self.fill_size = fill_size
319
+ self.fill_price = fill_price
320
+ self.status = status
321
+ self.broker_order_id = broker_order_id
322
+ self.exchange_order_id = exchange_order_id
323
+ self.broker = broker
324
+
325
+ def to_dict(self):
326
+ return {
327
+ "order_size": str(self.order_size),
328
+ "order_price": str(self.order_price),
329
+ "fill_size": str(self.fill_size),
330
+ "fill_price": str(self.fill_price),
331
+ "status": self.status.name if isinstance(self.status, Enum) else self.status,
332
+ "broker_order_id": self.broker_order_id,
333
+ "exchange_order_id": self.exchange_order_id,
334
+ "broker": self.broker.name if isinstance(self.broker, Enum) else self.broker,
335
+ }
336
+
337
+ def __repr__(self):
338
+ return json.dumps(self.to_dict(), indent=4, default=str)
339
+
340
+
341
+ class BrokerBase(ABC):
342
+ @abstractmethod
343
+ def __init__(self, **kwargs):
344
+ self.broker = Brokers.UNDEFINED
345
+ self.starting_order_ids_int = {}
346
+ self.redis_o = redis.Redis(db=0, charset="utf-8", decode_responses=True)
347
+ self.exchange_mappings = {
348
+ "symbol_map": {},
349
+ "contractsize_map": {},
350
+ "exchange_map": {},
351
+ "exchangetype_map": {},
352
+ "contracttick_map": {},
353
+ "symbol_map_reversed": {},
354
+ }
355
+
356
+ @abstractmethod
357
+ def update_symbology(self, **kwargs):
358
+ pass
359
+
360
+ @abstractmethod
361
+ def connect(self, redis_db: int):
362
+ pass
363
+
364
+ @abstractmethod
365
+ def is_connected(self):
366
+ pass
367
+
368
+ @abstractmethod
369
+ def disconnect(self):
370
+ pass
371
+
372
+ @abstractmethod
373
+ def place_order(self, order: Order, **kwargs) -> Order:
374
+ pass
375
+
376
+ @abstractmethod
377
+ def modify_order(self, **kwargs) -> Order:
378
+ pass
379
+
380
+ @abstractmethod
381
+ def cancel_order(self, **kwargs) -> Order:
382
+ pass
383
+
384
+ @abstractmethod
385
+ def get_order_info(self, **kwargs) -> OrderInfo:
386
+ pass
387
+
388
+ @abstractmethod
389
+ def get_historical(
390
+ self,
391
+ symbols: Union[str, pd.DataFrame, dict],
392
+ date_start: str,
393
+ date_end: str = dt.datetime.today().strftime("%Y-%m-%d"),
394
+ exchange: str = "N",
395
+ periodicity: str = "1m",
396
+ market_close_time: str = "15:30:00",
397
+ ) -> Dict[str, List[HistoricalData]]:
398
+ pass
399
+
400
+ @abstractmethod
401
+ def map_exchange_for_api(self, long_symbol, exchange) -> str:
402
+ """maps exchange to exchange code needed by broker API."""
403
+ pass
404
+
405
+ @abstractmethod
406
+ def map_exchange_for_db(self, long_symbol, exchange) -> str:
407
+ """maps exchange to NSE or BSE or MCX. The long form exchange name."""
408
+ pass
409
+
410
+ @abstractmethod
411
+ def get_quote(self, long_symbol: str, exchange="NSE") -> Price:
412
+ pass
413
+
414
+ @abstractmethod
415
+ def get_position(self, long_symbol: str) -> Union[pd.DataFrame, int]:
416
+ pass
417
+
418
+ @abstractmethod
419
+ def get_orders_today(self, **kwargs) -> pd.DataFrame:
420
+ pass
421
+
422
+ @abstractmethod
423
+ def get_trades_today(self, **kwargs) -> pd.DataFrame:
424
+ pass
425
+
426
+ @abstractmethod
427
+ def get_long_name_from_broker_identifier(self, **kwargs) -> pd.Series:
428
+ pass
429
+
430
+ @abstractmethod
431
+ def get_min_lot_size(self, long_symbol: str, exchange: str) -> int:
432
+ pass