""" AUTHOR: Khushal P Soonderji DATE: Tuesday, 24th Dec., 2024. OBJECTIVE: To provide a structure to represent trading tick updates. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # For making data behaviour_models: from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator, AwareDatetime, computed_field from typing import Optional, Literal, Union, List # My utils: from utils_v2.string import json from utils_v2.string import regex from utils_v2.date_time import date_time # To work with date and time: import datetime # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** class OneMarketDepth(BaseModel): price: float = Field( description = "a price at which trader(s) are willing to trade this instrument", frozen = True, alias = "price" ) qty: int = Field( description = "the no. of shares available at the above price", frozen = True, alias = "quantity" ) orders: int = Field( description = "how many orders have contributed to the above quantity", frozen = True, alias = "orders" ) @computed_field def lqdty(self) -> float: return self.price * self.qty # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ # ┛ class Config: extra = "forbid" # --------------------------------------------------------------------------------------------------------------------- class MarketDepth(BaseModel): buy: List[OneMarketDepth] = Field( description = "the buying side market depth", frozen = True ) sell: List[OneMarketDepth] = Field( description = "the selling side market depth", frozen = True ) # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ # ┛ class Config: extra = "forbid" # --------------------------------------------------------------------------------------------------------------------- class TradingTick(BaseModel): symbol: str = Field( description = "the symbol of the instrument" ) exchange: Literal["NSE", "NFO", "BSE", "BFO", "MCX", "CDS", "BCD"] = Field( description = "the exchange on which this instrument is traded", frozen = True ) exchangeToken: str | int = Field( description = "the code by which the exchange identifies this instrument", frozen = True ) broker: Literal["zerodhaKite", "iciciBreeze"] = Field( description = "the broker that gave you the details of this instrument", frozen = True ) brokerToken: str | int = Field( description = "the code by which the broker identifies this instrument", frozen = True ) tradeable: bool = Field( description = "whether, or not, this instrument is tradeable", frozen = True ) segment: str = Field( description = "the segment which this asset represents", frozen = True ) type: str = Field( description = "the type of the instrument in the segment", frozen = True ) strike: int | float | None = Field( description = "the strike price of the instrument if it is a derivative", default = None, frozen = True ) expiryTs: AwareDatetime | None = Field( description = "the expiry (utc) of this instrument if it is a derivative", default = None, frozen = True ) expiryTz: str | None = Field( description = "the timezone in which the expiry timestamp my be interpreted; should be compatible with pytz", frozen = True, examples = ["UTC", "Asia/Kolkata"] ) ltp: float = Field( description = "the last price of this instrument at the time of requesting the symbol list", frozen = True ) qty: int = Field( description = "how many units were traded in this tick" ) chg: float = Field( description = "the absolute change since the previous close", frozen=True ) pChg: float = Field( description = "the percentage change since last close", frozen = True ) o: float = Field( description = "this session's open price", frozen = True ) h: float = Field( description = "this session's high price", frozen = True ) l: float = Field( description = "this session's low price", frozen = True ) c: float = Field( description = "this session's close price", frozen = True ) totVol: int = Field( description = "the total volume of this instrument that has been traded in this session", frozen = True ) vwap: float | None = Field( description = "the volume weighted average price in this session", frozen = True ) totBuyQty: int = Field( description = "the total open buy qty. on the exchange for this symbol", frozen = True ) totSellQty: int = Field( description = "the total open sell qty. on the exchange for this symbol", frozen = True ) oi: int | None = Field( description = "the total open interest of this instrument (if derivative)", frozen = True ) oiDayHigh: int | None = Field( description = "this session's highest open interest of this instrument (if derivative)", frozen = True ) oiDayLow: int | None = Field( description = "this session's lowest open interest of this instrument (if derivative)", frozen = True ) tradeTs: AwareDatetime = Field( description = "the last trade time (utc) of this instrument", frozen = True ) tradeTz: str = Field( description = "the timezone in which the last trade time should be interpreted; should be compatible with pytz", frozen = True, examples = ["UTC", "Asia/Kolkata"] ) exchgTs: AwareDatetime = Field( description = "the time (utc) at which this update was received from the exchange", frozen = True ) exchgTz: str = Field( description = "the timezone in which the exchange's time should be interpreted; should be compatible with pytz", frozen = True, examples = ["UTC", "Asia/Kolkata"] ) depth: MarketDepth = Field( description = "the market depth data for this instrument at the time of this update" ) # ┏┓ ┏┓ ┓ ┏┓• ┓ ┓ # ┣┫┓┏╋┏┓━━┃ ┏┓┏┳┓┏┓┓┏╋┏┓┏┫ ┣ ┓┏┓┃┏┫┏ # ┛┗┗┻┗┗┛ ┗┛┗┛┛┗┗┣┛┗┻┗┗ ┗┻ ┻ ┗┗ ┗┗┻┛ # ┛ @computed_field def tickCashflow(self) -> float: return self.qty * self.ltp @computed_field def totCashflow(self) -> float: return self.totVol * self.vwap # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ # ┗┛┗┛┛┗┛┗┗┫ # ┛ class Config: extra = "forbid" # ┏┓ ┏┓ # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ @staticmethod def from_zerodha_kite( ticks: dict | List[dict], instrument_lookup: dict ) -> list: # Ensure that we are working with a list: if not isinstance(ticks, list): ticks = [ticks] # Iterate through the ticks and fit them into the model: modelled_ticks = [] for tick in ticks: # Stash frequently needed vars: broker_token = tick["instrument_token"] tick_lookup = instrument_lookup[broker_token] change = tick["change"] last_price = tick["last_price"] # Model the currently picked tick: modelled_ticks.append( TradingTick( symbol = tick_lookup["symbol"], exchange = tick_lookup["exchange"], exchangeToken = tick_lookup["exchangeToken"], broker = "zerodhaKite", brokerToken = broker_token, tradeable = tick["tradable"], segment = tick_lookup["segment"], type = tick_lookup["type"], strike = tick_lookup.get("strike"), expiryTs = tick_lookup["expiryTs"], expiryTz = tick_lookup["expiryTz"], ltp = last_price, qty = tick["last_traded_quantity"], chg = change, pChg = change / (last_price - change), o = tick["ohlc"]["open"], h = tick["ohlc"]["high"], l = tick["ohlc"]["low"], c = tick["ohlc"]["close"], totVol = tick["volume_traded"], vwap = tick["average_traded_price"], totBuyQty = tick["total_buy_quantity"], totSellQty = tick["total_sell_quantity"], oi = tick["oi"], oiDayHigh = tick["oi_day_high"], oiDayLow = tick["oi_day_low"], tradeTs = tick["last_trade_time"], tradeTz = "Asia/Kolkata", exchgTs = tick["exchange_timestamp"], exchgTz = "Asia/Kolkata", depth = tick["depth"] ) ) # Done here: return modelled_ticks # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ @field_validator( "expiryTs", "tradeTs", "exchgTs", mode = "before" ) def parse_date_time(cls, value): # If the input is null, # we can't do anything: if not value: value = None # When the input is a string, we try to parse it: elif isinstance(value, str): value = value.strip() value = date_time.parse_date_time( input_value = value, timezone = date_time.TIMEZONE_UTC, date_formats = [ "%Y-%m-%d", "%Y-%m-%d %H:%M:%S", ] ) # When the input is a datetime obj., # we only work on the timezone: elif isinstance(value, datetime.datetime): value = date_time.to_timezone( value, timezone = date_time.TIMEZONE_UTC ) # Done here: return value # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": zerodha_tick = { "tradable": True, "mode": "full", "instrument_token": 408065, "last_price": 1928.25, "last_traded_quantity": 157, "average_traded_price": 1942.5, "volume_traded": 5596931, "total_buy_quantity": 217126, "total_sell_quantity": 380092, "ohlc": { "open": 1975.15, "high": 1979.95, "low": 1911.25, "close": 1946.2 }, "change": -0.9223101428424646, "last_trade_time": "2024-12-20 14:46:14", "oi": 0, "oi_day_high": 0, "oi_day_low": 0, "exchange_timestamp": "2024-12-20 14:46:15", "depth": { "buy": [ { "quantity": 3, "price": 1928.15, "orders": 2 }, { "quantity": 6, "price": 1927.95, "orders": 2 }, { "quantity": 66, "price": 1927.7, "orders": 2 }, { "quantity": 25, "price": 1927.65, "orders": 1 }, { "quantity": 194, "price": 1927.6, "orders": 6 } ], "sell": [ { "quantity": 134, "price": 1928.25, "orders": 5 }, { "quantity": 3, "price": 1928.3, "orders": 1 }, { "quantity": 560, "price": 1928.35, "orders": 2 }, { "quantity": 64, "price": 1928.4, "orders": 2 }, { "quantity": 400, "price": 1928.45, "orders": 1 } ] } } zerodha_lookup = { 408065: { "symbol": "MYSTOCK", "exchange": "NSE", "exchangeToken": 12345678, "segment": "NFO-OPT", "type": "CE", "expiryTs": "2024-12-20", "expiryTz": "Asia/Kolkata" } } my_ticks = TradingTick.from_zerodha_kite( ticks = [zerodha_tick] * 10_000, instrument_lookup = zerodha_lookup ) print(json.to_string(my_ticks[0].model_dump(), default = str))