Skip to content

Commit 2e75b77

Browse files
AliceBlue upgraded to V3 (#966)
* AliceBlue V3 integrated * feat: Add AliceBlue broker API integration for market data and funds management. * feat: introduce AliceBlue `BrokerData` class for market data, quotes, and historicals, including WebSocket integration. * Fixed PnL tracking in AliceBlue
1 parent a831df1 commit 2e75b77

File tree

9 files changed

+1051
-1353
lines changed

9 files changed

+1051
-1353
lines changed

blueprints/brlogin.py

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -135,51 +135,30 @@ def broker_callback(broker, para=None):
135135
)
136136

137137
elif broker == "aliceblue":
138-
if request.method == "GET":
139-
# Redirect to React TOTP page
140-
return redirect("/broker/aliceblue/totp")
141-
142-
elif request.method == "POST":
143-
logger.info("Aliceblue Login Flow initiated")
144-
userid = request.form.get("userid")
145-
# Step 1: Get encryption key
146-
# Use the shared httpx client with connection pooling
147-
from utils.httpx_client import get_httpx_client
148-
149-
client = get_httpx_client()
150-
151-
# AliceBlue API expects only userId in the encryption key request
152-
# Do not include API key in this initial request
153-
payload = {"userId": userid}
154-
headers = {"Content-Type": "application/json"}
155-
try:
156-
# Get encryption key
157-
url = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/customer/getAPIEncpkey"
158-
response = client.post(url, json=payload, headers=headers)
159-
response.raise_for_status()
160-
data_dict = response.json()
161-
logger.debug(f"Aliceblue response data: {data_dict}")
162-
163-
# Check if we successfully got the encryption key
164-
if data_dict.get("stat") == "Ok" and data_dict.get("encKey"):
165-
enc_key = data_dict["encKey"]
166-
# Step 2: Authenticate with encryption key
167-
auth_token, error_message = auth_function(userid, enc_key)
168-
169-
if auth_token:
170-
return handle_auth_success(auth_token, session["user"], broker)
171-
else:
172-
return handle_auth_failure(error_message, forward_url="broker.html")
173-
else:
174-
# Failed to get encryption key
175-
error_msg = data_dict.get("emsg", "Failed to get encryption key")
176-
return handle_auth_failure(
177-
f"Failed to get encryption key: {error_msg}", forward_url="broker.html"
178-
)
179-
except Exception as e:
180-
return jsonify(
181-
{"status": "error", "message": f"Authentication error: {str(e)}"}
182-
), 500
138+
# New OAuth redirect flow:
139+
# 1. GET without authCode → redirect to AliceBlue login page with appcode
140+
# 2. GET with authCode + userId (callback) → authenticate and get session
141+
authCode = request.args.get("authCode")
142+
userId = request.args.get("userId")
143+
144+
if authCode and userId:
145+
# Callback from AliceBlue with authorization code
146+
logger.info(f"AliceBlue OAuth callback received for user {userId}")
147+
auth_token, client_id, error_message = auth_function(userId, authCode)
148+
user_id = client_id or userId # clientId from API response, fallback to OAuth userId
149+
feed_token = None # AliceBlue doesn't use a separate feed token
150+
forward_url = "broker.html"
151+
else:
152+
# Initial visit — redirect to AliceBlue login page
153+
logger.info("Redirecting to AliceBlue login page")
154+
appcode = os.environ.get("BROKER_API_KEY")
155+
if not appcode:
156+
return handle_auth_failure(
157+
"BROKER_API_KEY (appCode) not configured in environment",
158+
forward_url="broker.html",
159+
)
160+
aliceblue_login_url = f"https://ant.aliceblueonline.com/?appcode={appcode}"
161+
return redirect(aliceblue_login_url)
183162

184163
elif broker == "fivepaisaxts":
185164
code = "fivepaisaxts"
@@ -771,6 +750,9 @@ def broker_callback(broker, para=None):
771750
auth_token = f"{auth_token}"
772751

773752
# For brokers that have user_id and feed_token from authenticate_broker
753+
if broker in ["angel", "aliceblue", "compositedge", "pocketful", "definedge", "dhan"]:
754+
# For Compositedge, handle missing session user
755+
if broker == "compositedge" and "user" not in session:
774756
if broker in ["angel", "compositedge", "pocketful", "definedge", "dhan", "rmoney"]:
775757
# For OAuth brokers, handle missing session user
776758
if broker in ("compositedge", "rmoney") and "user" not in session:

broker/aliceblue/api/alicebluewebsocket.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ class AliceBlueWebSocket:
2626
PRIMARY_URL = "wss://ws1.aliceblueonline.com/NorenWS/"
2727
ALTERNATE_URL = "wss://ws2.aliceblueonline.com/NorenWS/"
2828

29-
# REST API base URL for WebSocket session management
30-
BASE_URL = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/"
29+
# REST API base URL for WebSocket session management (V2 API)
30+
BASE_URL = "https://a3.aliceblueonline.com/"
3131

3232
# Maximum reconnection attempts
3333
MAX_RECONNECT_ATTEMPTS = 5
@@ -61,7 +61,7 @@ def __init__(self, user_id: str, session_id: str):
6161
def _get_auth_header(self) -> dict:
6262
"""Get authorization header for REST API calls."""
6363
return {
64-
"Authorization": f"Bearer {self.user_id.upper()} {self.session_id}",
64+
"Authorization": f"Bearer {self.session_id}",
6565
"Content-Type": "application/json",
6666
}
6767

@@ -74,9 +74,8 @@ def _invalidate_socket_session(self) -> bool:
7474
bool: True if successful, False otherwise
7575
"""
7676
try:
77-
# Try both endpoint variants (createWsSession and createSocketSess)
78-
url = self.BASE_URL + "ws/invalidateWsSession"
79-
payload = {"loginType": "API"}
77+
url = self.BASE_URL + "open-api/od/v1/profile/invalidateWsSess"
78+
payload = {"source": "API", "userId": self.user_id}
8079

8180
with httpx.Client() as client:
8281
response = client.post(
@@ -105,9 +104,8 @@ def _create_socket_session(self) -> bool:
105104
bool: True if successful, False otherwise
106105
"""
107106
try:
108-
# Use the same endpoint as aliceblue_client.py: ws/createWsSession
109-
url = self.BASE_URL + "ws/createWsSession"
110-
payload = {"loginType": "API"}
107+
url = self.BASE_URL + "open-api/od/v1/profile/createWsSess"
108+
payload = {"source": "API", "userId": self.user_id}
111109

112110
with httpx.Client() as client:
113111
response = client.post(
@@ -119,7 +117,7 @@ def _create_socket_session(self) -> bool:
119117
logger.info(f"Create socket session response: {data}")
120118

121119
# Check for success
122-
if data.get("stat") == "Ok":
120+
if data.get("status") == "Ok":
123121
logger.info("WebSocket session created successfully")
124122
return True
125123
else:
@@ -366,10 +364,10 @@ def _process_tick_data(self, data):
366364
else:
367365
# Fallback to broker symbol from AliceBlue data
368366
symbol = data.get("ts", f"TOKEN_{token}")
369-
logger.warning(
370-
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
367+
logger.debug(
368+
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
371369
)
372-
logger.warning(f"Available subscriptions: {list(self.subscriptions.keys())}")
370+
logger.debug(f"Available subscriptions: {list(self.subscriptions.keys())}")
373371

374372
# Use consistent key format for data storage: exchange:token
375373
key = f"{exchange}:{token}"
@@ -396,6 +394,10 @@ def _process_tick_data(self, data):
396394
"prev_open_interest": int(float(data.get("poi", 0))) if data.get("poi") else 0,
397395
"total_buy_quantity": int(data.get("tbq", 0)),
398396
"total_sell_quantity": int(data.get("tsq", 0)),
397+
"bid": float(data.get("bp1", 0)),
398+
"ask": float(data.get("sp1", 0)),
399+
"bid_qty": int(data.get("bq1", 0)),
400+
"ask_qty": int(data.get("sq1", 0)),
399401
"symbol": symbol, # Use OpenAlgo symbol from subscription
400402
"broker_symbol": data.get("ts", ""), # Keep broker symbol for reference
401403
"timestamp": datetime.now().isoformat(),
@@ -502,10 +504,10 @@ def _process_depth_data(self, data):
502504
else:
503505
# Fallback to broker symbol from AliceBlue data
504506
symbol = data.get("ts", f"TOKEN_{token}")
505-
logger.warning(
506-
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
507+
logger.debug(
508+
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
507509
)
508-
logger.warning(f"Available subscriptions: {list(self.subscriptions.keys())}")
510+
logger.debug(f"Available subscriptions: {list(self.subscriptions.keys())}")
509511

510512
# Use consistent key format for data storage: exchange:token
511513
key = f"{exchange}:{token}"
@@ -543,6 +545,12 @@ def _process_depth_data(self, data):
543545
"token": token,
544546
"bids": bids,
545547
"asks": asks,
548+
"open": float(data.get("o", 0)),
549+
"high": float(data.get("h", 0)),
550+
"low": float(data.get("l", 0)),
551+
"close": float(data.get("c", 0)),
552+
"volume": int(data.get("v", 0)),
553+
"last_trade_quantity": int(data.get("ltq", 0)),
546554
"total_buy_quantity": int(data.get("tbq", 0)),
547555
"total_sell_quantity": int(data.get("tsq", 0)),
548556
"ltp": float(data.get("lp", 0)),
@@ -576,6 +584,16 @@ def _process_depth_data(self, data):
576584
# Update specific fields if they exist in the feed
577585
if "lp" in data:
578586
depth["ltp"] = float(data.get("lp", 0))
587+
if "o" in data:
588+
depth["open"] = float(data.get("o", 0))
589+
if "h" in data:
590+
depth["high"] = float(data.get("h", 0))
591+
if "l" in data:
592+
depth["low"] = float(data.get("l", 0))
593+
if "c" in data:
594+
depth["close"] = float(data.get("c", 0))
595+
if "v" in data:
596+
depth["volume"] = int(data.get("v", 0))
579597
if "pc" in data:
580598
depth["percent_change"] = float(data.get("pc", 0))
581599
if "ft" in data:

broker/aliceblue/api/auth_api.py

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import base64
21
import hashlib
32
import json
43
import os
@@ -11,40 +10,51 @@
1110
logger = get_logger(__name__)
1211

1312

14-
def authenticate_broker(userid, encKey):
13+
def authenticate_broker(userid, authCode):
14+
"""
15+
Authenticate with AliceBlue using the new V2 vendor API.
16+
17+
Returns:
18+
Tuple of (userSession, clientId, error_message)
19+
20+
Flow:
21+
1. Compute SHA-256 checksum of: userId + authCode + apiSecret
22+
2. POST {"checkSum": checksum} to /open-api/od/v1/vendor/getUserDetails
23+
3. Return the userSession from the response
24+
25+
Environment variables:
26+
BROKER_API_KEY = App Code (appCode)
27+
BROKER_API_SECRET = API Secret (apiSecret)
28+
"""
1529
try:
1630
# Fetching the necessary credentials from environment variables
17-
BROKER_API_KEY = os.environ.get("BROKER_API_KEY")
31+
# BROKER_API_KEY = appCode (used for the login redirect, not needed here)
32+
# BROKER_API_SECRET = apiSecret (used to build the checksum)
1833
BROKER_API_SECRET = os.environ.get("BROKER_API_SECRET")
1934

20-
if not BROKER_API_SECRET or not BROKER_API_KEY:
21-
logger.error("API keys not found in environment variables")
22-
return None, "API keys not set in environment variables"
35+
if not BROKER_API_SECRET:
36+
logger.error("BROKER_API_SECRET not found in environment variables")
37+
return None, None, "API secret not set in environment variables"
2338

2439
logger.debug(f"Authenticating with AliceBlue for user {userid}")
2540

26-
# Proper AliceBlue API authentication flow according to V2 API docs:
2741
# Step 1: Get the shared httpx client with connection pooling
2842
client = get_httpx_client()
2943

30-
# Step 2: Generate SHA-256 hash using the combination of User ID + API Key + Encryption Key
31-
# This is the pattern specified in their API docs
44+
# Step 2: Generate SHA-256 checksum = hash(userId + authCode + apiSecret)
3245
logger.debug("Generating checksum for authentication")
33-
# The AliceBlue API V2 documentation specifies: User ID + API Key + Encryption Key
34-
# This is the official order specified in their documentation
35-
checksum_input = f"{userid}{BROKER_API_SECRET}{encKey}"
36-
logger.debug("Checksum input pattern: userId + apiSecret + encKey")
46+
checksum_input = f"{userid}{authCode}{BROKER_API_SECRET}"
47+
logger.debug("Checksum input pattern: userId + authCode + apiSecret")
3748
checksum = hashlib.sha256(checksum_input.encode()).hexdigest()
3849

39-
# Step 3: Prepare request payload with exact parameters matching their API documentation
40-
payload = {"userId": userid, "userData": checksum, "source": "WEB"}
50+
# Step 3: Prepare request payload matching the new API documentation
51+
payload = {"checkSum": checksum}
4152

42-
# Set the headers exactly as expected by their API
4353
headers = {"Content-Type": "application/json", "Accept": "application/json"}
4454

45-
# Step 4: Make the API request to get session ID
46-
logger.debug("Making getUserSID request to AliceBlue API")
47-
url = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/customer/getUserSID"
55+
# Step 4: POST to the new vendor getUserDetails endpoint
56+
logger.debug("Making getUserDetails request to AliceBlue API")
57+
url = "https://ant.aliceblueonline.com/open-api/od/v1/vendor/getUserDetails"
4858
response = client.post(url, json=payload, headers=headers)
4959

5060
logger.debug(f"AliceBlue API response status: {response.status_code}")
@@ -53,57 +63,37 @@ def authenticate_broker(userid, encKey):
5363
# Log full response for debugging
5464
logger.info(f"AliceBlue API response: {json.dumps(data_dict, indent=2)}")
5565

56-
# Extract the session ID from the response
57-
# Handle all possible response formats from AliceBlue API
66+
# --- Parse the response ---
67+
68+
# Success case: stat == "Ok" and userSession is present
69+
if data_dict.get("stat") == "Ok" and data_dict.get("userSession"):
70+
client_id = data_dict.get("clientId")
71+
logger.info(f"Authentication successful for user {userid} (clientId={client_id})")
72+
return data_dict["userSession"], client_id, None
5873

59-
# Case 1: Check if response has sessionID field (typical success case)
60-
if data_dict.get("sessionID"):
61-
logger.info(f"Authentication successful for user {userid}")
62-
return data_dict.get("sessionID"), None
74+
# Error case: stat == "Not_ok" with an error message
75+
if data_dict.get("stat") == "Not_ok":
76+
error_msg = data_dict.get("emsg", "Unknown error occurred")
77+
logger.error(f"API returned Not_ok: {error_msg}")
78+
return None, None, f"API error: {error_msg}"
6379

64-
# Case 2: Check for specific error messages and handle them
80+
# Fallback: check for emsg in any other shape of response
6581
if "emsg" in data_dict and data_dict["emsg"]:
6682
error_msg = data_dict["emsg"]
67-
# Special case handling for common errors
68-
if "User does not login" in error_msg:
69-
logger.error(f"User not logged in: {error_msg}")
70-
return (
71-
None,
72-
"User is not logged in. Please login to the AliceBlue platform first and then try again.",
73-
)
74-
elif "Invalid Input" in error_msg:
75-
logger.error(f"Invalid input error: {error_msg}")
76-
return None, "Invalid input. Please check your user ID and API credentials."
77-
else:
78-
logger.error(f"API error: {error_msg}")
79-
return None, f"API error: {error_msg}"
80-
81-
# Case 3: Handle status field
82-
if data_dict.get("stat") == "Not ok":
83-
error_msg = data_dict.get("emsg", "Unknown error occurred")
84-
logger.error(f"API returned Not ok status: {error_msg}")
85-
return None, f"API error: {error_msg}"
86-
87-
# Case 4: Try to find any field that might contain the session token
88-
for field_name in ["sessionID", "session_id", "sessionId", "token"]:
89-
if field_name in data_dict and data_dict[field_name]:
90-
session_id = data_dict[field_name]
91-
logger.info(f"Found session ID in field {field_name}")
92-
return session_id, None
93-
94-
# Case 5: If we got this far, we couldn't find a session ID
95-
logger.error(f"Couldn't extract session ID from response: {data}")
83+
logger.error(f"API error: {error_msg}")
84+
return None, None, f"API error: {error_msg}"
85+
86+
# If we got here, we couldn't find a session token
87+
logger.error(f"Couldn't extract userSession from response: {data_dict}")
9688
return (
9789
None,
98-
"Failed to extract session ID from response. Please check API credentials and try again.",
90+
None,
91+
"Failed to extract session from response. Please check API credentials and try again.",
9992
)
10093

10194
except json.JSONDecodeError:
102-
# Handle invalid JSON response
103-
return None, "Invalid response format from AliceBlue API."
95+
return None, None, "Invalid response format from AliceBlue API."
10496
except httpx.HTTPError as e:
105-
# Handle HTTPX connection errors
106-
return None, f"HTTP connection error: {str(e)}"
97+
return None, None, f"HTTP connection error: {str(e)}"
10798
except Exception as e:
108-
# General exception handling
109-
return None, f"An exception occurred: {str(e)}"
99+
return None, None, f"An exception occurred: {str(e)}"

0 commit comments

Comments
 (0)