kkunal 1.0.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.
choice_api/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ from .client import ChoiceClient
2
+ from .orders import OrdersAPI
3
+ from .portfolio import PortfolioAPI
4
+ from .funds import FundsAPI
5
+ from .market import MarketAPI
6
+ from .historical import HistoricalAPI
7
+ from .scrip_master import ScripMaster
8
+ from .websockets_interactive import InteractiveSocketClient
9
+ from .websockets_feed import PriceFeedSocketClient
10
+
11
+ __version__ = "1.0.0"
12
+ __all__ = [
13
+ "ChoiceClient",
14
+ "OrdersAPI",
15
+ "PortfolioAPI",
16
+ "FundsAPI",
17
+ "MarketAPI",
18
+ "HistoricalAPI",
19
+ "ScripMaster",
20
+ "InteractiveSocketClient",
21
+ "PriceFeedSocketClient"
22
+ ]
choice_api/client.py ADDED
@@ -0,0 +1,190 @@
1
+ import requests
2
+ import base64
3
+ import os
4
+ import json
5
+ import datetime
6
+ from typing import Dict, Any, Optional
7
+ from Crypto.Cipher import AES
8
+ from Crypto.Util.Padding import pad
9
+
10
+ from .orders import OrdersAPI
11
+ from .portfolio import PortfolioAPI
12
+ from .funds import FundsAPI
13
+ from .market import MarketAPI
14
+ from .historical import HistoricalAPI
15
+ from .scrip_master import ScripMaster
16
+
17
+ class ChoiceClient:
18
+ """
19
+ Main client for interacting with the Choice API.
20
+ Handles authentication, session management, and provides access to other API modules.
21
+ """
22
+ def __init__(self, vendor_id: str, vendor_key: str, api_key: str, aes_key: str, aes_iv: str, base_url: str = "https://finxomne.choiceindia.com"):
23
+ self.vendor_id = vendor_id
24
+ self.vendor_key = vendor_key
25
+ self.api_key = api_key
26
+ self.aes_key = aes_key
27
+ self.aes_iv = aes_iv
28
+ self.base_url = base_url.rstrip('/')
29
+
30
+ self.session_id: Optional[str] = None
31
+ self.access_token: Optional[str] = None
32
+ self.bcast_ip: Optional[str] = None
33
+ self.bcast_port: Optional[int] = None
34
+
35
+ # Initialize sub-modules
36
+ self.orders = OrdersAPI(self)
37
+ self.portfolio = PortfolioAPI(self)
38
+ self.funds = FundsAPI(self)
39
+ self.market = MarketAPI(self)
40
+ self.historical = HistoricalAPI(self)
41
+ self.scrip_master = ScripMaster()
42
+
43
+ def _get_encrypted_mobile(self, mobile_no: str) -> str:
44
+ """Encrypts the mobile number using AES CBC with the provided keys."""
45
+ aes_key_bytes = self.aes_key.encode('utf-8')
46
+ aes_iv_bytes = self.aes_iv.encode('utf-8')
47
+ cipher = AES.new(aes_key_bytes, AES.MODE_CBC, aes_iv_bytes)
48
+ padded = pad(mobile_no.encode('utf-8'), AES.block_size)
49
+ encrypted = cipher.encrypt(padded)
50
+ return base64.b64encode(encrypted).decode('utf-8')
51
+
52
+ def get_headers(self, include_auth: bool = True) -> Dict[str, str]:
53
+ """Constructs headers required for API requests."""
54
+ headers = {
55
+ "VendorId": self.vendor_id,
56
+ "VendorKey": self.vendor_key,
57
+ "Bearer": self.api_key,
58
+ "Content-Type": "application/json"
59
+ }
60
+ if include_auth and self.session_id:
61
+ headers["Authorization"] = f"SessionId {self.session_id}"
62
+ return headers
63
+
64
+ def request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None, require_auth: bool = True) -> Dict[str, Any]:
65
+ """Base method for making HTTP requests to the API."""
66
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
67
+ headers = self.get_headers(include_auth=require_auth)
68
+
69
+ response = requests.request(method, url, headers=headers, json=data)
70
+
71
+ try:
72
+ response.raise_for_status()
73
+ return response.json()
74
+ except requests.exceptions.HTTPError as e:
75
+ raise Exception(f"HTTP Request failed: {e}\nResponse: {response.text}")
76
+ except json.JSONDecodeError:
77
+ raise Exception(f"Failed to decode JSON response: {response.text}")
78
+
79
+ def login(self, mobile_no: str) -> str:
80
+ """
81
+ Executes the full TOTP login flow to obtain a SessionId.
82
+ Flow:
83
+ 1. LoginTOTP
84
+ 2. GetClientLoginTOTP (Retrieve OTP)
85
+ 3. ValidateTOTP (Submit OTP to get SessionId)
86
+
87
+ Returns:
88
+ The acquired SessionId.
89
+ """
90
+ enc_mobile = self._get_encrypted_mobile(mobile_no)
91
+
92
+ # Step 1: Request TOTP
93
+ resp1 = self.request("POST", "api/OpenAPIV1/LoginTOTP", {"MobileNo": enc_mobile}, require_auth=False)
94
+ if resp1.get("Status") != "Success":
95
+ raise Exception(f"LoginTOTP failed: {resp1}")
96
+
97
+ # Step 2: Get OTP generated
98
+ resp2 = self.request("POST", "api/OpenAPIV1/GetClientLoginTOTP", {"MobileNo": enc_mobile}, require_auth=False)
99
+ if resp2.get("Status") != "Success":
100
+ raise Exception(f"GetClientLoginTOTP failed: {resp2}")
101
+
102
+ otp = resp2.get("Response")
103
+ if not otp:
104
+ raise Exception(f"OTP not found in response: {resp2}")
105
+
106
+ # Step 3: Validate TOTP
107
+ resp3 = self.request("POST", "api/OpenAPIV1/ValidateTOTP", {"MobileNo": enc_mobile, "OTP": str(otp)}, require_auth=False)
108
+ if resp3.get("Status") != "Success":
109
+ raise Exception(f"ValidateTOTP failed: {resp3}")
110
+
111
+ # Extract Session ID and other details
112
+ response_data = resp3.get("Response", {})
113
+ if isinstance(response_data, str):
114
+ self.session_id = response_data
115
+ elif isinstance(response_data, dict):
116
+ self.session_id = response_data.get("SessionId") or response_data.get("session_id")
117
+ self.access_token = response_data.get("AccessToken")
118
+ self.bcast_ip = response_data.get("OdinBcastIP")
119
+
120
+ bcast_port_raw = response_data.get("OdinBcastPort")
121
+ if bcast_port_raw:
122
+ try:
123
+ self.bcast_port = int(bcast_port_raw)
124
+ except ValueError:
125
+ self.bcast_port = None
126
+
127
+ # If AccessToken isn't present, fallback to the Bearer API_KEY since
128
+ # some versions of the API allow the same JWT to be reused for the WS.
129
+ if not self.access_token:
130
+ self.access_token = self.api_key
131
+
132
+ if not self.session_id:
133
+ raise Exception("Failed to extract SessionId from ValidateTOTP response")
134
+
135
+ # Automatically fetch the daily scrip master
136
+ try:
137
+ self.scrip_master.fetch()
138
+ except Exception as e:
139
+ print(f"Warning: Failed to fetch scrip master during login: {e}")
140
+
141
+ return self.session_id
142
+
143
+ def save_session(self, filepath: str) -> bool:
144
+ """Saves the current active session to a file."""
145
+ if not self.session_id:
146
+ return False
147
+ try:
148
+ with open(filepath, 'w') as f:
149
+ json.dump({
150
+ "date": datetime.date.today().isoformat(),
151
+ "session_id": self.session_id,
152
+ "access_token": self.access_token,
153
+ "bcast_ip": self.bcast_ip,
154
+ "bcast_port": self.bcast_port
155
+ }, f)
156
+ return True
157
+ except Exception as e:
158
+ print(f"Warning: Failed to save session file: {e}")
159
+ return False
160
+
161
+ def load_session(self, filepath: str) -> bool:
162
+ """Attempts to load a valid session for today from a file."""
163
+ if not os.path.exists(filepath):
164
+ return False
165
+ try:
166
+ with open(filepath, 'r') as f:
167
+ data = json.load(f)
168
+ if data.get("date") == datetime.date.today().isoformat():
169
+ self.session_id = data.get("session_id")
170
+ self.access_token = data.get("access_token")
171
+ self.bcast_ip = data.get("bcast_ip")
172
+ self.bcast_port = data.get("bcast_port")
173
+ if not self.access_token:
174
+ self.access_token = self.api_key
175
+
176
+ # Pre-load scrip master
177
+ try:
178
+ self.scrip_master.fetch()
179
+ except:
180
+ pass
181
+ return True
182
+ return False
183
+ except Exception:
184
+ return False
185
+
186
+ def logoff(self) -> Dict[str, Any]:
187
+ """Logs out the current session."""
188
+ response = self.request("GET", "api/OpenAPI/LogOff")
189
+ self.session_id = None
190
+ return response
choice_api/funds.py ADDED
@@ -0,0 +1,69 @@
1
+ from typing import Dict, Any, Optional
2
+
3
+ class FundsAPI:
4
+ def __init__(self, client):
5
+ self.client = client
6
+
7
+ def get_funds_view(self) -> Dict[str, Any]:
8
+ """Retrieves funds summary."""
9
+ return self.client.request("GET", "api/OpenAPI/FundsView")
10
+
11
+ def get_funds_view_new(self) -> Dict[str, Any]:
12
+ """Retrieves the new format funds summary."""
13
+ return self.client.request("GET", "api/OpenAPI/FundsViewNew")
14
+
15
+ def process_payout(self, amount: float, bank_acc_no: str, product_type: int = 0) -> Dict[str, Any]:
16
+ """Initiates a fund payout request."""
17
+ payload = {
18
+ "Amount": amount,
19
+ "ProductType": product_type,
20
+ "BankAccNo": bank_acc_no
21
+ }
22
+ return self.client.request("POST", "api/OpenAPI/ProcessPayout", payload)
23
+
24
+ def payment_via_netbanking(self, amount: float, bank_acc_no: str, bank_ifsc_code: str, return_url: str,
25
+ segment_id: int, product_type: int = 0) -> Dict[str, Any]:
26
+ """Initiates payment via net banking."""
27
+ payload = {
28
+ "Amount": amount,
29
+ "BankAccNo": bank_acc_no,
30
+ "BankIFSCCode": bank_ifsc_code,
31
+ "ReturnUrl": return_url,
32
+ "ProductType": product_type,
33
+ "SegmentId": segment_id
34
+ }
35
+ return self.client.request("POST", "api/OpenAPI/PaymentViaNB", payload)
36
+
37
+ def payment_via_hdfc_upi(self, amount: float, bank_acc_no: str, user_vpa: str,
38
+ segment_id: int, product_type: int = 0) -> Dict[str, Any]:
39
+ """Initiates payment via HDFC UPI."""
40
+ payload = {
41
+ "Amount": amount,
42
+ "BankAccNo": bank_acc_no,
43
+ "ProductType": product_type,
44
+ "SegmentId": segment_id,
45
+ "UserVPA": user_vpa
46
+ }
47
+ return self.client.request("POST", "api/OpenAPI/PaymentViaHDFCUPI", payload)
48
+
49
+ def check_vpa(self, user_vpa: str) -> Dict[str, Any]:
50
+ """Checks if a VPA is valid."""
51
+ return self.client.request("POST", "api/OpenAPI/CheckVPA", {"UserVPA": user_vpa})
52
+
53
+ def payment_via_razorpay(self, amount: float, bank_acc_no: str, bank_ifsc_code: str, upi_id: str,
54
+ segment_id: int, payment_type: int = 0, product_type: int = 0) -> Dict[str, Any]:
55
+ """Initiates payment via RazorPay."""
56
+ payload = {
57
+ "Amount": amount,
58
+ "BankAccNo": bank_acc_no,
59
+ "BankIFSCCode": bank_ifsc_code,
60
+ "ProductType": product_type,
61
+ "PaymentType": payment_type,
62
+ "UPIId": upi_id,
63
+ "SegmentId": segment_id
64
+ }
65
+ return self.client.request("POST", "api/OpenAPI/PaymentViaRazorPay", payload)
66
+
67
+ def payment_ack_response(self, transaction_id: str) -> Dict[str, Any]:
68
+ """Acknowledges a payment response."""
69
+ return self.client.request("POST", "api/OpenAPI/PaymentAckResponse", {"TransactionId": transaction_id})
@@ -0,0 +1,87 @@
1
+ from typing import Dict, Any, Union, TYPE_CHECKING
2
+ from datetime import datetime
3
+
4
+ if TYPE_CHECKING:
5
+ import pandas as pd
6
+
7
+ class HistoricalAPI:
8
+ def __init__(self, client):
9
+ self.client = client
10
+
11
+ def _parse_date(self, date_val: Union[str, int]) -> int:
12
+ if isinstance(date_val, int):
13
+ return date_val
14
+ epoch_1980 = datetime(1980, 1, 1)
15
+ try:
16
+ if " " in date_val:
17
+ dt = datetime.strptime(date_val, "%Y-%m-%d %H:%M:%S")
18
+ else:
19
+ dt = datetime.strptime(date_val, "%Y-%m-%d")
20
+ return int((dt - epoch_1980).total_seconds())
21
+ except Exception:
22
+ return 0
23
+
24
+ def get_historical_data(self, segment_id: int, token: int, from_date: Union[str, int], to_date: Union[str, int], resolution: str) -> "pd.DataFrame":
25
+ """
26
+ Retrieves historical chart data (e.g., OHLCV) as a Pandas DataFrame.
27
+
28
+ Args:
29
+ segment_id: Exchange Segment ID (e.g., 1 for NSE Cash).
30
+ token: Instrument token.
31
+ from_date: Start date (format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' or seconds from 1980).
32
+ to_date: End date (format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' or seconds from 1980).
33
+ resolution: Timeframe (e.g., '1', '5', 'D').
34
+ """
35
+ import pandas as pd
36
+
37
+ payload = {
38
+ "SegmentId": segment_id,
39
+ "Token": token,
40
+ "FromDate": self._parse_date(from_date),
41
+ "ToDate": self._parse_date(to_date),
42
+ "Interval": resolution
43
+ }
44
+
45
+ resp = self.client.request("POST", "api/OpenGraph/ChartData", payload)
46
+
47
+ if resp.get("Status") == "Success":
48
+ data = resp.get("Response", {})
49
+ history = data.get("lstChartHistory", [])
50
+ divisor = data.get("PriceDivisor", 1)
51
+
52
+ if not history:
53
+ return pd.DataFrame()
54
+
55
+ parsed_data = []
56
+ for row in history:
57
+ parts = row.split(',')
58
+ # Usually: Time, Open, High, Low, Close, Volume, OI
59
+ if len(parts) >= 7:
60
+ parsed_data.append([
61
+ int(parts[0]),
62
+ float(parts[1]) / divisor if divisor else float(parts[1]),
63
+ float(parts[2]) / divisor if divisor else float(parts[2]),
64
+ float(parts[3]) / divisor if divisor else float(parts[3]),
65
+ float(parts[4]) / divisor if divisor else float(parts[4]),
66
+ int(parts[5]),
67
+ int(parts[6])
68
+ ])
69
+ else:
70
+ parsed_row = [float(p) / divisor if idx in (1,2,3,4) else float(p) for idx, p in enumerate(parts)]
71
+ parsed_data.append(parsed_row)
72
+
73
+ columns = ["Time", "Open", "High", "Low", "Close", "Volume", "OI"]
74
+ if parsed_data and len(parsed_data[0]) <= len(columns):
75
+ df = pd.DataFrame(parsed_data, columns=columns[:len(parsed_data[0])])
76
+ else:
77
+ df = pd.DataFrame(parsed_data)
78
+
79
+ if "Time" in df.columns:
80
+ df["Time"] = pd.to_datetime(df["Time"], unit='s', origin=pd.Timestamp('1980-01-01'))
81
+
82
+ return df
83
+
84
+ # If not successful, return empty dataframe or raise?
85
+ # Return empty DataFrame to maintain return type consistency,
86
+ # as self.client.request raises Exception for HTTP errors.
87
+ return pd.DataFrame()
choice_api/market.py ADDED
@@ -0,0 +1,22 @@
1
+ from typing import Dict, Any
2
+
3
+ class MarketAPI:
4
+ def __init__(self, client):
5
+ self.client = client
6
+
7
+ def get_market_status(self) -> Dict[str, Any]:
8
+ """Retrieves current market status across segments."""
9
+ return self.client.request("GET", "api/OpenAPI/MarketStatus")
10
+
11
+ def get_user_profile(self) -> Dict[str, Any]:
12
+ """Retrieves user profile information."""
13
+ return self.client.request("GET", "api/OpenAPI/UserProfile")
14
+
15
+ def get_multiple_touchline(self, multiple_seg_token: str) -> Dict[str, Any]:
16
+ """
17
+ Retrieves multiple touchlines.
18
+
19
+ Args:
20
+ multiple_seg_token: Comma-separated list of segments and tokens.
21
+ """
22
+ return self.client.request("POST", "api/OpenAPI/MultipleTouchline", {"MultipleSegToken": multiple_seg_token})
choice_api/orders.py ADDED
@@ -0,0 +1,93 @@
1
+ from typing import Dict, Any, Optional
2
+
3
+ class OrdersAPI:
4
+ def __init__(self, client):
5
+ self.client = client
6
+
7
+ def place_order(self, segment_id: int, token: int, order_type: str, bs: int, qty: int,
8
+ price: float, trigger_price: float, validity: int, product_type: str,
9
+ disclosed_qty: int = 0, is_edis_req: bool = False) -> Dict[str, Any]:
10
+ """Places a new order."""
11
+ payload = {
12
+ "SegmentId": segment_id,
13
+ "Token": token,
14
+ "OrderType": order_type,
15
+ "BS": bs,
16
+ "Qty": qty,
17
+ "DisclosedQty": disclosed_qty,
18
+ "Price": price,
19
+ "TriggerPrice": trigger_price,
20
+ "Validity": validity,
21
+ "ProductType": product_type,
22
+ "IsEdisReq": is_edis_req,
23
+ "Remarks": "API",
24
+ "ModeTyp": "WEBAPI",
25
+ "Mode": 1,
26
+ "DeviceId": "MAC",
27
+ "ClientOrderNo": 123456
28
+ }
29
+ return self.client.request("POST", "api/OpenAPI/V2/NewOrder", payload)
30
+
31
+ def modify_order(self, client_order_no: int, exchange_order_no: str, gateway_order_no: str,
32
+ segment_id: int, token: int, order_type: str, bs: int, qty: int, price: float,
33
+ trigger_price: float, validity: int, product_type: str, disclosed_qty: int = 0) -> Dict[str, Any]:
34
+ """Modifies an existing order."""
35
+ payload = {
36
+ "ClientOrderNo": client_order_no,
37
+ "ExchangeOrderNo": exchange_order_no,
38
+ "GatewayOrderNo": gateway_order_no,
39
+ "SegmentId": segment_id,
40
+ "Token": token,
41
+ "OrderType": order_type,
42
+ "BS": bs,
43
+ "Qty": qty,
44
+ "DisclosedQty": disclosed_qty,
45
+ "Price": price,
46
+ "TriggerPrice": trigger_price,
47
+ "Validity": validity,
48
+ "ProductType": product_type
49
+ }
50
+ return self.client.request("POST", "api/OpenAPI/ModifyOrder", payload)
51
+
52
+ def cancel_order(self, client_order_no: int, exchange_order_no: str, gateway_order_no: str,
53
+ segment_id: int, token: int, order_type: str, bs: int, qty: int, price: float,
54
+ trigger_price: float, validity: int, product_type: str, exchange_order_time: str = "",
55
+ disclosed_qty: int = 0) -> Dict[str, Any]:
56
+ """Cancels an order."""
57
+ payload = {
58
+ "ExchangeOrderTime": exchange_order_time,
59
+ "ClientOrderNo": client_order_no,
60
+ "ExchangeOrderNo": exchange_order_no,
61
+ "GatewayOrderNo": gateway_order_no,
62
+ "SegmentId": segment_id,
63
+ "Token": token,
64
+ "OrderType": order_type,
65
+ "BS": bs,
66
+ "Qty": qty,
67
+ "DisclosedQty": disclosed_qty,
68
+ "Price": price,
69
+ "TriggerPrice": trigger_price,
70
+ "Validity": validity,
71
+ "ProductType": product_type
72
+ }
73
+ return self.client.request("POST", "api/OpenAPI/CancelOrder", payload)
74
+
75
+ def get_order_book(self) -> Dict[str, Any]:
76
+ """Retrieves the full order book."""
77
+ return self.client.request("GET", "api/OpenAPI/OrderBook")
78
+
79
+ def get_order_book_v2(self) -> Dict[str, Any]:
80
+ """Retrieves order book version 2."""
81
+ return self.client.request("GET", "api/OpenAPI/OrderBookV2")
82
+
83
+ def get_order_by_no(self, order_no: int) -> Dict[str, Any]:
84
+ """Retrieves a specific order by its order number."""
85
+ return self.client.request("GET", f"api/OpenAPI/OrderBookByOrderNo/{order_no}")
86
+
87
+ def get_trade_book(self) -> Dict[str, Any]:
88
+ """Retrieves the trade book."""
89
+ return self.client.request("GET", "api/OpenAPI/TradeBook")
90
+
91
+ def get_order_messages(self, req_id: str) -> Dict[str, Any]:
92
+ """Retrieves order messages."""
93
+ return self.client.request("POST", "api/OpenAPI/OrderMessages", {"ReqId": req_id})
@@ -0,0 +1,43 @@
1
+ from typing import Dict, Any, List
2
+
3
+ class PortfolioAPI:
4
+ def __init__(self, client):
5
+ self.client = client
6
+
7
+ def get_holdings(self) -> Dict[str, Any]:
8
+ """Retrieves user's holdings."""
9
+ return self.client.request("GET", "api/OpenAPI/Holdings")
10
+
11
+ def get_net_position(self) -> Dict[str, Any]:
12
+ """Retrieves the net position of the user."""
13
+ return self.client.request("GET", "api/OpenAPI/NetPosition")
14
+
15
+ def position_conversion(self, segment_id: int, token: int, client_order_no: int, buy_sell: int,
16
+ quantity: int, product_type: str, source_product_type: str, is_edis_req: bool = True) -> Dict[str, Any]:
17
+ """Converts an open position from one product type to another."""
18
+ payload = {
19
+ "SegmentId": segment_id,
20
+ "Token": token,
21
+ "ClientOrderNo": client_order_no,
22
+ "BuySell": buy_sell,
23
+ "Quantity": quantity,
24
+ "ProductType": product_type,
25
+ "SourceProductType": source_product_type,
26
+ "IsEDISReq": is_edis_req
27
+ }
28
+ return self.client.request("POST", "api/OpenAPI/PositionConversion", payload)
29
+
30
+ def verify_dis(self, boid: str, ctr_boid: str, ex_id: int, channel: int, edis_stocks: List[Dict[str, Any]]) -> Dict[str, Any]:
31
+ """Verifies eDIS."""
32
+ payload = {
33
+ "BOID": boid,
34
+ "CtrBOID": ctr_boid,
35
+ "ExId": ex_id,
36
+ "channel": channel,
37
+ "eDISStocks": edis_stocks
38
+ }
39
+ return self.client.request("POST", "api/OpenAPI/VerifyDIS", payload)
40
+
41
+ def get_dis_status(self) -> Dict[str, Any]:
42
+ """Retrieves the DIS status."""
43
+ return self.client.request("POST", "api/OpenAPI/GetDISStatus", {})
@@ -0,0 +1,105 @@
1
+ import csv
2
+ import urllib.request
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class ScripMaster:
9
+ """
10
+ Manages downloading and parsing the daily Scrip Master from Choice API.
11
+ Provides fast lookups for Tokens, Lot Sizes, and Symbols.
12
+ """
13
+ def __init__(self):
14
+ self.symbol_to_token = {}
15
+ self.token_to_details = {}
16
+ self.is_loaded = False
17
+
18
+ def fetch(self):
19
+ """
20
+ Fetches the Scrip Master CSV for the current date.
21
+ If today's file is unavailable (e.g., weekends/holidays early morning),
22
+ it falls back to the previous day.
23
+ """
24
+ logger.info("Fetching daily scrip master...")
25
+
26
+ # Try today, then yesterday, then the day before
27
+ for days_back in range(3):
28
+ target_date = datetime.now() - timedelta(days=days_back)
29
+ date_str = target_date.strftime("%d%b%Y")
30
+ url = f"https://scripmaster.choiceindia.com/scripmaster/SCRIP_MASTER_{date_str}.csv"
31
+
32
+ try:
33
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
34
+ with urllib.request.urlopen(req, timeout=10) as response:
35
+ # Decode the response and parse as CSV
36
+ lines = (line.decode('utf-8', errors='ignore') for line in response)
37
+ reader = csv.DictReader(lines)
38
+
39
+ count = 0
40
+ for row in reader:
41
+ token = row.get('Token', '').strip()
42
+ if not token:
43
+ continue
44
+
45
+ symbol = row.get('Symbol', '').strip()
46
+ sec_desc = row.get('SecDesc', '').strip()
47
+
48
+ # Store complete details
49
+ self.token_to_details[token] = row
50
+
51
+ # Map symbol/sec_desc to token for reverse lookup
52
+ if sec_desc:
53
+ self.symbol_to_token[sec_desc] = token
54
+ if symbol and symbol not in self.symbol_to_token:
55
+ self.symbol_to_token[symbol] = token
56
+
57
+ count += 1
58
+
59
+ self.is_loaded = True
60
+ logger.info(f"Successfully loaded {count} symbols from {date_str} master.")
61
+ return True
62
+
63
+ except urllib.error.HTTPError as e:
64
+ if e.code == 404:
65
+ logger.debug(f"Scrip master not found for {date_str}, trying previous day.")
66
+ continue
67
+ else:
68
+ logger.error(f"HTTP Error fetching scrip master: {e}")
69
+ break
70
+ except Exception as e:
71
+ logger.error(f"Failed to fetch scrip master: {e}")
72
+ break
73
+
74
+ logger.warning("Could not download Scrip Master.")
75
+ return False
76
+
77
+ def get_token(self, symbol_or_desc: str, exchange: str = None) -> str:
78
+ """
79
+ Returns the Token for a given Symbol or SecDesc.
80
+ If exchange is provided (e.g., 'NSE' or 'BSE'), it will ensure the exact match for that exchange.
81
+ """
82
+ if exchange:
83
+ exchange_upper = exchange.upper().strip()
84
+ for token, details in self.token_to_details.items():
85
+ d_symbol = details.get('Symbol', '').strip()
86
+ d_sec_desc = details.get('SecDesc', '').strip()
87
+ d_exch = details.get('Exchange', '').strip().upper()
88
+ if (d_symbol == symbol_or_desc or d_sec_desc == symbol_or_desc) and d_exch == exchange_upper:
89
+ return token
90
+ return None
91
+
92
+ return self.symbol_to_token.get(symbol_or_desc)
93
+
94
+ def get_details(self, token: str) -> dict:
95
+ """Returns all CSV row details for a given Token."""
96
+ return self.token_to_details.get(str(token), {})
97
+
98
+ def get_lot_size(self, token: str) -> int:
99
+ """Helper to quickly get the lot size for a token."""
100
+ details = self.get_details(token)
101
+ lot_size_str = details.get("MarketLot", "1")
102
+ try:
103
+ return int(lot_size_str)
104
+ except ValueError:
105
+ return 1
@@ -0,0 +1,106 @@
1
+ import asyncio
2
+ import zlib
3
+ import logging
4
+ from typing import Callable
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class PriceFeedSocketClient:
9
+ """
10
+ Live Price Feed Socket using FIX3.0 delimited ASCII formats and Zlib compression.
11
+ """
12
+ def __init__(self, host: str, port: int, user_id: str):
13
+ self.host = host
14
+ self.port = port
15
+ self.user_id = user_id
16
+ self.reader = None
17
+ self.writer = None
18
+ self._connected = False
19
+ self._callbacks = []
20
+
21
+ def on_message(self, callback: Callable):
22
+ """Registers a callback for parsed feed messages."""
23
+ self._callbacks.append(callback)
24
+
25
+ async def connect(self):
26
+ """Connects to the Web Feed Handler via TCP Socket."""
27
+ try:
28
+ self.reader, self.writer = await asyncio.open_connection(self.host, self.port)
29
+ self._connected = True
30
+ logger.info(f"Connected to Price Feed Socket at {self.host}:{self.port}")
31
+
32
+ # Send Logon Request
33
+ await self.send_login()
34
+ await self._listen()
35
+ except Exception as e:
36
+ logger.error(f"Failed to connect to Price Feed Socket: {e}")
37
+
38
+ async def disconnect(self):
39
+ """Disconnects the socket."""
40
+ self._connected = False
41
+ if self.writer:
42
+ self.writer.close()
43
+ await self.writer.wait_closed()
44
+ logger.info("Disconnected from Price Feed Socket.")
45
+
46
+ async def send_login(self):
47
+ """Sends the Logon Request (101)."""
48
+ # Format: 63=FIX3.0|64=101|65=66|66=2022-05-04 133022|400=11|67=USERID|68=|
49
+ login_msg = f"63=FIX3.0|64=101|65=66|66=2023-03-06 113929|67={self.user_id}|68=|400=11"
50
+ self._send_raw(login_msg)
51
+
52
+ def subscribe_touchline(self, session_id: str, segment_id: int, token: int):
53
+ """Subscribes to Touchline (Market Data)."""
54
+ # Message code 206
55
+ msg = f"63=FIX3.0|64=206|65=107|66=2023-02-11 19:02:31|1=1$7={token}|230=1|4={session_id}|"
56
+ self._send_raw(msg)
57
+
58
+ def subscribe_best_five(self, session_id: str, segment_id: int, token: int):
59
+ """Subscribes to Best Five (Depth Data)."""
60
+ # Message code 127
61
+ msg = f"63=FIX3.0|64=127|65=84|66=2023-03-06 19:02:31|1=1|7={token}|230=1|4={session_id}"
62
+ self._send_raw(msg)
63
+
64
+ def _send_raw(self, msg: str):
65
+ """Sends a raw uncompressed message (first byte = '2', next 5 bytes = length)."""
66
+ length_str = str(len(msg)).zfill(5)
67
+ packet = f"2{length_str}{msg}".encode('utf-8')
68
+ if self.writer:
69
+ self.writer.write(packet)
70
+ logger.debug(f"Sent: {msg}")
71
+
72
+ async def _listen(self):
73
+ """Listens and decompresses incoming messages."""
74
+ try:
75
+ while self._connected and self.reader:
76
+ # Read 6 byte header: 1 byte type, 5 bytes length
77
+ header = await self.reader.readexactly(6)
78
+ msg_type = chr(header[0])
79
+ msg_len = int(header[1:6].decode('ascii'))
80
+
81
+ payload = await self.reader.readexactly(msg_len)
82
+
83
+ if msg_type == '5': # Compressed
84
+ try:
85
+ decompressed = zlib.decompress(payload)
86
+ self._process_message(decompressed.decode('ascii', errors='ignore'))
87
+ except Exception as e:
88
+ logger.error(f"Zlib decompression failed: {e}")
89
+ elif msg_type == '2': # Uncompressed
90
+ self._process_message(payload.decode('ascii', errors='ignore'))
91
+ else:
92
+ logger.warning(f"Unknown message format indicator: {msg_type}")
93
+ except asyncio.IncompleteReadError:
94
+ logger.info("Connection closed by server.")
95
+ self._connected = False
96
+ except Exception as e:
97
+ logger.error(f"Feed listen error: {e}")
98
+
99
+ def _process_message(self, raw_str: str):
100
+ """Parses the FIX3.0 pipe delimited string."""
101
+ # Simple split by pipe
102
+ for cb in self._callbacks:
103
+ try:
104
+ cb(raw_str)
105
+ except Exception as e:
106
+ logger.error(f"Feed callback error: {e}")
@@ -0,0 +1,94 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import websockets
5
+ from typing import Callable, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class InteractiveSocketClient:
10
+ """
11
+ FINX Interactive Socket for receiving order status, trade confirmations, and market events.
12
+ """
13
+ def __init__(self, token: str, host: str = "wss://finxsocket.choiceindia.com/ws/"):
14
+ self.url = f"{host}?token={token}"
15
+ self.ws: Optional[websockets.WebSocketClientProtocol] = None
16
+ self._connected = False
17
+ self._callbacks = {
18
+ "MKT_STAT": [],
19
+ "ORD_NRML": [],
20
+ "TRD_MSG": [],
21
+ "error": []
22
+ }
23
+ self._keepalive_task = None
24
+
25
+ def on(self, event_type: str, callback: Callable):
26
+ """Registers a callback for a specific MessageType (MKT_STAT, ORD_NRML, TRD_MSG)."""
27
+ if event_type in self._callbacks:
28
+ self._callbacks[event_type].append(callback)
29
+
30
+ async def connect(self):
31
+ """Connects to the interactive socket."""
32
+ try:
33
+ self.ws = await websockets.connect(self.url)
34
+ self._connected = True
35
+ logger.info("Connected to FINX Interactive Socket.")
36
+ self._keepalive_task = asyncio.create_task(self._keepalive())
37
+ await self._listen()
38
+ except Exception as e:
39
+ logger.error(f"Failed to connect to Interactive Socket: {e}")
40
+ self._trigger_callbacks("error", e)
41
+
42
+ async def disconnect(self):
43
+ """Disconnects the socket."""
44
+ self._connected = False
45
+ if self._keepalive_task:
46
+ self._keepalive_task.cancel()
47
+ if self.ws:
48
+ await self.ws.close()
49
+ logger.info("Disconnected from Interactive Socket.")
50
+
51
+ async def _keepalive(self):
52
+ """Sends '2' every 25 seconds to keep the connection alive."""
53
+ try:
54
+ while self._connected and self.ws:
55
+ await asyncio.sleep(25)
56
+ await self.ws.send("2")
57
+ logger.debug("Sent heartbeat (2)")
58
+ except asyncio.CancelledError:
59
+ pass
60
+ except Exception as e:
61
+ logger.error(f"Keepalive error: {e}")
62
+
63
+ async def _listen(self):
64
+ """Listens for incoming messages."""
65
+ try:
66
+ while self._connected and self.ws:
67
+ message = await self.ws.recv()
68
+ if message == "3":
69
+ logger.debug("Received heartbeat ack (3)")
70
+ continue
71
+
72
+ try:
73
+ data = json.loads(message)
74
+ msg_type = data.get("MessageType")
75
+ if msg_type:
76
+ self._trigger_callbacks(msg_type, data)
77
+ else:
78
+ logger.warning(f"Unknown message format: {message}")
79
+ except json.JSONDecodeError:
80
+ logger.warning(f"Non-JSON message received: {message}")
81
+ except websockets.exceptions.ConnectionClosed:
82
+ logger.info("Connection closed by server.")
83
+ self._connected = False
84
+ except Exception as e:
85
+ logger.error(f"Listen error: {e}")
86
+ self._trigger_callbacks("error", e)
87
+
88
+ def _trigger_callbacks(self, event_type: str, data):
89
+ """Triggers all callbacks registered for an event type."""
90
+ for cb in self._callbacks.get(event_type, []):
91
+ try:
92
+ cb(data)
93
+ except Exception as e:
94
+ logger.error(f"Error in callback for {event_type}: {e}")
@@ -0,0 +1,410 @@
1
+ Metadata-Version: 2.4
2
+ Name: kkunal
3
+ Version: 1.0.0
4
+ Summary: Kkunal - Python library for Choice FINX Trading API
5
+ Author: Kkunal
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests>=2.28.0
13
+ Requires-Dist: pycryptodome>=3.17.0
14
+ Requires-Dist: websockets>=11.0.3
15
+ Requires-Dist: pandas>=1.3.0
16
+ Dynamic: author
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: license-file
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ # Kkunal
26
+
27
+ A Python library for the Choice FINX Trading API. Supports REST API, Interactive WebSockets (order/trade updates), and Live Price Feed WebSockets (FIX3.0 compressed data).
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install kkunal
33
+ ```
34
+
35
+ All dependencies (`requests`, `pycryptodome`, `websockets`, `pandas`) are installed automatically.
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from choice_api import ChoiceClient
43
+
44
+ client = ChoiceClient(
45
+ vendor_id="YOUR_VENDOR_ID",
46
+ vendor_key="YOUR_VENDOR_KEY",
47
+ api_key="YOUR_JWT_BEARER_TOKEN",
48
+ aes_key="YOUR_AES_KEY",
49
+ aes_iv="YOUR_AES_IV"
50
+ )
51
+
52
+ # Login (TOTP flow is handled automatically)
53
+ session_id = client.login(mobile_no="1234567890")
54
+ print(f"Session ID: {session_id}")
55
+ ```
56
+
57
+ ### Session Persistence
58
+
59
+ You can save and reload sessions to avoid logging in repeatedly during the same trading day:
60
+
61
+ ```python
62
+ session_file = "my_session.json"
63
+
64
+ if client.load_session(session_file):
65
+ print("Restored today's session.")
66
+ else:
67
+ client.login(mobile_no="1234567890")
68
+ client.save_session(session_file)
69
+ ```
70
+
71
+ > **Note:** Sessions expire daily. `load_session` will return `False` if the saved session is from a previous day.
72
+
73
+ ---
74
+
75
+ ## Scrip Master
76
+
77
+ The Scrip Master CSV is automatically downloaded when you log in. It maps instrument symbols to their tokens, lot sizes, and other metadata.
78
+
79
+ ### `get_token(symbol, exchange=None)`
80
+
81
+ Returns the token for a given symbol.
82
+
83
+ - For **NSE** instruments, no `exchange` parameter is needed (returns NSE by default).
84
+ - For **BSE** instruments, pass `exchange="BSE"` explicitly.
85
+
86
+ ```python
87
+ # NSE (default)
88
+ reliance_token = client.scrip_master.get_token("RELIANCE")
89
+
90
+ # BSE (must specify exchange)
91
+ reliance_bse_token = client.scrip_master.get_token("RELIANCE", exchange="BSE")
92
+ ```
93
+
94
+ ### `get_details(token)`
95
+
96
+ Returns all CSV row details for a given token as a dictionary.
97
+
98
+ ```python
99
+ details = client.scrip_master.get_details("2885")
100
+ print(details)
101
+ # {'Exchange': 'NSE', 'Segment': '1', 'Token': '2885', 'Symbol': 'RELIANCE', ...}
102
+ ```
103
+
104
+ ### `get_lot_size(token)`
105
+
106
+ Returns the market lot size for a token.
107
+
108
+ ```python
109
+ lot = client.scrip_master.get_lot_size("2885")
110
+ print(lot) # 1 for equity, 250 for NIFTY futures, etc.
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Orders
116
+
117
+ > **Important:** Prices must be in **paisa** (multiply INR by 100). For F&O orders, `qty` must be in **total shares** (multiples of the lot size), not the number of lots.
118
+
119
+ ### `client.orders.place_order(...)`
120
+
121
+ | Parameter | Type | Description |
122
+ |---|---|---|
123
+ | `segment_id` | `int` | `1` = NSE Cash, `2` = NSE F&O, `3` = BSE Cash |
124
+ | `token` | `int` | Instrument token from Scrip Master |
125
+ | `order_type` | `str` | `"RL_MKT"` = Market, `"RL_LIMIT"` = Limit |
126
+ | `bs` | `int` | `1` = Buy, `2` = Sell |
127
+ | `qty` | `int` | Total quantity in shares |
128
+ | `price` | `float` | Price in paisa (e.g., 1300 INR → `130000`) |
129
+ | `trigger_price` | `float` | Trigger price in paisa (0 for non-SL orders) |
130
+ | `validity` | `int` | `1` = Day |
131
+ | `product_type` | `str` | `"M"` = Intraday (Margin), `"D"` = Delivery/CarryForward |
132
+ | `disclosed_qty` | `int` | Optional. Disclosed quantity (default `0`) |
133
+
134
+ ```python
135
+ response = client.orders.place_order(
136
+ segment_id=1,
137
+ token=2885,
138
+ order_type="RL_MKT",
139
+ bs=1,
140
+ qty=1,
141
+ price=0,
142
+ trigger_price=0,
143
+ validity=1,
144
+ product_type="D"
145
+ )
146
+ ```
147
+
148
+ ### `client.orders.modify_order(...)`
149
+
150
+ Modifies an existing order. Requires `client_order_no`, `exchange_order_no`, and `gateway_order_no` from the order book.
151
+
152
+ ```python
153
+ response = client.orders.modify_order(
154
+ client_order_no=123456,
155
+ exchange_order_no="1234567890",
156
+ gateway_order_no="1234567890",
157
+ segment_id=1,
158
+ token=2885,
159
+ order_type="RL_LIMIT",
160
+ bs=1,
161
+ qty=1,
162
+ price=130000,
163
+ trigger_price=0,
164
+ validity=1,
165
+ product_type="D"
166
+ )
167
+ ```
168
+
169
+ ### `client.orders.cancel_order(...)`
170
+
171
+ Cancels an existing order. Same parameters as `modify_order` plus optional `exchange_order_time`.
172
+
173
+ ### `client.orders.get_order_book()`
174
+
175
+ Returns all orders placed during the current session.
176
+
177
+ ```python
178
+ order_book = client.orders.get_order_book()
179
+ ```
180
+
181
+ ### `client.orders.get_order_book_v2()`
182
+
183
+ Returns the order book (version 2 format).
184
+
185
+ ### `client.orders.get_order_by_no(order_no)`
186
+
187
+ Returns details for a specific order number.
188
+
189
+ ```python
190
+ order = client.orders.get_order_by_no(123456)
191
+ ```
192
+
193
+ ### `client.orders.get_trade_book()`
194
+
195
+ Returns all executed trades.
196
+
197
+ ```python
198
+ trades = client.orders.get_trade_book()
199
+ ```
200
+
201
+ ### `client.orders.get_order_messages(req_id)`
202
+
203
+ Returns order-related messages for a given request ID.
204
+
205
+ ---
206
+
207
+ ## Portfolio
208
+
209
+ ### `client.portfolio.get_holdings()`
210
+
211
+ Returns current holdings.
212
+
213
+ ```python
214
+ holdings = client.portfolio.get_holdings()
215
+ ```
216
+
217
+ ### `client.portfolio.get_net_position()`
218
+
219
+ Returns net positions.
220
+
221
+ ```python
222
+ positions = client.portfolio.get_net_position()
223
+ ```
224
+
225
+ ### `client.portfolio.position_conversion(...)`
226
+
227
+ Converts an open position from one product type to another (e.g., Intraday to Delivery).
228
+
229
+ | Parameter | Type | Description |
230
+ |---|---|---|
231
+ | `segment_id` | `int` | Exchange segment |
232
+ | `token` | `int` | Instrument token |
233
+ | `client_order_no` | `int` | Client order number |
234
+ | `buy_sell` | `int` | `1` = Buy, `2` = Sell |
235
+ | `quantity` | `int` | Quantity to convert |
236
+ | `product_type` | `str` | Target product type |
237
+ | `source_product_type` | `str` | Current product type |
238
+
239
+ ### `client.portfolio.verify_dis(...)`
240
+
241
+ Verifies eDIS (Electronic Delivery Instruction Slip) for delivery sell orders.
242
+
243
+ ### `client.portfolio.get_dis_status()`
244
+
245
+ Returns the current DIS verification status.
246
+
247
+ ---
248
+
249
+ ## Funds
250
+
251
+ ### `client.funds.get_funds_view()`
252
+
253
+ Returns funds summary.
254
+
255
+ ```python
256
+ funds = client.funds.get_funds_view()
257
+ ```
258
+
259
+ ### `client.funds.get_funds_view_new()`
260
+
261
+ Returns funds summary in the new format.
262
+
263
+ ### `client.funds.process_payout(amount, bank_acc_no, product_type=0)`
264
+
265
+ Initiates a fund withdrawal.
266
+
267
+ ### `client.funds.payment_via_netbanking(amount, bank_acc_no, bank_ifsc_code, return_url, segment_id, product_type=0)`
268
+
269
+ Initiates a net banking payment.
270
+
271
+ ### `client.funds.payment_via_hdfc_upi(amount, bank_acc_no, user_vpa, segment_id, product_type=0)`
272
+
273
+ Initiates a HDFC UPI payment.
274
+
275
+ ### `client.funds.check_vpa(user_vpa)`
276
+
277
+ Validates a UPI VPA address.
278
+
279
+ ### `client.funds.payment_via_razorpay(amount, bank_acc_no, bank_ifsc_code, upi_id, segment_id, payment_type=0, product_type=0)`
280
+
281
+ Initiates a RazorPay payment.
282
+
283
+ ### `client.funds.payment_ack_response(transaction_id)`
284
+
285
+ Acknowledges a payment transaction.
286
+
287
+ ---
288
+
289
+ ## Market
290
+
291
+ ### `client.market.get_market_status()`
292
+
293
+ Returns current market status across all segments.
294
+
295
+ ```python
296
+ status = client.market.get_market_status()
297
+ ```
298
+
299
+ ### `client.market.get_user_profile()`
300
+
301
+ Returns the authenticated user's profile.
302
+
303
+ ```python
304
+ profile = client.market.get_user_profile()
305
+ ```
306
+
307
+ ### `client.market.get_multiple_touchline(multiple_seg_token)`
308
+
309
+ Returns touchline data for multiple instruments.
310
+
311
+ ```python
312
+ # Format: "SegmentId1,Token1|SegmentId2,Token2"
313
+ touchline = client.market.get_multiple_touchline("1,2885|1,11536")
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Historical Data
319
+
320
+ ### `client.historical.get_historical_data(segment_id, token, from_date, to_date, resolution)`
321
+
322
+ Returns historical OHLCV data as a **Pandas DataFrame**.
323
+
324
+ | Parameter | Type | Description |
325
+ |---|---|---|
326
+ | `segment_id` | `int` | Exchange segment |
327
+ | `token` | `int` | Instrument token |
328
+ | `from_date` | `str` or `int` | Start date (`"YYYY-MM-DD"` or seconds from 1980) |
329
+ | `to_date` | `str` or `int` | End date (`"YYYY-MM-DD"` or seconds from 1980) |
330
+ | `resolution` | `str` | `"1"` = 1 min, `"5"` = 5 min, `"D"` = Daily |
331
+
332
+ ```python
333
+ df = client.historical.get_historical_data(
334
+ segment_id=1,
335
+ token=2885,
336
+ from_date="2024-01-01",
337
+ to_date="2024-12-31",
338
+ resolution="D"
339
+ )
340
+ print(df.head())
341
+ # Time Open High Low Close Volume OI
342
+ # 0 2024-01-01 00:00:00 2501.00 2520.50 2490.00 2515.30 1234567 0
343
+ ```
344
+
345
+ The returned DataFrame has columns: `Time`, `Open`, `High`, `Low`, `Close`, `Volume`, `OI`. Prices are automatically adjusted using the `PriceDivisor` from the API response.
346
+
347
+ ---
348
+
349
+ ## Interactive WebSockets
350
+
351
+ Receives live order updates, trade confirmations, and market status events.
352
+
353
+ ```python
354
+ import asyncio
355
+ from choice_api import InteractiveSocketClient
356
+
357
+ async def main():
358
+ ws = InteractiveSocketClient(token=client.session_id)
359
+
360
+ ws.on("ORD_NRML", lambda data: print(f"Order Update: {data}"))
361
+ ws.on("TRD_MSG", lambda data: print(f"Trade: {data}"))
362
+ ws.on("MKT_STAT", lambda data: print(f"Market Status: {data}"))
363
+
364
+ await ws.connect()
365
+
366
+ asyncio.run(main())
367
+ ```
368
+
369
+ **Event types:** `ORD_NRML` (order updates), `TRD_MSG` (trade confirmations), `MKT_STAT` (market open/close).
370
+
371
+ ---
372
+
373
+ ## Price Feed WebSockets (FIX3.0)
374
+
375
+ Receives live Level 1 (Touchline) and Level 2 (Best Five / Depth) market data via TCP socket with Zlib compression.
376
+
377
+ ```python
378
+ import asyncio
379
+ from choice_api import PriceFeedSocketClient
380
+
381
+ async def main():
382
+ feed = PriceFeedSocketClient(
383
+ host=client.bcast_ip,
384
+ port=client.bcast_port,
385
+ user_id="YOUR_USER_ID"
386
+ )
387
+
388
+ feed.on_message(lambda raw: print(f"Feed: {raw}"))
389
+
390
+ # Start connection (sends login automatically)
391
+ asyncio.create_task(feed.connect())
392
+
393
+ # Wait for connection, then subscribe
394
+ await asyncio.sleep(2)
395
+ feed.subscribe_touchline(client.session_id, segment_id=1, token=2885)
396
+ feed.subscribe_best_five(client.session_id, segment_id=1, token=2885)
397
+
398
+ # Keep running
399
+ await asyncio.sleep(3600)
400
+
401
+ asyncio.run(main())
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Logoff
407
+
408
+ ```python
409
+ client.logoff()
410
+ ```
@@ -0,0 +1,15 @@
1
+ choice_api/__init__.py,sha256=fY65cwH-JaEdWKAhq3-Ml8-_JHZwFhbld5wmoBU5s3o,570
2
+ choice_api/client.py,sha256=Ak9LYlT7A60DL8DGYQ1FqBBF84OcTxfkrV4f6CcZTQs,7664
3
+ choice_api/funds.py,sha256=i0wAEdgOVbeJ_fKsj48TnJvBPnZd_nvqtCX98S47zYU,2991
4
+ choice_api/historical.py,sha256=3nv9XpRA6vr3wvnVoYnZiYFRDYW_G_S_jaZ0KG5oNBQ,3641
5
+ choice_api/market.py,sha256=EQPy7H6M4NgiMCQwiMTO-T-jn38qUNmCWsvuOWGW7tY,834
6
+ choice_api/orders.py,sha256=Fpm9Y10J5c4dpEQCH7k998q3qzT8OcI5e9CqUkX3kUY,3958
7
+ choice_api/portfolio.py,sha256=5O1iQCzHnkw-2x6mJlfINsVZcn_9jUpHXJmGoEWwzfM,1775
8
+ choice_api/scrip_master.py,sha256=suvihBC94Ju0iE0txHftickOXHySPhEo0kb8pMh14v4,4398
9
+ choice_api/websockets_feed.py,sha256=SQAGFV3CMoQS_1kmE4Uf_IP4hhDq7Ah6DCSrN1c814I,4236
10
+ choice_api/websockets_interactive.py,sha256=bGWNnFcuECmFIAoK9F7yxF5YMBqAokI8_gg6gYch588,3553
11
+ kkunal-1.0.0.dist-info/licenses/LICENSE,sha256=Uov9DNV_BCILceTqoxV67xKBx7xFisUYLV_jas2RWoQ,1062
12
+ kkunal-1.0.0.dist-info/METADATA,sha256=t6cBhWOJcKHbtFINGkDKfd0vsBIvRiDRbfGhjIKLzTk,10212
13
+ kkunal-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
+ kkunal-1.0.0.dist-info/top_level.txt,sha256=VsLK_s-yRDOGB9DrN_5TkpyT-ejeAXp_kKygPh9_z18,11
15
+ kkunal-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kkunal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT/TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ choice_api