diff --git a/test/sdk_tests/market_basic_v3.py b/test/sdk_tests/market_basic_v3.py new file mode 100644 index 0000000..ac54356 --- /dev/null +++ b/test/sdk_tests/market_basic_v3.py @@ -0,0 +1,46 @@ +import time + +import upstox_client +import data_token + +configuration = upstox_client.Configuration() +configuration.access_token = data_token.access_token +streamer = upstox_client.MarketDataStreamerV3( + upstox_client.ApiClient(configuration), instrumentKeys=["NSE_FO|53023", "MCX_FO|428750"], mode="full_d3") + +streamer.auto_reconnect(True, 5, 10) + + +def on_open(): + print("on open message") + + +def close(a, b): + print(f"on close message {a}") + + +def message(data): + print(f"on message message{data}") + + +def error(er): + print(f"on error message= {er}") + + +def reconnecting(data): + print(f"reconnecting event= {data}") + + +streamer.on("open", on_open) +streamer.on("message", message) +streamer.on("close", close) +streamer.on("reconnecting", reconnecting) +streamer.on("error", error) +streamer.connect() +time.sleep(10) +print("changing mode to full_d3") +streamer.change_mode(["MCX_FO|428750"], "full_d3") +time.sleep(10) +print("changing mode to ltpc") +streamer.change_mode(["MCX_FO|428750"], "ltpc") + diff --git a/upstox_client/__init__.py b/upstox_client/__init__.py index c979bcf..590b8d2 100644 --- a/upstox_client/__init__.py +++ b/upstox_client/__init__.py @@ -29,6 +29,7 @@ from upstox_client.api.websocket_api import WebsocketApi # import websocket interfaces into sdk package from upstox_client.feeder.market_data_streamer import MarketDataStreamer +from upstox_client.feeder.market_data_streamer_v3 import MarketDataStreamerV3 from upstox_client.feeder.portfolio_data_streamer import PortfolioDataStreamer # import ApiClient from upstox_client.api_client import ApiClient diff --git a/upstox_client/feeder/__init__.py b/upstox_client/feeder/__init__.py index 1ac070e..a244313 100644 --- a/upstox_client/feeder/__init__.py +++ b/upstox_client/feeder/__init__.py @@ -4,4 +4,5 @@ # import websocket interfaces into feeder package from upstox_client.feeder.market_data_streamer import MarketDataStreamer +from upstox_client.feeder.market_data_streamer_v3 import MarketDataStreamerV3 from upstox_client.feeder.portfolio_data_streamer import PortfolioDataStreamer diff --git a/upstox_client/feeder/market_data_feeder_v3.py b/upstox_client/feeder/market_data_feeder_v3.py new file mode 100644 index 0000000..b62fadb --- /dev/null +++ b/upstox_client/feeder/market_data_feeder_v3.py @@ -0,0 +1,92 @@ +import websocket +import json +import uuid +import threading +import ssl +from .feeder import Feeder + + +class MarketDataFeederV3(Feeder): + Mode = { + "LTPC": "ltpc", + "FULL": "full", + "OPTION": "option_greeks", + "D30": "full_d30" + } + + Method = { + "SUBSCRIBE": "sub", + "CHANGE_METHOD": "change_mode", + "UNSUBSCRIBE": "unsub", + } + + def __init__(self, api_client=None, instrumentKeys=[], mode="full", on_open=None, on_message=None, on_error=None, on_close=None): + super().__init__(api_client=api_client) + self.api_client = api_client + self.instrumentKeys = instrumentKeys + self.mode = mode + self.on_open = on_open + self.on_message = on_message + self.on_error = on_error + self.on_close = on_close + self.ws = None + self.closingCode = -1 + + def connect(self): + if self.ws and self.ws.sock: + return + + sslopt = { + "cert_reqs": ssl.CERT_NONE, + "check_hostname": False, + } + ws_url = "wss://api.upstox.com/v3/feed/market-data-feed" + headers = {'Authorization': self.api_client.configuration.auth_settings().get("OAUTH2")[ + "value"]} + self.ws = websocket.WebSocketApp(ws_url, + header=headers, + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + + threading.Thread(target=self.ws.run_forever, + kwargs={"sslopt": sslopt}).start() + + def subscribe(self, instrumentKeys, mode=None): + if self.ws and self.ws.sock: + request = self.build_request( + instrumentKeys, self.Method["SUBSCRIBE"], mode) + self.ws.send(request, opcode=websocket.ABNF.OPCODE_BINARY) + else: + raise Exception("WebSocket is not open.") + + def unsubscribe(self, instrumentKeys): + if self.ws and self.ws.sock: + request = self.build_request( + instrumentKeys, self.Method["UNSUBSCRIBE"]) + self.ws.send(request, opcode=websocket.ABNF.OPCODE_BINARY) + else: + raise Exception("WebSocket is not open.") + + def change_mode(self, instrumentKeys, newMode): + + if self.ws and self.ws.sock: + request = self.build_request( + instrumentKeys, self.Method["CHANGE_METHOD"], newMode) + self.ws.send(request, opcode=websocket.ABNF.OPCODE_BINARY) + else: + raise Exception("WebSocket is not open.") + + def build_request(self, instrumentKeys, method, mode=None): + requestObj = { + "guid": str(uuid.uuid4()), + "method": method, + "data": { + "instrumentKeys": instrumentKeys, + }, + } + if mode is not None: + requestObj["data"]["mode"] = mode + + return json.dumps(requestObj).encode('utf-8') diff --git a/upstox_client/feeder/market_data_streamer_v3.py b/upstox_client/feeder/market_data_streamer_v3.py new file mode 100644 index 0000000..c9f4a52 --- /dev/null +++ b/upstox_client/feeder/market_data_streamer_v3.py @@ -0,0 +1,97 @@ +from .market_data_feeder_v3 import MarketDataFeederV3 +from .streamer import Streamer +from .proto import MarketDataFeedV3_pb2 +from google.protobuf import json_format + + +class MarketDataStreamerV3(Streamer): + Mode = { + "LTPC": "ltpc", + "FULL": "full", + "OPTION": "option_greeks", + "D30": "full_d30" + } + def __init__(self, api_client=None, instrumentKeys=[], mode="ltpc"): + super().__init__(api_client) + self.protobufRoot = MarketDataFeedV3_pb2 + self.api_client = api_client + self.instrumentKeys = instrumentKeys + self.mode = mode + self.subscriptions = { + self.Mode["LTPC"]: set(), + self.Mode["FULL"]: set(), + self.Mode["OPTION"]: set(), + self.Mode["D30"]: set(), + } + if mode not in self.Mode.values(): + raise Exception(f"Invalid mode provided {mode}") + # Populate initial subscriptions if provided + for key in instrumentKeys: + self.subscriptions[mode].add(key) + + def connect(self): + self.feeder = MarketDataFeederV3( + api_client=self.api_client, instrumentKeys=self.instrumentKeys, mode=self.mode, on_open=self.handle_open, on_message=self.handle_message, on_error=self.handle_error, on_close=self.handle_close) + self.feeder.connect() + + def subscribe_to_initial_keys(self): + for mode, keys in self.subscriptions.items(): + if keys: + self.feeder.subscribe(list(keys), mode) + + def subscribe(self, instrumentKeys, mode): + if not self.feeder: + raise Exception("WebSocket is not open.") + + if self.is_invalid_mode(mode): + return + + self.feeder.subscribe(instrumentKeys, mode) + self.subscriptions[mode].update(instrumentKeys) + + def unsubscribe(self, instrumentKeys): + self.feeder.unsubscribe(instrumentKeys) + for mode_keys in self.subscriptions.values(): + mode_keys.difference_update(instrumentKeys) + + def change_mode(self, instrumentKeys, newMode): + if not self.feeder: + raise Exception("WebSocket is not open.") + + if self.is_invalid_mode(newMode): + return + + oldMode = self.mode + self.feeder.change_mode(instrumentKeys, newMode) + # Remove keys from the old mode + self.subscriptions[oldMode].difference_update(instrumentKeys) + # Add keys to the new mode + self.subscriptions[newMode].update(instrumentKeys) + + self.mode = newMode + + def clear_subscriptions(self): + for mode_keys in self.subscriptions.values(): + mode_keys.clear() + + def decode_protobuf(self, buffer): + FeedResponse = self.protobufRoot.FeedResponse + return FeedResponse.FromString(buffer) + + def handle_open(self, ws): + self.disconnect_valid = False + self.reconnect_in_progress = False + self.reconnect_attempts = 0 + self.subscribe_to_initial_keys() + self.emit(self.Event["OPEN"]) + + def handle_message(self, ws, message): + decoded_data = self.decode_protobuf(message) + data_dict = json_format.MessageToDict(decoded_data) + self.emit(self.Event["MESSAGE"], data_dict) + + def is_invalid_mode(self, mode): + if mode not in self.Mode.values(): + self.emit(self.Event["ERROR"], f"Invalid mode provided {mode}") + return True + return False \ No newline at end of file diff --git a/upstox_client/feeder/proto/MarketDataFeedV3.proto b/upstox_client/feeder/proto/MarketDataFeedV3.proto new file mode 100644 index 0000000..1621bf5 --- /dev/null +++ b/upstox_client/feeder/proto/MarketDataFeedV3.proto @@ -0,0 +1,119 @@ +syntax = "proto3"; +package com.upstox.marketdatafeederv3udapi.rpc.proto; + +message LTPC { + double ltp = 1; + int64 ltt = 2; + double cp = 3; //close price +} + +message MarketLevel { + repeated Quote bidAskQuote = 1; +} + +message MarketOHLC { + repeated OHLC ohlc = 1; +} + +message Quote { + int64 bidQ = 1; + double bidP = 2; + int64 askQ = 3; + double askP = 4; +} + +message OptionGreeks { + double delta = 1; + double theta = 2; + double gamma = 3; + double vega = 4; + double rho = 5; +} + +message OHLC { + string interval = 1; + double open = 2; + double high = 3; + double low = 4; + double close = 5; + int64 vol = 6; + int64 ts = 7; +} + +enum Type{ + initial_feed = 0; + live_feed = 1; + market_info = 2; +} + +message MarketFullFeed{ + LTPC ltpc = 1; + MarketLevel marketLevel = 2; + OptionGreeks optionGreeks = 3; + MarketOHLC marketOHLC = 4; + double atp = 5; //avg traded price + int64 vtt = 6; //volume traded today + double oi = 7; //open interest + double iv = 8; //implied volatility + double tbq =9; //total buy quantity + double tsq = 10; //total sell quantity +} + +message IndexFullFeed{ + LTPC ltpc = 1; + MarketOHLC marketOHLC = 2; +} + + +message FullFeed { + oneof FullFeedUnion { + MarketFullFeed marketFF = 1; + IndexFullFeed indexFF = 2; + } +} + +message FirstLevelWithGreeks{ + LTPC ltpc = 1; + Quote firstDepth = 2; + OptionGreeks optionGreeks = 3; + int64 vtt = 4; //volume traded today + double oi = 5; //open interest + double iv = 6; //implied volatility +} + +message Feed { + oneof FeedUnion { + LTPC ltpc = 1; + FullFeed fullFeed = 2; + FirstLevelWithGreeks firstLevelWithGreeks = 3; + } + RequestMode requestMode = 4; +} + +enum RequestMode { + ltpc = 0; + full_d5 = 1; + option_greeks = 2; + full_d30 = 3; +} + +enum MarketStatus { + PRE_OPEN_START = 0; + PRE_OPEN_END = 1; + NORMAL_OPEN = 2; + NORMAL_CLOSE = 3; + CLOSING_START = 4; + CLOSING_END = 5; +} + + +message MarketInfo { + map segmentStatus = 1; +} + +message FeedResponse{ + Type type = 1; + map feeds = 2; + int64 currentTs = 3; + MarketInfo marketInfo = 4; +} \ No newline at end of file diff --git a/upstox_client/feeder/proto/MarketDataFeedV3_pb2.py b/upstox_client/feeder/proto/MarketDataFeedV3_pb2.py new file mode 100644 index 0000000..73b647f --- /dev/null +++ b/upstox_client/feeder/proto/MarketDataFeedV3_pb2.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: MarketDataFeedV3.proto +# Protobuf Python Version: 5.28.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 28, + 3, + '', + 'MarketDataFeedV3.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16MarketDataFeedV3.proto\x12,com.upstox.marketdatafeederv3udapi.rpc.proto\",\n\x04LTPC\x12\x0b\n\x03ltp\x18\x01 \x01(\x01\x12\x0b\n\x03ltt\x18\x02 \x01(\x03\x12\n\n\x02\x63p\x18\x03 \x01(\x01\"W\n\x0bMarketLevel\x12H\n\x0b\x62idAskQuote\x18\x01 \x03(\x0b\x32\x33.com.upstox.marketdatafeederv3udapi.rpc.proto.Quote\"N\n\nMarketOHLC\x12@\n\x04ohlc\x18\x01 \x03(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.OHLC\"?\n\x05Quote\x12\x0c\n\x04\x62idQ\x18\x01 \x01(\x03\x12\x0c\n\x04\x62idP\x18\x02 \x01(\x01\x12\x0c\n\x04\x61skQ\x18\x03 \x01(\x03\x12\x0c\n\x04\x61skP\x18\x04 \x01(\x01\"V\n\x0cOptionGreeks\x12\r\n\x05\x64\x65lta\x18\x01 \x01(\x01\x12\r\n\x05theta\x18\x02 \x01(\x01\x12\r\n\x05gamma\x18\x03 \x01(\x01\x12\x0c\n\x04vega\x18\x04 \x01(\x01\x12\x0b\n\x03rho\x18\x05 \x01(\x01\"i\n\x04OHLC\x12\x10\n\x08interval\x18\x01 \x01(\t\x12\x0c\n\x04open\x18\x02 \x01(\x01\x12\x0c\n\x04high\x18\x03 \x01(\x01\x12\x0b\n\x03low\x18\x04 \x01(\x01\x12\r\n\x05\x63lose\x18\x05 \x01(\x01\x12\x0b\n\x03vol\x18\x06 \x01(\x03\x12\n\n\x02ts\x18\x07 \x01(\x03\"\x8e\x03\n\x0eMarketFullFeed\x12@\n\x04ltpc\x18\x01 \x01(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.LTPC\x12N\n\x0bmarketLevel\x18\x02 \x01(\x0b\x32\x39.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketLevel\x12P\n\x0coptionGreeks\x18\x03 \x01(\x0b\x32:.com.upstox.marketdatafeederv3udapi.rpc.proto.OptionGreeks\x12L\n\nmarketOHLC\x18\x04 \x01(\x0b\x32\x38.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketOHLC\x12\x0b\n\x03\x61tp\x18\x05 \x01(\x01\x12\x0b\n\x03vtt\x18\x06 \x01(\x03\x12\n\n\x02oi\x18\x07 \x01(\x01\x12\n\n\x02iv\x18\x08 \x01(\x01\x12\x0b\n\x03tbq\x18\t \x01(\x01\x12\x0b\n\x03tsq\x18\n \x01(\x01\"\x9f\x01\n\rIndexFullFeed\x12@\n\x04ltpc\x18\x01 \x01(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.LTPC\x12L\n\nmarketOHLC\x18\x02 \x01(\x0b\x32\x38.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketOHLC\"\xbd\x01\n\x08\x46ullFeed\x12P\n\x08marketFF\x18\x01 \x01(\x0b\x32<.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketFullFeedH\x00\x12N\n\x07indexFF\x18\x02 \x01(\x0b\x32;.com.upstox.marketdatafeederv3udapi.rpc.proto.IndexFullFeedH\x00\x42\x0f\n\rFullFeedUnion\"\x98\x02\n\x14\x46irstLevelWithGreeks\x12@\n\x04ltpc\x18\x01 \x01(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.LTPC\x12G\n\nfirstDepth\x18\x02 \x01(\x0b\x32\x33.com.upstox.marketdatafeederv3udapi.rpc.proto.Quote\x12P\n\x0coptionGreeks\x18\x03 \x01(\x0b\x32:.com.upstox.marketdatafeederv3udapi.rpc.proto.OptionGreeks\x12\x0b\n\x03vtt\x18\x04 \x01(\x03\x12\n\n\x02oi\x18\x05 \x01(\x01\x12\n\n\x02iv\x18\x06 \x01(\x01\"\xd7\x02\n\x04\x46\x65\x65\x64\x12\x42\n\x04ltpc\x18\x01 \x01(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.LTPCH\x00\x12J\n\x08\x66ullFeed\x18\x02 \x01(\x0b\x32\x36.com.upstox.marketdatafeederv3udapi.rpc.proto.FullFeedH\x00\x12\x62\n\x14\x66irstLevelWithGreeks\x18\x03 \x01(\x0b\x32\x42.com.upstox.marketdatafeederv3udapi.rpc.proto.FirstLevelWithGreeksH\x00\x12N\n\x0brequestMode\x18\x04 \x01(\x0e\x32\x39.com.upstox.marketdatafeederv3udapi.rpc.proto.RequestModeB\x0b\n\tFeedUnion\"\xe2\x01\n\nMarketInfo\x12\x62\n\rsegmentStatus\x18\x01 \x03(\x0b\x32K.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketInfo.SegmentStatusEntry\x1ap\n\x12SegmentStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12I\n\x05value\x18\x02 \x01(\x0e\x32:.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketStatus:\x02\x38\x01\"\xe9\x02\n\x0c\x46\x65\x65\x64Response\x12@\n\x04type\x18\x01 \x01(\x0e\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.Type\x12T\n\x05\x66\x65\x65\x64s\x18\x02 \x03(\x0b\x32\x45.com.upstox.marketdatafeederv3udapi.rpc.proto.FeedResponse.FeedsEntry\x12\x11\n\tcurrentTs\x18\x03 \x01(\x03\x12L\n\nmarketInfo\x18\x04 \x01(\x0b\x32\x38.com.upstox.marketdatafeederv3udapi.rpc.proto.MarketInfo\x1a`\n\nFeedsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.com.upstox.marketdatafeederv3udapi.rpc.proto.Feed:\x02\x38\x01*8\n\x04Type\x12\x10\n\x0cinitial_feed\x10\x00\x12\r\n\tlive_feed\x10\x01\x12\x0f\n\x0bmarket_info\x10\x02*E\n\x0bRequestMode\x12\x08\n\x04ltpc\x10\x00\x12\x0b\n\x07\x66ull_d5\x10\x01\x12\x11\n\roption_greeks\x10\x02\x12\x0c\n\x08\x66ull_d30\x10\x03*{\n\x0cMarketStatus\x12\x12\n\x0ePRE_OPEN_START\x10\x00\x12\x10\n\x0cPRE_OPEN_END\x10\x01\x12\x0f\n\x0bNORMAL_OPEN\x10\x02\x12\x10\n\x0cNORMAL_CLOSE\x10\x03\x12\x11\n\rCLOSING_START\x10\x04\x12\x0f\n\x0b\x43LOSING_END\x10\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'MarketDataFeedV3_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_MARKETINFO_SEGMENTSTATUSENTRY']._loaded_options = None + _globals['_MARKETINFO_SEGMENTSTATUSENTRY']._serialized_options = b'8\001' + _globals['_FEEDRESPONSE_FEEDSENTRY']._loaded_options = None + _globals['_FEEDRESPONSE_FEEDSENTRY']._serialized_options = b'8\001' + _globals['_TYPE']._serialized_start=2524 + _globals['_TYPE']._serialized_end=2580 + _globals['_REQUESTMODE']._serialized_start=2582 + _globals['_REQUESTMODE']._serialized_end=2651 + _globals['_MARKETSTATUS']._serialized_start=2653 + _globals['_MARKETSTATUS']._serialized_end=2776 + _globals['_LTPC']._serialized_start=72 + _globals['_LTPC']._serialized_end=116 + _globals['_MARKETLEVEL']._serialized_start=118 + _globals['_MARKETLEVEL']._serialized_end=205 + _globals['_MARKETOHLC']._serialized_start=207 + _globals['_MARKETOHLC']._serialized_end=285 + _globals['_QUOTE']._serialized_start=287 + _globals['_QUOTE']._serialized_end=350 + _globals['_OPTIONGREEKS']._serialized_start=352 + _globals['_OPTIONGREEKS']._serialized_end=438 + _globals['_OHLC']._serialized_start=440 + _globals['_OHLC']._serialized_end=545 + _globals['_MARKETFULLFEED']._serialized_start=548 + _globals['_MARKETFULLFEED']._serialized_end=946 + _globals['_INDEXFULLFEED']._serialized_start=949 + _globals['_INDEXFULLFEED']._serialized_end=1108 + _globals['_FULLFEED']._serialized_start=1111 + _globals['_FULLFEED']._serialized_end=1300 + _globals['_FIRSTLEVELWITHGREEKS']._serialized_start=1303 + _globals['_FIRSTLEVELWITHGREEKS']._serialized_end=1583 + _globals['_FEED']._serialized_start=1586 + _globals['_FEED']._serialized_end=1929 + _globals['_MARKETINFO']._serialized_start=1932 + _globals['_MARKETINFO']._serialized_end=2158 + _globals['_MARKETINFO_SEGMENTSTATUSENTRY']._serialized_start=2046 + _globals['_MARKETINFO_SEGMENTSTATUSENTRY']._serialized_end=2158 + _globals['_FEEDRESPONSE']._serialized_start=2161 + _globals['_FEEDRESPONSE']._serialized_end=2522 + _globals['_FEEDRESPONSE_FEEDSENTRY']._serialized_start=2426 + _globals['_FEEDRESPONSE_FEEDSENTRY']._serialized_end=2522 +# @@protoc_insertion_point(module_scope) diff --git a/upstox_client/models/profile_data.py b/upstox_client/models/profile_data.py index a242a40..35dc2cc 100644 --- a/upstox_client/models/profile_data.py +++ b/upstox_client/models/profile_data.py @@ -130,7 +130,7 @@ def exchanges(self, exchanges): :param exchanges: The exchanges of this ProfileData. # noqa: E501 :type: list[str] """ - allowed_values = ["NSE", "NFO", "CDS", "BSE", "BFO", "BCD", "MCX"] # noqa: E501 + allowed_values = ["NSE", "NFO", "CDS", "BSE", "BFO", "BCD", "MCX", "NSCOM"] # noqa: E501 if not set(exchanges).issubset(set(allowed_values)): raise ValueError( "Invalid values for `exchanges` [{0}], must be a subset of [{1}]" # noqa: E501