Files
api_utils_converse_v2/models/finstitutions/trading/ticks.py
T

586 lines
18 KiB
Python

"""
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"
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
@field_validator("buy", mode = "after")
def sort_buying_depth(cls, value):
value.sort(key = lambda x: x.price, reverse = True)
return value
@field_validator("sell", mode = "after")
def sort_selling_depth(cls, value):
value.sort(key = lambda x: x.price, reverse = False)
return value
# ---------------------------------------------------------------------------------------------------------------------
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"]
)
prevClose: float = Field(
description = "the closing price of this instrument on the previous day",
frozen = True
)
ltp: float = Field(
description = "the last price of this instrument at the time of requesting the symbol list",
frozen = True
)
qty: int | None = 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 highest price",
frozen = True
)
l: float = Field(
description = "this session's lowest price",
frozen = True
)
c: float = Field(
description = "this session's close price; typically the same as the ltp",
frozen = True
)
totVol: int | None = 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 | None = Field(
description = "the total open buy qty. on the exchange for this symbol",
frozen = True
)
totSellQty: int | None = 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 | None = Field(
description = "the last trade time (utc) of this instrument",
frozen = True
)
tradeTz: str | None = 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 | None = Field(
description = "the time (utc) at which this update was received from the exchange",
frozen = True
)
exchgTz: str | None = 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 | None = Field(
description = "the market depth data for this instrument at the time of this update"
)
# ┏┓ ┏┓ ┓ ┏┓• ┓ ┓
# ┣┫┓┏╋┏┓━━┃ ┏┓┏┳┓┏┓┓┏╋┏┓┏┫ ┣ ┓┏┓┃┏┫┏
# ┛┗┗┻┗┗┛ ┗┛┗┛┛┗┗┣┛┗┻┗┗ ┗┻ ┻ ┗┗ ┗┗┻┛
# ┛
@computed_field
def tickCashflow(self) -> float | None:
if self.qty is not None and self.ltp is not None:
return self.qty * self.ltp
@computed_field
def totCashflow(self) -> float | None:
if self.totVol is not None and self.vwap is not None:
return self.totVol * self.vwap
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ┏┓ ┏┓
# ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏
# ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛
@property
def summary(self):
highest_bid = None
lowest_ask = None
if self.depth:
highest_bid = self.depth.buy[0] if self.depth.buy else None
lowest_ask = self.depth.sell[0] if self.depth.sell else None
return {
"broker": self.broker,
"brokerToken": self.brokerToken,
"exchange": self.exchange,
"exchangeToken": self.exchangeToken,
"segment": self.segment,
"type": self.type,
"symbol": self.symbol,
"expiry": date_time.to_timezone(
self.expiryTs,
timezone = self.expiryTz
).strftime("%Y-%m-%d") if self.expiryTs is not None else None,
"strike": self.strike,
"bidQty": highest_bid.qty if highest_bid else None,
"bidRate": highest_bid.price if highest_bid else None,
"askQty": lowest_ask.qty if lowest_ask else None,
"askRate": lowest_ask.price if lowest_ask else None,
"ltp": self.ltp,
"chg": self.chg,
"pChg": self.pChg,
"totVol": self.totVol
}
@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]
last_price = tick["last_price"]
prev_close = tick["ohlc"]["close"]
change = last_price - prev_close
p_change = tick["change"]
# 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.get("expiryTs"),
expiryTz = tick_lookup.get("expiryTz"),
prevClose = prev_close,
ltp = last_price,
qty = tick.get("last_traded_quantity"),
chg = change,
pChg = p_change,
o = tick["ohlc"]["open"],
h = tick["ohlc"]["high"],
l = tick["ohlc"]["low"],
c = last_price,
totVol = tick.get("volume_traded"),
vwap = tick.get("average_traded_price"),
totBuyQty = tick.get("total_buy_quantity"),
totSellQty = tick.get("total_sell_quantity"),
oi = tick.get("oi"),
oiDayHigh = tick.get("oi_day_high"),
oiDayLow = tick.get("oi_day_low"),
tradeTs = tick.get("last_trade_time"),
tradeTz = "Asia/Kolkata",
exchgTs = tick.get("exchange_timestamp"),
exchgTz = "Asia/Kolkata",
depth = tick.get("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_a = {
"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_tick_b = {
}
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_a],
instrument_lookup = zerodha_lookup
)
print(json.to_string(my_ticks[0].model_dump(), default = str))
print(json.to_string(my_ticks[0].summary, default = str))