From 49bd9f299ce086c716d0ea49fc60420e70327c55 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:02:06 +0000 Subject: [PATCH 1/8] feat: Add mstock broker integration This commit introduces a new broker integration for mstock, following the existing architecture for broker plugins. The integration includes: - Authentication API for TOTP-based login. - Order and data APIs for placing orders, and retrieving positions, holdings, and funds. - A WebSocket streaming adapter for real-time data. - A callback endpoint and login page for the mstock broker. - Configuration variables and test cases. --- .sample.env | 8 +- blueprints/brlogin.py | 10 ++ broker/mstock/__init__.py | 0 broker/mstock/api/auth_api.py | 44 +++++++++ broker/mstock/api/data.py | 70 ++++++++++++++ broker/mstock/api/order_api.py | 145 +++++++++++++++++++++++++++++ broker/mstock/plugin.json | 8 ++ broker/mstock/streaming/adapter.py | 83 +++++++++++++++++ templates/mstock.html | 17 ++++ test/test_mstock.py | 51 ++++++++++ 10 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 broker/mstock/__init__.py create mode 100644 broker/mstock/api/auth_api.py create mode 100644 broker/mstock/api/data.py create mode 100644 broker/mstock/api/order_api.py create mode 100644 broker/mstock/plugin.json create mode 100644 broker/mstock/streaming/adapter.py create mode 100644 templates/mstock.html create mode 100644 test/test_mstock.py diff --git a/.sample.env b/.sample.env index 47513c03..af0e33cc 100644 --- a/.sample.env +++ b/.sample.env @@ -19,7 +19,13 @@ REDIRECT_URL = 'http://127.0.0.1:5000//callback' # Change if different # Valid Brokers Configuration -VALID_BROKERS = 'fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,dhan,dhan_sandbox,definedge,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha' +VALID_BROKERS = 'fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,dhan,dhan_sandbox,definedge,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock' + +# mstock Configuration +# BROKER_API_KEY = 'YOUR_MSTOCK_API_KEY' +# BROKER_USERNAME = 'YOUR_MSTOCK_CLIENT_ID' +# BROKER_PIN = 'YOUR_MSTOCK_PIN' +# BROKER_TOTP_CODE = 'YOUR_MSTOCK_TOTP' # Security Configuration diff --git a/blueprints/brlogin.py b/blueprints/brlogin.py index c479bb3c..646a54a8 100644 --- a/blueprints/brlogin.py +++ b/blueprints/brlogin.py @@ -81,6 +81,16 @@ def broker_callback(broker,para=None): user_id = clientcode auth_token, feed_token, error_message = auth_function(clientcode, broker_pin, totp_code) forward_url = 'angel.html' + + elif broker == 'mstock': + if request.method == 'GET': + return render_template('mstock.html') + + elif request.method == 'POST': + totp_code = request.form.get('totp') + api_key = get_broker_api_key() + auth_token, feed_token, error_message = auth_function(api_key, totp_code) + forward_url = 'mstock.html' elif broker == 'aliceblue': if request.method == 'GET': diff --git a/broker/mstock/__init__.py b/broker/mstock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/broker/mstock/api/auth_api.py b/broker/mstock/api/auth_api.py new file mode 100644 index 00000000..73960695 --- /dev/null +++ b/broker/mstock/api/auth_api.py @@ -0,0 +1,44 @@ +import httpx +import json +import os +from utils.httpx_client import get_httpx_client + +def authenticate_broker(api_key, totp_code): + """ + Authenticate with mstock and return the auth token. + """ + try: + client = get_httpx_client() + + # If TOTP is enabled, we can directly verify it without a prior login call + headers = { + 'X-Mirae-Version': '1', + 'Content-Type': 'application/x-www-form-urlencoded', + } + data = { + 'api_key': api_key, + 'totp': totp_code, + } + + response = client.post( + 'https://api.mstock.trade/openapi/typea/session/verifytotp', + headers=headers, + data=data + ) + + response.raise_for_status() + data_dict = response.json() + + if data_dict.get("status") == "success" and "data" in data_dict: + auth_token = data_dict["data"].get("access_token") + # mstock does not provide a separate feed token in this response + feed_token = None + return auth_token, feed_token, None + else: + error_message = data_dict.get("message", "Authentication failed.") + return None, None, error_message + + except httpx.HTTPStatusError as e: + return None, None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, None, str(e) diff --git a/broker/mstock/api/data.py b/broker/mstock/api/data.py new file mode 100644 index 00000000..c9602282 --- /dev/null +++ b/broker/mstock/api/data.py @@ -0,0 +1,70 @@ +import httpx +import json +import os +from utils.httpx_client import get_httpx_client + +def get_positions(api_key, auth_token): + """ + Retrieves the user's positions. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/portfolio/positions', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def get_holdings(api_key, auth_token): + """ + Retrieves the user's holdings. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/portfolio/holdings', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def get_funds(api_key, auth_token): + """ + Retrieves the user's funds. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/user/fundsummary', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) diff --git a/broker/mstock/api/order_api.py b/broker/mstock/api/order_api.py new file mode 100644 index 00000000..e2dbedea --- /dev/null +++ b/broker/mstock/api/order_api.py @@ -0,0 +1,145 @@ +import httpx +import json +import os +from utils.httpx_client import get_httpx_client + +def place_order(api_key, auth_token, variety, tradingsymbol, exchange, transaction_type, quantity, product, order_type, price=0, trigger_price=0, squareoff=0, stoploss=0, trailing_stoploss=0, disclosed_quantity=0, validity='DAY', amo='NO', ret='DAY'): + """ + Places an order with the broker. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + 'Content-Type': 'application/json', + } + + order_params = { + "variety": variety, + "tradingsymbol": tradingsymbol, + "transactiontype": transaction_type, + "exchange": exchange, + "ordertype": order_type, + "producttype": product, + "duration": validity, + "price": str(price), + "squareoff": str(squareoff), + "stoploss": str(stoploss), + "quantity": str(quantity) + } + + try: + client = get_httpx_client() + response = client.post( + 'https://api.mstock.trade/openapi/typea/orders', + headers=headers, + json=order_params + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def modify_order(api_key, auth_token, order_id, variety, tradingsymbol, exchange, transaction_type, quantity, product, order_type, price=0, trigger_price=0): + """ + Modifies an existing order. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + 'Content-Type': 'application/json', + } + + order_params = { + "orderId": order_id, + "variety": variety, + "tradingsymbol": tradingsymbol, + "transactiontype": transaction_type, + "exchange": exchange, + "ordertype": order_type, + "producttype": product, + "duration": "DAY", + "price": str(price), + "quantity": str(quantity) + } + + try: + client = get_httpx_client() + response = client.put( + f'https://api.mstock.trade/openapi/typea/orders', + headers=headers, + json=order_params + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def cancel_order(api_key, auth_token, order_id, variety): + """ + Cancels an existing order. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.delete( + f'https://api.mstock.trade/openapi/typea/orders/{order_id}?variety={variety}', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def get_order_book(api_key, auth_token): + """ + Retrieves the order book. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/orders', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) + +def get_trade_book(api_key, auth_token): + """ + Retrieves the trade book. + """ + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/trades', + headers=headers, + ) + response.raise_for_status() + return response.json(), None + except httpx.HTTPStatusError as e: + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + except Exception as e: + return None, str(e) diff --git a/broker/mstock/plugin.json b/broker/mstock/plugin.json new file mode 100644 index 00000000..a622c40d --- /dev/null +++ b/broker/mstock/plugin.json @@ -0,0 +1,8 @@ +{ + "Plugin Name": "mstock", + "Plugin URI": "https://openalgo.in", + "Description": "mstock OpenAlgo Plugin", + "Version": "1.0", + "Author": "Rajandran R", + "Author URI": "https://openalgo.in" +} \ No newline at end of file diff --git a/broker/mstock/streaming/adapter.py b/broker/mstock/streaming/adapter.py new file mode 100644 index 00000000..f327ca77 --- /dev/null +++ b/broker/mstock/streaming/adapter.py @@ -0,0 +1,83 @@ +from websocket_proxy.base_adapter import BaseBrokerWebSocketAdapter +import websocket +import json +import threading +from utils.logging import get_logger + +logger = get_logger(__name__) + +class MstockWebSocketAdapter(BaseBrokerWebSocketAdapter): + def __init__(self): + super().__init__() + self.ws = None + self.thread = None + + def initialize(self, broker_name, user_id, auth_data=None): + self.broker_name = broker_name + self.user_id = user_id + self.auth_data = auth_data + + def connect(self): + def on_message(ws, message): + data = json.loads(message) + # Process the message and publish to ZMQ + # This is a placeholder, the actual topic and data will depend on the message format + topic = f"{self.broker_name}_{data.get('symbol', 'UNKNOWN')}" + self.publish_market_data(topic, data) + + def on_error(ws, error): + logger.error(f"WebSocket error: {error}") + + def on_close(ws, close_status_code, close_msg): + logger.info("WebSocket closed") + self.connected = False + + def on_open(ws): + logger.info("WebSocket connection opened") + self.connected = True + # Subscribe to symbols upon connection + for (symbol, exchange), mode in self.subscriptions.items(): + self.subscribe(symbol, exchange, mode) + + self.ws = websocket.WebSocketApp( + "wss://ws.mstock.trade", + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close + ) + self.thread = threading.Thread(target=self.ws.run_forever) + self.thread.start() + + def disconnect(self): + if self.ws: + self.ws.close() + if self.thread: + self.thread.join() + + def subscribe(self, symbol, exchange, mode=2, depth_level=5): + if not self.connected: + return self._create_error_response(503, "WebSocket not connected") + + # mstock's websocket uses a simple string format for subscriptions + # Format: |# + # Mode: 1 for LTP, 2 for Quote, 4 for Depth + # We'll need a way to get the token for a given symbol + # For now, we will use a placeholder token. + # This will be addressed in a future commit. + placeholder_token = "12345" + subscription_string = f"{exchange}|{placeholder_token}#{mode}" + self.ws.send(subscription_string) + self.subscriptions[(symbol, exchange)] = mode + return self._create_success_response("Subscribed successfully") + + def unsubscribe(self, symbol, exchange, mode=2): + if not self.connected: + return self._create_error_response(503, "WebSocket not connected") + + placeholder_token = "12345" + unsubscription_string = f"{exchange}|{placeholder_token}#0" # Mode 0 to unsubscribe + self.ws.send(unsubscription_string) + if (symbol, exchange) in self.subscriptions: + del self.subscriptions[(symbol, exchange)] + return self._create_success_response("Unsubscribed successfully") diff --git a/templates/mstock.html b/templates/mstock.html new file mode 100644 index 00000000..8c62ca0c --- /dev/null +++ b/templates/mstock.html @@ -0,0 +1,17 @@ + + + + mstock Login + + +

mstock Login

+
+
+

+ +
+ {% if error %} +

{{ error }}

+ {% endif %} + + diff --git a/test/test_mstock.py b/test/test_mstock.py new file mode 100644 index 00000000..0f3a4e39 --- /dev/null +++ b/test/test_mstock.py @@ -0,0 +1,51 @@ +import os +import unittest +from openalgo import api as OAClient +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +class TestMstockBroker(unittest.TestCase): + def setUp(self): + """Set up for the test case.""" + # The test assumes that the OpenAlgo server is running and + # the user is already logged into the mstock broker. + self.api_key = os.getenv( + "OPENALGO_API_KEY", + "3bb8d260915ff680a7258108c0483b9eb7675ced31309a36f5846366943ee9fa" + ) + self.client = OAClient(api_key=self.api_key, host="http://127.0.0.1:5000") + + def test_place_order(self): + """Test placing a simple order.""" + # This test requires an active mstock session in the OpenAlgo server + order_response = self.client.placeorder( + strategy="TEST", + symbol="TCS", + exchange="NSE", + price_type="MARKET", + product="MIS", + action="BUY", + quantity=1 + ) + self.assertEqual(order_response.get("status"), "success") + self.assertIn("orderid", order_response) + + def test_get_positions(self): + """Test retrieving positions.""" + positions_response = self.client.positionbook() + self.assertEqual(positions_response.get("status"), "success") + + def test_get_holdings(self): + """Test retrieving holdings.""" + holdings_response = self.client.holdings() + self.assertEqual(holdings_response.get("status"), "success") + + def test_get_funds(self): + """Test retrieving funds.""" + funds_response = self.client.funds() + self.assertEqual(funds_response.get("status"), "success") + +if __name__ == '__main__': + unittest.main() From 7317481e5d6b8146806f09dbf53fe830b86e407f Mon Sep 17 00:00:00 2001 From: Mex Web <50516767+mex-web@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:10:32 +0000 Subject: [PATCH 2/8] update mstock integration --- broker/mstock/api/data.py | 46 ++----- broker/mstock/api/funds.py | 82 +++++++++++++ broker/mstock/api/order_api.py | 53 +++------ broker/mstock/database/master_contract_db.py | 119 +++++++++++++++++++ broker/mstock/mapping/order_data.py | 79 ++++++++++++ broker/mstock/mapping/transform_data.py | 51 ++++++++ templates/broker.html | 5 +- 7 files changed, 361 insertions(+), 74 deletions(-) create mode 100644 broker/mstock/api/funds.py create mode 100644 broker/mstock/database/master_contract_db.py create mode 100644 broker/mstock/mapping/order_data.py create mode 100644 broker/mstock/mapping/transform_data.py diff --git a/broker/mstock/api/data.py b/broker/mstock/api/data.py index c9602282..6410f247 100644 --- a/broker/mstock/api/data.py +++ b/broker/mstock/api/data.py @@ -1,9 +1,8 @@ -import httpx -import json -import os from utils.httpx_client import get_httpx_client +from broker.mstock.mapping.order_data import transform_positions_data, transform_holdings_data -def get_positions(api_key, auth_token): +def get_positions(auth_token): + api_key = os.getenv('BROKER_API_KEY') """ Retrieves the user's positions. """ @@ -11,7 +10,7 @@ def get_positions(api_key, auth_token): 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', } - + try: client = get_httpx_client() response = client.get( @@ -19,13 +18,13 @@ def get_positions(api_key, auth_token): headers=headers, ) response.raise_for_status() - return response.json(), None - except httpx.HTTPStatusError as e: - return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + positions = response.json() + return transform_positions_data(positions), None except Exception as e: return None, str(e) -def get_holdings(api_key, auth_token): +def get_holdings(auth_token): + api_key = os.getenv('BROKER_API_KEY') """ Retrieves the user's holdings. """ @@ -33,7 +32,7 @@ def get_holdings(api_key, auth_token): 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', } - + try: client = get_httpx_client() response = client.get( @@ -41,30 +40,7 @@ def get_holdings(api_key, auth_token): headers=headers, ) response.raise_for_status() - return response.json(), None - except httpx.HTTPStatusError as e: - return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" - except Exception as e: - return None, str(e) - -def get_funds(api_key, auth_token): - """ - Retrieves the user's funds. - """ - headers = { - 'X-Mirae-Version': '1', - 'Authorization': f'token {api_key}:{auth_token}', - } - - try: - client = get_httpx_client() - response = client.get( - 'https://api.mstock.trade/openapi/typea/user/fundsummary', - headers=headers, - ) - response.raise_for_status() - return response.json(), None - except httpx.HTTPStatusError as e: - return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + holdings = response.json() + return transform_holdings_data(holdings), None except Exception as e: return None, str(e) diff --git a/broker/mstock/api/funds.py b/broker/mstock/api/funds.py new file mode 100644 index 00000000..9cd50e53 --- /dev/null +++ b/broker/mstock/api/funds.py @@ -0,0 +1,82 @@ +import os +import httpx +from utils.httpx_client import get_httpx_client +from utils.logging import get_logger + +logger = get_logger(__name__) + +def get_margin_data(auth_token): + """ + Fetch margin (fund) data from MStock API using Type A authentication. + Returns: + (dict, str): Tuple of (margin_data, error_message) + """ + api_key = os.getenv('BROKER_API_KEY') + if not api_key: + return None, "Missing environment variable: BROKER_API_KEY" + + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + + try: + client = get_httpx_client() + response = client.get( + 'https://api.mstock.trade/openapi/typea/user/fundsummary', + headers=headers, + timeout=10.0 + ) + response.raise_for_status() + margin_data = response.json() + + + # Validate response + if margin_data.get('status') == 'success' and margin_data.get('data'): + data = margin_data['data'][0] + + # Mapping between MStock keys and OpenAlgo internal keys + key_mapping = { + "AVAILABLE_BALANCE": "availablecash", + "COLLATERALS": "collateral", + "REALISED_PROFITS": "m2mrealized", + "MTM_COMBINED": "m2munrealized", + "AMOUNT_UTILIZED": "utiliseddebits", + } + + filtered_data = {} + for mstock_key, openalgo_key in key_mapping.items(): + value = data.get(mstock_key) + if value in (None, "None", ""): + value = 0 + try: + formatted_value = "{:.2f}".format(float(value)) + except (ValueError, TypeError): + formatted_value = "0.00" + filtered_data[openalgo_key] = formatted_value + + # Include optional balance summary fields if available + filtered_data["totalbalance"] = "{:.2f}".format( + float(data.get("SUM_OF_ALL", filtered_data.get("availablecash", 0))) + ) + + logger.info(f"filteredMargin Data: {filtered_data}") + + return filtered_data, None + + # If status is not success + error_message = margin_data.get('message', 'Failed to fetch margin data') + logger.error(f"Margin API failed: {error_message}") + return None, error_message + + except httpx.HTTPStatusError as e: + logger.error(f"HTTP Error while fetching margin data: {e}") + return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" + + except httpx.RequestError as e: + logger.error(f"Network Error while fetching margin data: {e}") + return None, f"Network error: {str(e)}" + + except Exception as e: + logger.exception("Unexpected error while fetching margin data.") + return None, str(e) diff --git a/broker/mstock/api/order_api.py b/broker/mstock/api/order_api.py index e2dbedea..6e55d4bb 100644 --- a/broker/mstock/api/order_api.py +++ b/broker/mstock/api/order_api.py @@ -1,32 +1,19 @@ import httpx -import json -import os from utils.httpx_client import get_httpx_client +from broker.mstock.mapping.transform_data import transform_data, transform_modify_order_data +from broker.mstock.mapping.order_data import transform_order_data, transform_tradebook_data -def place_order(api_key, auth_token, variety, tradingsymbol, exchange, transaction_type, quantity, product, order_type, price=0, trigger_price=0, squareoff=0, stoploss=0, trailing_stoploss=0, disclosed_quantity=0, validity='DAY', amo='NO', ret='DAY'): +def place_order(api_key, auth_token, data): """ Places an order with the broker. """ + order_params = transform_data(data) headers = { 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', 'Content-Type': 'application/json', } - - order_params = { - "variety": variety, - "tradingsymbol": tradingsymbol, - "transactiontype": transaction_type, - "exchange": exchange, - "ordertype": order_type, - "producttype": product, - "duration": validity, - "price": str(price), - "squareoff": str(squareoff), - "stoploss": str(stoploss), - "quantity": str(quantity) - } - + try: client = get_httpx_client() response = client.post( @@ -41,29 +28,17 @@ def place_order(api_key, auth_token, variety, tradingsymbol, exchange, transacti except Exception as e: return None, str(e) -def modify_order(api_key, auth_token, order_id, variety, tradingsymbol, exchange, transaction_type, quantity, product, order_type, price=0, trigger_price=0): +def modify_order(api_key, auth_token, data): """ Modifies an existing order. """ + order_params = transform_modify_order_data(data) headers = { 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', 'Content-Type': 'application/json', } - - order_params = { - "orderId": order_id, - "variety": variety, - "tradingsymbol": tradingsymbol, - "transactiontype": transaction_type, - "exchange": exchange, - "ordertype": order_type, - "producttype": product, - "duration": "DAY", - "price": str(price), - "quantity": str(quantity) - } - + try: client = get_httpx_client() response = client.put( @@ -86,7 +61,7 @@ def cancel_order(api_key, auth_token, order_id, variety): 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', } - + try: client = get_httpx_client() response = client.delete( @@ -108,7 +83,7 @@ def get_order_book(api_key, auth_token): 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', } - + try: client = get_httpx_client() response = client.get( @@ -116,7 +91,8 @@ def get_order_book(api_key, auth_token): headers=headers, ) response.raise_for_status() - return response.json(), None + order_book = response.json() + return transform_order_data(order_book), None except httpx.HTTPStatusError as e: return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" except Exception as e: @@ -130,7 +106,7 @@ def get_trade_book(api_key, auth_token): 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', } - + try: client = get_httpx_client() response = client.get( @@ -138,7 +114,8 @@ def get_trade_book(api_key, auth_token): headers=headers, ) response.raise_for_status() - return response.json(), None + trade_book = response.json() + return transform_tradebook_data(trade_book), None except httpx.HTTPStatusError as e: return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" except Exception as e: diff --git a/broker/mstock/database/master_contract_db.py b/broker/mstock/database/master_contract_db.py new file mode 100644 index 00000000..207034b0 --- /dev/null +++ b/broker/mstock/database/master_contract_db.py @@ -0,0 +1,119 @@ +import os +import pandas as pd +import requests +from io import StringIO +from sqlalchemy import create_engine, Column, Integer, String, Float, Sequence, Index +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from extensions import socketio +from utils.logging import get_logger +from database.auth_db import get_auth_token +from database.user_db import find_user_by_username + +logger = get_logger(__name__) + +DATABASE_URL = os.getenv('DATABASE_URL') +engine = create_engine(DATABASE_URL) +db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) +Base = declarative_base() +Base.query = db_session.query_property() + +class SymToken(Base): + __tablename__ = 'symtoken' + id = Column(Integer, Sequence('symtoken_id_seq'), primary_key=True) + symbol = Column(String, nullable=False, index=True) + brsymbol = Column(String, nullable=False, index=True) + name = Column(String) + exchange = Column(String, index=True) + brexchange = Column(String, index=True) + token = Column(String, index=True) + expiry = Column(String) + strike = Column(Float) + lotsize = Column(Integer) + instrumenttype = Column(String) + tick_size = Column(Float) + __table_args__ = (Index('idx_symbol_exchange', 'symbol', 'exchange'),) + +def init_db(): + logger.info("Initializing Master Contract DB") + Base.metadata.create_all(bind=engine) + +def delete_symtoken_table(): + logger.info("Deleting Symtoken Table") + SymToken.query.delete() + db_session.commit() + +def copy_from_dataframe(df): + logger.info("Performing Bulk Insert") + data_dict = df.to_dict(orient='records') + existing_tokens = {result.token for result in db_session.query(SymToken.token).all()} + filtered_data_dict = [row for row in data_dict if row.get('token') and str(row['token']) not in existing_tokens] + + try: + if filtered_data_dict: + db_session.bulk_insert_mappings(SymToken, filtered_data_dict) + db_session.commit() + logger.info(f"Bulk insert completed successfully with {len(filtered_data_dict)} new records.") + else: + logger.info("No new records to insert.") + except Exception as e: + logger.error(f"Error during bulk insert: {e}") + db_session.rollback() + +def download_csv_mstock_data(auth_token): + api_key = os.getenv('BROKER_API_KEY') + url = 'https://api.mstock.trade/openapi/typea/instruments/scriptmaster' + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + } + response = requests.get(url, headers=headers, timeout=30) + if response.status_code == 200: + return response.text + else: + logger.error(f"Failed to download mstock master contract. Status code: {response.status_code}") + return None + +def process_mstock_csv(csv_data): + df = pd.read_csv(StringIO(csv_data)) + df = df.rename(columns={ + 'Exchange': 'exchange', + 'InstrumentType': 'instrumenttype', + 'LotSize': 'lotsize', + 'StrikePrice': 'strike', + 'Symbol': 'symbol', + 'Token': 'token', + 'InstrumentName': 'name', + 'TickSize': 'tick_size', + 'ExpiryDate': 'expiry' + }) + df['brsymbol'] = df['symbol'] + df['brexchange'] = df['exchange'] + + # Data type conversions and formatting + df['strike'] = pd.to_numeric(df['strike'], errors='coerce').fillna(0) + df['lotsize'] = pd.to_numeric(df['lotsize'], errors='coerce').fillna(0).astype(int) + df['tick_size'] = pd.to_numeric(df['tick_size'], errors='coerce').fillna(0) + + return df + +def master_contract_download(): + login_username = os.getenv('LOGIN_USERNAME') + auth_token = get_auth_token(login_username) + api_key = os.getenv('BROKER_API_KEY') + logger.info("Downloading mstock Master Contract") + try: + csv_data = download_csv_mstock_data(api_key, auth_token) + if csv_data: + token_df = process_mstock_csv(csv_data) + delete_symtoken_table() + copy_from_dataframe(token_df) + socketio.emit('master_contract_download', {'status': 'success', 'message': 'Successfully Downloaded'}) + else: + socketio.emit('master_contract_download', {'status': 'error', 'message': 'Failed to download master contract'}) + except Exception as e: + logger.error(f"Error in mstock master contract download: {str(e)}") + socketio.emit('master_contract_download', {'status': 'error', 'message': str(e)}) + +def search_symbols(symbol, exchange): + return SymToken.query.filter(SymToken.symbol.like(f'%{symbol}%'), SymToken.exchange == exchange).all() diff --git a/broker/mstock/mapping/order_data.py b/broker/mstock/mapping/order_data.py new file mode 100644 index 00000000..18536abf --- /dev/null +++ b/broker/mstock/mapping/order_data.py @@ -0,0 +1,79 @@ +from utils.logging import get_logger + +logger = get_logger(__name__) + +def transform_order_data(orders): + transformed_orders = [] + if not orders or not orders.get('data'): + return transformed_orders + + for order in orders['data']: + transformed_order = { + "symbol": order.get("tradingsymbol"), + "exchange": order.get("exchange"), + "action": order.get("transactiontype"), + "quantity": order.get("quantity"), + "price": order.get("price"), + "trigger_price": order.get("triggerprice"), + "pricetype": order.get("ordertype"), + "product": order.get("producttype"), + "orderid": order.get("orderid"), + "order_status": order.get("status"), + "timestamp": order.get("updatetime") + } + transformed_orders.append(transformed_order) + return transformed_orders + +def transform_tradebook_data(tradebook_data): + transformed_data = [] + if not tradebook_data or not tradebook_data.get('data'): + return transformed_data + + for trade in tradebook_data['data']: + transformed_trade = { + "symbol": trade.get('tradingsymbol'), + "exchange": trade.get('exchange'), + "product": trade.get('producttype'), + "action": trade.get('transactiontype'), + "quantity": trade.get('quantity'), + "average_price": trade.get('fillprice'), + "orderid": trade.get('orderid'), + "timestamp": trade.get('filltime') + } + transformed_data.append(transformed_trade) + return transformed_data + +def transform_positions_data(positions_data): + transformed_data = [] + if not positions_data or not positions_data.get('data'): + return transformed_data + + for position in positions_data['data']: + transformed_position = { + "symbol": position.get('tradingsymbol'), + "exchange": position.get('exchange'), + "product": position.get('producttype'), + "quantity": position.get('netqty'), + "average_price": position.get('avgnetprice'), + "ltp": position.get('ltp'), + "pnl": position.get('pnl'), + } + transformed_data.append(transformed_position) + return transformed_data + +def transform_holdings_data(holdings_data): + transformed_data = [] + if not holdings_data or not holdings_data.get('data'): + return transformed_data + + for holding in holdings_data['data']: + transformed_holding = { + "symbol": holding.get('tradingsymbol'), + "exchange": holding.get('exchange'), + "quantity": holding.get('quantity'), + "product": holding.get('product'), + "ltp": holding.get('ltp'), + "pnl": holding.get('pnl'), + } + transformed_data.append(transformed_holding) + return transformed_data diff --git a/broker/mstock/mapping/transform_data.py b/broker/mstock/mapping/transform_data.py new file mode 100644 index 00000000..21c506ce --- /dev/null +++ b/broker/mstock/mapping/transform_data.py @@ -0,0 +1,51 @@ +from database.token_db import get_br_symbol + +def transform_data(data): + """ + Transforms the OpenAlgo API request to the mstock API format. + """ + symbol = get_br_symbol(data["symbol"], data["exchange"]) + transformed = { + "variety": "NORMAL", # mstock only supports NORMAL variety + "tradingsymbol": symbol, + "transactiontype": data["action"].upper(), + "exchange": data["exchange"], + "ordertype": map_order_type(data["pricetype"]), + "producttype": data["product"], + "duration": "DAY", + "price": data.get("price", "0"), + "triggerprice": data.get("trigger_price", "0"), + "quantity": data["quantity"] + } + return transformed + +def transform_modify_order_data(data): + """ + Transforms the OpenAlgo API request to the mstock API format for modifying an order. + """ + symbol = get_br_symbol(data["symbol"], data["exchange"]) + return { + "orderId": data["orderid"], + "variety": "NORMAL", + "tradingsymbol": symbol, + "transactiontype": data["action"].upper(), + "exchange": data["exchange"], + "ordertype": map_order_type(data["pricetype"]), + "producttype": data["product"], + "duration": "DAY", + "price": data["price"], + "quantity": data["quantity"], + "triggerprice": data.get("trigger_price", "0"), + } + +def map_order_type(pricetype): + """ + Maps the OpenAlgo pricetype to the mstock order type. + """ + order_type_mapping = { + "MARKET": "MARKET", + "LIMIT": "LIMIT", + "SL": "SL", + "SL-M": "SL-M" + } + return order_type_mapping.get(pricetype, "MARKET") diff --git a/templates/broker.html b/templates/broker.html index 79a54b87..9dd37aa1 100644 --- a/templates/broker.html +++ b/templates/broker.html @@ -41,7 +41,9 @@ case 'angel': loginUrl = '/angel/callback'; break; - + case 'mstock': + loginUrl = '/mstock/callback'; + break; case 'dhan': // Directly initiate OAuth flow (client_id from .env) loginUrl = '/dhan/initiate-oauth'; @@ -159,6 +161,7 @@

Connect Your Trading - + diff --git a/templates/mstock.html b/templates/mstock.html index 8c62ca0c..3fdee52d 100644 --- a/templates/mstock.html +++ b/templates/mstock.html @@ -1,17 +1,109 @@ - - - - mstock Login - - -

mstock Login

-
-
-

- -
- {% if error %} -

{{ error }}

- {% endif %} - - +{% extends "layout.html" %} + +{% block title %}MStock Authentication - OpenAlgo{% endblock %} + +{% block content %} +
+
+
+ + +
+
+
+ OpenAlgo +
+ +
+ + +
+ + + +
+ + {% if error_message %} +
+ + + + {{ error_message }} +
+ {% endif %} + +
+ +
+
+ +
OR
+ + + + + + Back to Broker Selection + +
+
+ + +
+

Connect MStock

+

+ Enter your 6-digit TOTP from your MStock authenticator app to securely connect your trading account with OpenAlgo. +

+ +
+
+ + + +
+

Security Note

+
Only enter your TOTP code generated by your registered authenticator app.
+
+
+ + +
+
+
+
+
+{% endblock %} From 59466e9ef6626d40f87bd77859a57bf169e52169 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:31:06 +0000 Subject: [PATCH 6/8] feat: Add mstock to installation scripts Adds mstock to the list of valid brokers in the installation scripts. --- broker/mstock/remainwork.md | 39 +++++++++++++++++++++++++++++++++++++ install/install-docker.sh | 4 ++-- install/install-multi.sh | 4 ++-- install/install.sh | 4 ++-- 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 broker/mstock/remainwork.md diff --git a/broker/mstock/remainwork.md b/broker/mstock/remainwork.md new file mode 100644 index 00000000..15790f0f --- /dev/null +++ b/broker/mstock/remainwork.md @@ -0,0 +1,39 @@ +# Remaining Work for MStock Broker Integration + +## Missing Files + +- `broker/mstock/api/__init__.py` +- `broker/mstock/streaming/mstock_mapping.py` +- `broker/mstock/streaming/smartWebSocketV2.py` (or equivalent) + +## Missing Functions + +### `broker/mstock/api/data.py` + +- `get_quotes(symbol, exchange)` +- `get_history(symbol, exchange, interval, start_date, end_date)` +- `get_depth(symbol, exchange)` + +### `broker/mstock/api/order_api.py` + +- `get_positions(auth)` +- `get_holdings(auth)` +- `get_open_position(tradingsymbol, exchange, producttype,auth)` +- `place_smartorder_api(data,auth)` +- `close_all_positions(current_api_key,auth)` +- `cancel_all_orders_api(data,auth)` + +### `broker/mstock/mapping/order_data.py` + +- `map_order_data(order_data)` +- `calculate_order_statistics(order_data)` +- `map_trade_data(trade_data)` +- `map_position_data(position_data)` +- `map_portfolio_data(portfolio_data)` +- `calculate_portfolio_statistics(holdings_data)` + +### `broker/mstock/mapping/transform_data.py` + +- `map_product_type(product)` +- `reverse_map_product_type(product)` +- `map_variety(pricetype)` diff --git a/install/install-docker.sh b/install/install-docker.sh index 14091c2c..61c9ac56 100644 --- a/install/install-docker.sh +++ b/install/install-docker.sh @@ -42,7 +42,7 @@ generate_hex() { # Function to validate broker validate_broker() { local broker=$1 - local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha" + local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock" [[ $valid_brokers == *"$broker"* ]] } @@ -116,7 +116,7 @@ while true; do echo "fivepaisa, fivepaisaxts, aliceblue, angel, compositedge, definedge," echo "dhan, dhan_sandbox, firstock, flattrade, fyers, groww, ibulls, iifl," echo "indmoney, kotak, motilal, paytm, pocketful, shoonya, tradejini," - echo "upstox, wisdom, zebu, zerodha" + echo "upstox, wisdom, zebu, zerodha, mstock" echo "" read -p "Enter your broker name: " BROKER_NAME if validate_broker "$BROKER_NAME"; then diff --git a/install/install-multi.sh b/install/install-multi.sh index 4d4d19ea..317025f3 100644 --- a/install/install-multi.sh +++ b/install/install-multi.sh @@ -50,7 +50,7 @@ generate_hex() { # Function to validate broker name validate_broker() { local broker=$1 - local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha" + local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock" if [[ $valid_brokers == *"$broker"* ]]; then return 0 @@ -152,7 +152,7 @@ for ((i=1; i<=INSTANCES; i++)); do # Get broker while true; do - log_message "\nValid brokers: fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha" "$BLUE" + log_message "\nValid brokers: fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock" "$BLUE" read -p "Enter broker name for instance $i: " broker if validate_broker "$broker"; then BROKERS+=("$broker") diff --git a/install/install.sh b/install/install.sh index 08dd4411..1942787f 100644 --- a/install/install.sh +++ b/install/install.sh @@ -110,7 +110,7 @@ generate_hex() { validate_broker() { local broker=$1 - local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha" + local valid_brokers="fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock" if [[ $valid_brokers == *"$broker"* ]]; then return 0 @@ -355,7 +355,7 @@ done # Get broker name while true; do - log_message "\nValid brokers: fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha" "$BLUE" + log_message "\nValid brokers: fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,definedge,dhan,dhan_sandbox,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock" "$BLUE" read -p "Enter your broker name: " BROKER_NAME if validate_broker "$BROKER_NAME"; then From 3a483be72fdfe1a8dda73bc08b13fe46b4134471 Mon Sep 17 00:00:00 2001 From: Mex Web <50516767+mex-web@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:55:39 +0000 Subject: [PATCH 7/8] mstock map data --- .sample.env | 2 +- broker/mstock/api/__init__.py | 0 broker/mstock/api/order_api.py | 70 ++++--- broker/mstock/mapping/order_data.py | 234 ++++++++++++++++++------ broker/mstock/mapping/transform_data.py | 72 +++++--- broker/mstock/remainwork.md | 6 +- 6 files changed, 279 insertions(+), 105 deletions(-) create mode 100644 broker/mstock/api/__init__.py diff --git a/.sample.env b/.sample.env index af0e33cc..6abd403d 100644 --- a/.sample.env +++ b/.sample.env @@ -19,7 +19,7 @@ REDIRECT_URL = 'http://127.0.0.1:5000//callback' # Change if different # Valid Brokers Configuration -VALID_BROKERS = 'fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,dhan,dhan_sandbox,definedge,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock' +VALID_BROKERS = 'mstock,fivepaisa,fivepaisaxts,aliceblue,angel,compositedge,dhan,dhan_sandbox,definedge,firstock,flattrade,fyers,groww,ibulls,iifl,indmoney,kotak,motilal,paytm,pocketful,shoonya,tradejini,upstox,wisdom,zebu,zerodha,mstock' # mstock Configuration # BROKER_API_KEY = 'YOUR_MSTOCK_API_KEY' diff --git a/broker/mstock/api/__init__.py b/broker/mstock/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/broker/mstock/api/order_api.py b/broker/mstock/api/order_api.py index 6e55d4bb..9d0862f3 100644 --- a/broker/mstock/api/order_api.py +++ b/broker/mstock/api/order_api.py @@ -1,7 +1,47 @@ import httpx +import json +import os from utils.httpx_client import get_httpx_client from broker.mstock.mapping.transform_data import transform_data, transform_modify_order_data from broker.mstock.mapping.order_data import transform_order_data, transform_tradebook_data +from utils.logging import get_logger +logger = get_logger(__name__) + +def get_api_response(endpoint, auth, method="GET", payload=''): + auth_token = auth + api_key = os.getenv('BROKER_API_KEY') + + # Get the shared httpx client with connection pooling + client = get_httpx_client() + + headers = { + 'X-Mirae-Version': '1', + 'Authorization': f'token {api_key}:{auth_token}', + 'Content-Type': 'application/json', + } + + url = f"https://api.mstock.trade/openapi/typea{endpoint}" + + if method == "GET": + response = client.get(url, headers=headers) + elif method == "POST": + response = client.post(url, headers=headers, content=payload) + else: + response = client.request(method, url, headers=headers, content=payload) + + # Add status attribute for compatibility with the existing codebase + response.status = response.status_code + + # Handle empty response + if not response.text: + return {} + + try: + logger.info(f"data from {endpoint}: {response.text}") + return json.loads(response.text) + except json.JSONDecodeError: + logger.error(f"Failed to parse JSON response from {endpoint}: {response.text}") + return {} def place_order(api_key, auth_token, data): """ @@ -75,28 +115,8 @@ def cancel_order(api_key, auth_token, order_id, variety): except Exception as e: return None, str(e) -def get_order_book(api_key, auth_token): - """ - Retrieves the order book. - """ - headers = { - 'X-Mirae-Version': '1', - 'Authorization': f'token {api_key}:{auth_token}', - } - - try: - client = get_httpx_client() - response = client.get( - 'https://api.mstock.trade/openapi/typea/orders', - headers=headers, - ) - response.raise_for_status() - order_book = response.json() - return transform_order_data(order_book), None - except httpx.HTTPStatusError as e: - return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" - except Exception as e: - return None, str(e) +def get_order_book(auth): + return get_api_response("/orders",auth) def get_trade_book(api_key, auth_token): """ @@ -120,3 +140,9 @@ def get_trade_book(api_key, auth_token): return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" except Exception as e: return None, str(e) + +def get_positions(auth): + return get_api_response("/portfolio/positions",auth) + +def get_holdings(auth): + return get_api_response("/portfolio/holdings",auth) \ No newline at end of file diff --git a/broker/mstock/mapping/order_data.py b/broker/mstock/mapping/order_data.py index 18536abf..8eb38f27 100644 --- a/broker/mstock/mapping/order_data.py +++ b/broker/mstock/mapping/order_data.py @@ -1,79 +1,207 @@ +import json +from database.token_db import get_symbol, get_oa_symbol from utils.logging import get_logger logger = get_logger(__name__) + +def map_order_data(order_data): + """ + Processes and modifies a list of order dictionaries based on specific conditions. + """ + if not order_data or 'data' not in order_data or order_data['data'] is None: + logger.info("No data available.") + order_data = [] + else: + order_data = order_data['data'] + logger.info(f"{order_data}") + + if order_data: + for order in order_data: + symboltoken = order.get('instrument_token') + exchange = order.get('exchange') + + if symboltoken and exchange: + symbol_from_db = get_symbol(symboltoken, exchange) + if symbol_from_db: + order['tradingsymbol'] = symbol_from_db + else: + logger.info(f"Symbol not found for token {symboltoken} and exchange {exchange}. Keeping original trading symbol.") + return order_data + + +def calculate_order_statistics(order_data): + """ + Calculates statistics from order data. + """ + total_buy_orders = total_sell_orders = 0 + total_completed_orders = total_open_orders = total_rejected_orders = 0 + + if order_data: + for order in order_data: + if order.get('transaction_type') == 'BUY': + total_buy_orders += 1 + elif order.get('transaction_type') == 'SELL': + total_sell_orders += 1 + + status = order.get('status', '').lower() + if 'complete' in status or 'traded' in status: + total_completed_orders += 1 + elif 'open' in status or 'pending' in status: + total_open_orders += 1 + elif 'rejected' in status: + total_rejected_orders += 1 + + return { + 'total_buy_orders': total_buy_orders, + 'total_sell_orders': total_sell_orders, + 'total_completed_orders': total_completed_orders, + 'total_open_orders': total_open_orders, + 'total_rejected_orders': total_rejected_orders + } + + def transform_order_data(orders): + if isinstance(orders, dict): + orders = [orders] + transformed_orders = [] - if not orders or not orders.get('data'): - return transformed_orders + + for order in orders: + if not isinstance(order, dict): + logger.warning(f"Warning: Expected a dict, but found a {type(order)}. Skipping this item.") + continue - for order in orders['data']: transformed_order = { - "symbol": order.get("tradingsymbol"), - "exchange": order.get("exchange"), - "action": order.get("transactiontype"), - "quantity": order.get("quantity"), - "price": order.get("price"), - "trigger_price": order.get("triggerprice"), - "pricetype": order.get("ordertype"), - "product": order.get("producttype"), - "orderid": order.get("orderid"), - "order_status": order.get("status"), - "timestamp": order.get("updatetime") + "symbol": order.get("tradingsymbol", ""), + "exchange": order.get("exchange", ""), + "action": order.get("transaction_type", ""), + "quantity": order.get("quantity", 0), + "price": order.get("average_price", 0.0), + "trigger_price": order.get("trigger_price", 0.0), + "pricetype": order.get("order_type", ""), + "product": order.get("product", ""), + "orderid": order.get("order_id", ""), + "order_status": order.get("status", ""), + "timestamp": order.get("order_timestamp", "") } transformed_orders.append(transformed_order) + return transformed_orders + +def map_trade_data(trade_data): + """ + Processes and modifies a list of trade dictionaries. + """ + if not trade_data or 'data' not in trade_data or trade_data['data'] is None: + logger.info("No trade data available.") + return [] + + trade_data = trade_data['data'] + for trade in trade_data: + symbol = trade.get('SYMBOL') + exchange = trade.get('EXCHANGE') + if symbol and exchange: + oa_symbol = get_oa_symbol(symbol, exchange) + if oa_symbol: + trade['tradingsymbol'] = oa_symbol + else: + logger.info(f"Unable to find the OA symbol for {symbol} and exchange {exchange}.") + return trade_data + + def transform_tradebook_data(tradebook_data): transformed_data = [] - if not tradebook_data or not tradebook_data.get('data'): - return transformed_data - - for trade in tradebook_data['data']: + for trade in tradebook_data: transformed_trade = { - "symbol": trade.get('tradingsymbol'), - "exchange": trade.get('exchange'), - "product": trade.get('producttype'), - "action": trade.get('transactiontype'), - "quantity": trade.get('quantity'), - "average_price": trade.get('fillprice'), - "orderid": trade.get('orderid'), - "timestamp": trade.get('filltime') + "symbol": trade.get('tradingsymbol', ''), + "exchange": trade.get('EXCHANGE', ''), + "product": trade.get('PRODUCT', ''), + "action": trade.get('BUY_SELL', ''), + "quantity": trade.get('QUANTITY', 0), + "average_price": trade.get('PRICE', 0.0), + "trade_value": trade.get('TRADE_VALUE', 0), + "orderid": trade.get('ORDER_NUMBER', ''), + "timestamp": trade.get('ORDER_DATE_TIME', '') } transformed_data.append(transformed_trade) return transformed_data + +def map_position_data(position_data): + return map_order_data(position_data) + + def transform_positions_data(positions_data): transformed_data = [] - if not positions_data or not positions_data.get('data'): - return transformed_data - - for position in positions_data['data']: - transformed_position = { - "symbol": position.get('tradingsymbol'), - "exchange": position.get('exchange'), - "product": position.get('producttype'), - "quantity": position.get('netqty'), - "average_price": position.get('avgnetprice'), - "ltp": position.get('ltp'), - "pnl": position.get('pnl'), - } - transformed_data.append(transformed_position) + if 'data' in positions_data and positions_data['data']: + for position in positions_data['data']: + transformed_position = { + "symbol": position.get('tradingsymbol', ''), + "exchange": position.get('exchange', ''), + "product": position.get('product', ''), + "quantity": position.get('net_quantity', 0), + "average_price": position.get('average_price', 0.0), + "ltp": position.get('last_traded_price', 0.0), + "pnl": position.get('pnl', 0.0), + } + transformed_data.append(transformed_position) return transformed_data def transform_holdings_data(holdings_data): transformed_data = [] - if not holdings_data or not holdings_data.get('data'): - return transformed_data - - for holding in holdings_data['data']: - transformed_holding = { - "symbol": holding.get('tradingsymbol'), - "exchange": holding.get('exchange'), - "quantity": holding.get('quantity'), - "product": holding.get('product'), - "ltp": holding.get('ltp'), - "pnl": holding.get('pnl'), - } - transformed_data.append(transformed_holding) + if 'data' in holdings_data and holdings_data['data']: + for holding in holdings_data['data']: + transformed_holding = { + "symbol": holding.get('trading_symbol', ''), + "exchange": holding.get('exchange', ''), + "quantity": holding.get('quantity', 0), + "product": holding.get('product', ''), + "pnl": holding.get('pnl', 0.0), + "pnlpercent": holding.get('pnl_percentage', 0.0) + } + transformed_data.append(transformed_holding) return transformed_data + + +def map_portfolio_data(portfolio_data): + """ + Processes portfolio data. + """ + if portfolio_data.get('data') is None: + logger.info("No portfolio data available.") + return {} + + data = portfolio_data['data'] + if 'holdings' in data and data['holdings']: + for holding in data['holdings']: + symbol = holding.get('trading_symbol') + exchange = holding.get('exchange') + if symbol and exchange: + oa_symbol = get_oa_symbol(symbol, exchange) + if oa_symbol: + holding['tradingsymbol'] = oa_symbol + + return data + + +def calculate_portfolio_statistics(holdings_data): + totalholdingvalue = 0 + totalinvvalue = 0 + totalprofitandloss = 0 + totalpnlpercentage = 0 + + if 'data' in holdings_data and 'total_holding' in holdings_data['data']: + total_holding = holdings_data['data']['total_holding'] + totalholdingvalue = total_holding.get('total_holding_value', 0) + totalinvvalue = total_holding.get('total_investment_value', 0) + totalprofitandloss = total_holding.get('total_pnl', 0) + totalpnlpercentage = total_holding.get('total_pnl_percentage', 0) + + return { + 'totalholdingvalue': totalholdingvalue, + 'totalinvvalue': totalinvvalue, + 'totalprofitandloss': totalprofitandloss, + 'totalpnlpercentage': totalpnlpercentage + } diff --git a/broker/mstock/mapping/transform_data.py b/broker/mstock/mapping/transform_data.py index 21c506ce..e159a8ad 100644 --- a/broker/mstock/mapping/transform_data.py +++ b/broker/mstock/mapping/transform_data.py @@ -1,46 +1,43 @@ +#Mapping OpenAlgo API Request https://openalgo.in/docs +#Mapping MStock Parameters https://tradingapi.mstock.com/docs/v1/typeA/Orders/ + from database.token_db import get_br_symbol -def transform_data(data): +def transform_data(data,token): """ - Transforms the OpenAlgo API request to the mstock API format. + Transforms the new API request structure to the current expected structure. """ - symbol = get_br_symbol(data["symbol"], data["exchange"]) + symbol = get_br_symbol(data["symbol"],data["exchange"]) transformed = { - "variety": "NORMAL", # mstock only supports NORMAL variety "tradingsymbol": symbol, - "transactiontype": data["action"].upper(), "exchange": data["exchange"], - "ordertype": map_order_type(data["pricetype"]), - "producttype": data["product"], - "duration": "DAY", + "transaction_type": data["action"].upper(), + "order_type": map_order_type(data["pricetype"]), + "quantity": data["quantity"], + "product": map_product_type(data["product"]), + "validity": "DAY", # Assuming DAY as default "price": data.get("price", "0"), - "triggerprice": data.get("trigger_price", "0"), - "quantity": data["quantity"] + "trigger_price": data.get("trigger_price", "0"), + "disclosed_quantity": data.get("disclosed_quantity", "0"), } return transformed -def transform_modify_order_data(data): - """ - Transforms the OpenAlgo API request to the mstock API format for modifying an order. - """ - symbol = get_br_symbol(data["symbol"], data["exchange"]) + +def transform_modify_order_data(data, token): return { - "orderId": data["orderid"], - "variety": "NORMAL", - "tradingsymbol": symbol, - "transactiontype": data["action"].upper(), - "exchange": data["exchange"], - "ordertype": map_order_type(data["pricetype"]), - "producttype": data["product"], - "duration": "DAY", - "price": data["price"], + "order_type": map_order_type(data["pricetype"]), "quantity": data["quantity"], - "triggerprice": data.get("trigger_price", "0"), + "price": data["price"], + "validity": "DAY", + "disclosed_quantity": data.get("disclosed_quantity", "0"), + "trigger_price": data.get("trigger_price", "0") } + + def map_order_type(pricetype): """ - Maps the OpenAlgo pricetype to the mstock order type. + Maps the new pricetype to the existing order type. """ order_type_mapping = { "MARKET": "MARKET", @@ -49,3 +46,26 @@ def map_order_type(pricetype): "SL-M": "SL-M" } return order_type_mapping.get(pricetype, "MARKET") + +def map_product_type(product): + """ + Maps the new product type to the existing product type. + """ + product_type_mapping = { + "CNC": "CNC", + "NRML": "NRML", + "MIS": "MIS", + } + return product_type_mapping.get(product, "MIS") + + +def reverse_map_product_type(product): + """ + Maps the new product type to the existing product type. + """ + reverse_product_type_mapping = { + "CNC": "CNC", + "NRML": "NRML", + "MIS": "MIS", + } + return reverse_product_type_mapping.get(product) diff --git a/broker/mstock/remainwork.md b/broker/mstock/remainwork.md index 15790f0f..e2b09fc6 100644 --- a/broker/mstock/remainwork.md +++ b/broker/mstock/remainwork.md @@ -2,7 +2,7 @@ ## Missing Files -- `broker/mstock/api/__init__.py` +- `broker/mstock/api/__init__.py` ok - `broker/mstock/streaming/mstock_mapping.py` - `broker/mstock/streaming/smartWebSocketV2.py` (or equivalent) @@ -16,8 +16,8 @@ ### `broker/mstock/api/order_api.py` -- `get_positions(auth)` -- `get_holdings(auth)` +- `get_positions(auth)` ok +- `get_holdings(auth)` ok - `get_open_position(tradingsymbol, exchange, producttype,auth)` - `place_smartorder_api(data,auth)` - `close_all_positions(current_api_key,auth)` From de46167e97a2fd89aa76363facb2f7286d1116c2 Mon Sep 17 00:00:00 2001 From: Mex Web <50516767+mex-web@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:55:53 +0000 Subject: [PATCH 8/8] order book and tradebook mstock --- broker/mstock/api/order_api.py | 108 +++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/broker/mstock/api/order_api.py b/broker/mstock/api/order_api.py index 9d0862f3..0ce40a43 100644 --- a/broker/mstock/api/order_api.py +++ b/broker/mstock/api/order_api.py @@ -43,10 +43,11 @@ def get_api_response(endpoint, auth, method="GET", payload=''): logger.error(f"Failed to parse JSON response from {endpoint}: {response.text}") return {} -def place_order(api_key, auth_token, data): +def place_order(auth_token, data): """ Places an order with the broker. """ + api_key = os.getenv('BROKER_API_KEY') order_params = transform_data(data) headers = { 'X-Mirae-Version': '1', @@ -68,10 +69,11 @@ def place_order(api_key, auth_token, data): except Exception as e: return None, str(e) -def modify_order(api_key, auth_token, data): +def modify_order(auth_token, data): """ Modifies an existing order. """ + api_key = os.getenv('BROKER_API_KEY') order_params = transform_modify_order_data(data) headers = { 'X-Mirae-Version': '1', @@ -93,10 +95,11 @@ def modify_order(api_key, auth_token, data): except Exception as e: return None, str(e) -def cancel_order(api_key, auth_token, order_id, variety): +def cancel_order(auth_token, order_id, variety): """ Cancels an existing order. """ + api_key = os.getenv('BROKER_API_KEY') headers = { 'X-Mirae-Version': '1', 'Authorization': f'token {api_key}:{auth_token}', @@ -116,33 +119,86 @@ def cancel_order(api_key, auth_token, order_id, variety): return None, str(e) def get_order_book(auth): - return get_api_response("/orders",auth) + order_data = get_api_response("/orders", auth) + if ( + order_data + and isinstance(order_data, dict) + and "status" in order_data + and "data" in order_data + and isinstance(order_data["data"], list) + ): + for order in order_data["data"]: + tradingsymbol = str(order.get("tradingsymbol", "")).upper() + if tradingsymbol.endswith("CE") or tradingsymbol.endswith("PE"): + if order.get("exchange", "").upper() == "NSE": + order["exchange"] = "NFO" -def get_trade_book(api_key, auth_token): - """ - Retrieves the trade book. - """ - headers = { - 'X-Mirae-Version': '1', - 'Authorization': f'token {api_key}:{auth_token}', + return { + "status": order_data["status"], + "data": order_data["data"] + } + + # fallback if structure invalid + return { + "status": order_data.get("status", "error") if isinstance(order_data, dict) else "error", + "data": [] + } + +def get_trade_book(auth): + trades_data = get_api_response("/typea/tradebook", auth) + + if ( + trades_data + and isinstance(trades_data, dict) + and "status" in trades_data + and "data" in trades_data + and isinstance(trades_data["data"], list) + ): + for trade in trades_data["data"]: + tradingsymbol = str(trade.get("tradingsymbol", "")).upper() + if tradingsymbol.endswith("CE") or tradingsymbol.endswith("PE"): + if trade.get("exchange", "").upper() == "NSE": + trade["exchange"] = "NFO" + + return { + "status": trades_data["status"], + "data": trades_data["data"] + } + + # fallback if structure invalid + return { + "status": trades_data.get("status", "error") if isinstance(trades_data, dict) else "error", + "data": [] } - - try: - client = get_httpx_client() - response = client.get( - 'https://api.mstock.trade/openapi/typea/trades', - headers=headers, - ) - response.raise_for_status() - trade_book = response.json() - return transform_tradebook_data(trade_book), None - except httpx.HTTPStatusError as e: - return None, f"HTTP error occurred: {e.response.status_code} - {e.response.text}" - except Exception as e: - return None, str(e) def get_positions(auth): - return get_api_response("/portfolio/positions",auth) + positions_data = get_api_response("/portfolio/positions", auth) + + if ( + positions_data + and isinstance(positions_data, dict) + and "status" in positions_data + and "data" in positions_data + and isinstance(positions_data["data"], dict) + and "net" in positions_data["data"] + ): + + for position in positions_data["data"]["net"]: + tradingsymbol = str(position.get("tradingsymbol", "")).upper() + if tradingsymbol.endswith("CE") or tradingsymbol.endswith("PE"): + if position.get("exchange", "").upper() == "NSE": + position["exchange"] = "NFO" + + return { + "status": positions_data["status"], + "data": positions_data["data"]["net"] + } + + # If data missing or malformed, still preserve status if possible + return { + "status": positions_data.get("status", "error") if isinstance(positions_data, dict) else "error", + "data": [] + } def get_holdings(auth): return get_api_response("/portfolio/holdings",auth) \ No newline at end of file