quantplay 2.0.178__tar.gz → 2.0.181__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantplay-2.0.178 → quantplay-2.0.181}/PKG-INFO +1 -1
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/broker_factory.py +2 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/icici_direct.py +2 -2
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/kotak.py +72 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/noren.py +1 -1
- quantplay-2.0.181/quantplay/core/strategy.py +193 -0
- quantplay-2.0.181/quantplay/indicator/iv.py +37 -0
- quantplay-2.0.181/quantplay/strategy/iv_spike.py +93 -0
- quantplay-2.0.181/quantplay/strategy/obuy.py +51 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/wrapper/redis.py +2 -2
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay.egg-info/PKG-INFO +1 -1
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay.egg-info/SOURCES.txt +7 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/setup.py +1 -1
- quantplay-2.0.181/tests/conftest.py +0 -0
- quantplay-2.0.181/tests/wrapper/__init__.py +0 -0
- quantplay-2.0.181/tests/wrapper/aws/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/README.md +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/pyproject.toml +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/aliceblue.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/angelone.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/auto_login/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/auto_login/aliceblue.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/breeze/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/breeze/breeze_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/dhan.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/finvasia_utils/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/finvasia_utils/fa_noren.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/five_paisa.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/flattrade.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/ft_utils/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/ft_utils/flattrade_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/ft_utils/ft_noren.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/generics/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/generics/broker.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/iifl_xts.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/jainam_xts.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/kite_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/motilal.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/shoonya.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/uplink/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/uplink/uplink_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/upstox.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts_utils/Connect.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts_utils/Exception.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts_utils/InteractiveSocketClient.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts_utils/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/zerodha.py +0 -0
- {quantplay-2.0.178/quantplay/model → quantplay-2.0.181/quantplay/core}/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/exception/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/exception/exceptions.py +0 -0
- {quantplay-2.0.178/quantplay/wrapper → quantplay-2.0.181/quantplay/indicator}/__init__.py +0 -0
- {quantplay-2.0.178/quantplay/wrapper/aws → quantplay-2.0.181/quantplay/model}/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/model/broker.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/model/broker_response.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/model/generics.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/model/instrument_data.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/model/order_event.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/py.typed +0 -0
- {quantplay-2.0.178/tests → quantplay-2.0.181/quantplay/strategy}/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/caching.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/constant.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/exchange.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/number_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/pickle_utils.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/utils/selenium_utils.py +0 -0
- {quantplay-2.0.178/tests → quantplay-2.0.181/quantplay}/wrapper/__init__.py +0 -0
- {quantplay-2.0.178/tests → quantplay-2.0.181/quantplay}/wrapper/aws/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay/wrapper/aws/s3.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay.egg-info/dependency_links.txt +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay.egg-info/requires.txt +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/quantplay.egg-info/top_level.txt +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/setup.cfg +0 -0
- /quantplay-2.0.178/tests/conftest.py → /quantplay-2.0.181/tests/__init__.py +0 -0
- {quantplay-2.0.178 → quantplay-2.0.181}/tests/wrapper/aws/s3_test.py +0 -0
|
@@ -182,6 +182,7 @@ class BrokerFactory:
|
|
|
182
182
|
|
|
183
183
|
elif broker == Broker.KOTAK:
|
|
184
184
|
broker_client = Kotak(
|
|
185
|
+
user_id=broker_data["user_id"],
|
|
185
186
|
consumer_key=broker_data["consumer_key"],
|
|
186
187
|
consumer_secret=broker_data["consumer_secret"],
|
|
187
188
|
mobilenumber=broker_data["mobilenumber"],
|
|
@@ -384,6 +385,7 @@ class BrokerFactory:
|
|
|
384
385
|
|
|
385
386
|
elif broker == Broker.KOTAK:
|
|
386
387
|
broker_client = Kotak(
|
|
388
|
+
user_id=broker_data["user_id"],
|
|
387
389
|
configuration=broker_data["configuration"],
|
|
388
390
|
otp=broker_data.get("otp"),
|
|
389
391
|
load_instrument=load_instrument,
|
|
@@ -417,7 +417,7 @@ class ICICI(Broker):
|
|
|
417
417
|
margins = self.invoke_wrapper(self.wrapper.get_funds)
|
|
418
418
|
|
|
419
419
|
return {
|
|
420
|
-
"total_balance": margins["total_bank_balance"],
|
|
420
|
+
"total_balance": float(margins["total_bank_balance"]),
|
|
421
421
|
"margin_available": (
|
|
422
422
|
float(margins["allocated_equity"])
|
|
423
423
|
+ float(margins["allocated_fno"])
|
|
@@ -430,7 +430,7 @@ class ICICI(Broker):
|
|
|
430
430
|
+ float(margins["block_by_trade_commodity"])
|
|
431
431
|
+ float(margins["block_by_trade_currency"])
|
|
432
432
|
),
|
|
433
|
-
"cash": margins["unallocated_balance"],
|
|
433
|
+
"cash": float(margins["unallocated_balance"]),
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
def ltp(self, exchange: ExchangeType, tradingsymbol: str) -> float:
|
|
@@ -10,6 +10,7 @@ import urllib
|
|
|
10
10
|
import jwt
|
|
11
11
|
|
|
12
12
|
from quantplay.broker.generics.broker import Broker
|
|
13
|
+
from quantplay.broker.kotak_utils.kotak_ws import NeoWebSocket
|
|
13
14
|
from quantplay.model.broker import (
|
|
14
15
|
MarginsResponse,
|
|
15
16
|
ModifyOrderRequest,
|
|
@@ -20,6 +21,7 @@ from quantplay.model.generics import (
|
|
|
20
21
|
OrderTypeType,
|
|
21
22
|
ProductType,
|
|
22
23
|
TransactionType,
|
|
24
|
+
OrderStatusType,
|
|
23
25
|
)
|
|
24
26
|
from quantplay.model.order_event import OrderUpdateEvent
|
|
25
27
|
from quantplay.exception.exceptions import (
|
|
@@ -40,6 +42,7 @@ class Kotak(Broker):
|
|
|
40
42
|
|
|
41
43
|
def __init__(
|
|
42
44
|
self,
|
|
45
|
+
user_id: str,
|
|
43
46
|
order_updates: Queue[OrderUpdateEvent] | None = None,
|
|
44
47
|
consumer_key: str | None = None,
|
|
45
48
|
consumer_secret: str | None = None,
|
|
@@ -76,6 +79,8 @@ class Kotak(Broker):
|
|
|
76
79
|
|
|
77
80
|
if load_instrument:
|
|
78
81
|
self.load_instrument()
|
|
82
|
+
self.order_updates = order_updates
|
|
83
|
+
self.user_id = user_id
|
|
79
84
|
|
|
80
85
|
def load_instrument(self, file_name: str | None = None) -> None:
|
|
81
86
|
try:
|
|
@@ -573,6 +578,25 @@ class Kotak(Broker):
|
|
|
573
578
|
|
|
574
579
|
return order_type_map.get(order_type, order_type)
|
|
575
580
|
|
|
581
|
+
def get_quantplay_order_type(self, order_type: str) -> OrderTypeType:
|
|
582
|
+
order_type_map: dict[str, str] = {
|
|
583
|
+
"L": "LIMIT",
|
|
584
|
+
"MKT": "MARKET",
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return order_type_map.get(order_type, order_type) # type:ignore
|
|
588
|
+
|
|
589
|
+
def get_quantplay_order_status(self, status: str) -> OrderStatusType:
|
|
590
|
+
if status in ["put order req received", "validation pending", "open pending"]:
|
|
591
|
+
return "OPEN"
|
|
592
|
+
if status == "cancel pending":
|
|
593
|
+
return "CANCELLED"
|
|
594
|
+
status = status.upper()
|
|
595
|
+
if status not in ["OPEN", "TRIGGER_PENDING", "CANCELLED", "REJECTED"]:
|
|
596
|
+
Constants.logger.error(f"{status} not supported for Kotak")
|
|
597
|
+
return "INVALID STATUS" # type:ignore
|
|
598
|
+
return status # type:ignore
|
|
599
|
+
|
|
576
600
|
def get_exchange(self, exchange: ExchangeType) -> str:
|
|
577
601
|
exchange_segment_map: dict[ExchangeType, str] = {
|
|
578
602
|
"NSE": "nse_cm",
|
|
@@ -739,3 +763,51 @@ class Kotak(Broker):
|
|
|
739
763
|
raise BrokerException("Request Failed: No response from Kotak server")
|
|
740
764
|
|
|
741
765
|
return resp_data
|
|
766
|
+
|
|
767
|
+
def event_handler_order_update(self, order: dict[str, Any]):
|
|
768
|
+
if "data" not in order:
|
|
769
|
+
return
|
|
770
|
+
data = json.loads(order["data"])
|
|
771
|
+
if data["type"] != "order":
|
|
772
|
+
return
|
|
773
|
+
order = data["data"]
|
|
774
|
+
|
|
775
|
+
quantplay_order: OrderUpdateEvent = {
|
|
776
|
+
"placed_by": self.user_id, # type:ignore
|
|
777
|
+
"tradingsymbol": order["trdSym"],
|
|
778
|
+
"exchange": self.get_quantplay_exchange(order["exSeg"]),
|
|
779
|
+
"tag": order["GuiOrdId"],
|
|
780
|
+
"order_id": order["nOrdNo"],
|
|
781
|
+
"exchange_order_id": order["exOrdId"],
|
|
782
|
+
"order_type": self.get_quantplay_order_type(order["prcTp"]),
|
|
783
|
+
"price": float(order["prc"]),
|
|
784
|
+
"quantity": int(order["qty"]),
|
|
785
|
+
"product": order["prod"],
|
|
786
|
+
"status": self.get_quantplay_order_status(order["ordSt"]),
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if quantplay_order["exchange"] == "NSE":
|
|
790
|
+
quantplay_order["tradingsymbol"] = quantplay_order["tradingsymbol"].replace(
|
|
791
|
+
"-EQ", ""
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
quantplay_order["transaction_type"] = "BUY" if order["trnsTp"] == "B" else "SELL"
|
|
795
|
+
if "trgPrc" in order:
|
|
796
|
+
quantplay_order["trigger_price"] = float(order["trgPrc"])
|
|
797
|
+
else:
|
|
798
|
+
quantplay_order["trigger_price"] = None
|
|
799
|
+
print(quantplay_order)
|
|
800
|
+
|
|
801
|
+
if self.order_updates:
|
|
802
|
+
self.order_updates.put(quantplay_order)
|
|
803
|
+
|
|
804
|
+
def stream_order_data(self):
|
|
805
|
+
ws = NeoWebSocket(
|
|
806
|
+
sid=self.configuration.get("sid", ""),
|
|
807
|
+
token=self.configuration.get("edit_token", ""),
|
|
808
|
+
server_id=self.configuration.get("serverId", ""),
|
|
809
|
+
)
|
|
810
|
+
ws.on_message = self.event_handler_order_update
|
|
811
|
+
ws.on_error = lambda x: print(x)
|
|
812
|
+
|
|
813
|
+
ws.get_order_feed()
|
|
@@ -764,7 +764,7 @@ class Noren(Broker):
|
|
|
764
764
|
cash = 0
|
|
765
765
|
|
|
766
766
|
margins: MarginsResponse = {
|
|
767
|
-
"margin_used": api_margins["margin_used"],
|
|
767
|
+
"margin_used": float(api_margins["margin_used"]),
|
|
768
768
|
"margin_available": margin_available,
|
|
769
769
|
"cash": cash,
|
|
770
770
|
"total_balance": float(cash) + float(holdings_val),
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import polars as pl
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
pl.Config.set_tbl_cols(10)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Strategy:
|
|
9
|
+
def __init__(self, name: str = "BaseStrategy") -> None:
|
|
10
|
+
self.index: str
|
|
11
|
+
self.type: str
|
|
12
|
+
self.exit_time: str
|
|
13
|
+
self.index: str
|
|
14
|
+
|
|
15
|
+
self.name = name
|
|
16
|
+
self.underlying_map = {"NIFTY BANK": "BANKNIFTY"}
|
|
17
|
+
self.data_source = "~/.quantplay"
|
|
18
|
+
self.symbol_expiry = pl.read_parquet(f"{self.data_source}/symbol_expiry.parquet")
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
|
|
22
|
+
self.logger = logging.getLogger("testing")
|
|
23
|
+
|
|
24
|
+
def add_days_to_expiry(self, trades: pl.DataFrame) -> pl.DataFrame:
|
|
25
|
+
trades = trades.join(self.symbol_expiry, on=["symbol"], how="left")
|
|
26
|
+
trades = trades.with_columns(
|
|
27
|
+
pl.col("expiry")
|
|
28
|
+
.sub(pl.col("date_only"))
|
|
29
|
+
.dt.total_days()
|
|
30
|
+
.alias("days_to_expiry")
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return trades
|
|
34
|
+
|
|
35
|
+
def backtest(self, trades: pl.DataFrame):
|
|
36
|
+
"""
|
|
37
|
+
Backtest the strategy using historical data.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
data (pl.DataFrame): Historical OHLC data.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
pl.DataFrame: DataFrame containing signals and performance metrics.
|
|
44
|
+
"""
|
|
45
|
+
underlying_name = self.underlying_map[self.index]
|
|
46
|
+
opt_data = pl.read_parquet(
|
|
47
|
+
f"{self.data_source}/RAW/OPT/{underlying_name}.parquet"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if self.type == "intraday":
|
|
51
|
+
trades = trades.select(
|
|
52
|
+
[
|
|
53
|
+
"date",
|
|
54
|
+
"date_only",
|
|
55
|
+
"underlying_price",
|
|
56
|
+
"symbol",
|
|
57
|
+
"close",
|
|
58
|
+
]
|
|
59
|
+
).rename({"close": "entry_price", "date": "entry_time"})
|
|
60
|
+
|
|
61
|
+
market_data = (
|
|
62
|
+
opt_data.filter(
|
|
63
|
+
pl.col("date")
|
|
64
|
+
.dt.time()
|
|
65
|
+
.eq(datetime.strptime(self.exit_time, "%H:%M").time())
|
|
66
|
+
)
|
|
67
|
+
.select(["symbol", "date", "close"])
|
|
68
|
+
.with_columns(pl.col("date").dt.date().alias("date_only"))
|
|
69
|
+
.rename({"close": "exit_price", "date": "exit_time"})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
bt_trades = trades.join(market_data, on=["date_only", "symbol"], how="left")
|
|
73
|
+
bt_trades = bt_trades.with_columns(
|
|
74
|
+
(pl.col("exit_price").sub(pl.col("entry_price")).alias("pnl"))
|
|
75
|
+
).select(
|
|
76
|
+
[
|
|
77
|
+
"symbol",
|
|
78
|
+
"entry_time",
|
|
79
|
+
"exit_time",
|
|
80
|
+
"entry_price",
|
|
81
|
+
"exit_price",
|
|
82
|
+
"pnl",
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
print(bt_trades)
|
|
87
|
+
print(bt_trades.select(pl.col("exit_price").sub(pl.col("entry_price"))).sum())
|
|
88
|
+
bt_trades.write_parquet(f"./out/{self.name}.parquet")
|
|
89
|
+
|
|
90
|
+
elif self.type == "overnight":
|
|
91
|
+
trades = (
|
|
92
|
+
trades.with_columns(
|
|
93
|
+
pl.col("date_only")
|
|
94
|
+
.add(
|
|
95
|
+
pl.when(pl.col("date_only").dt.weekday().eq(5))
|
|
96
|
+
.then(pl.duration(days=3))
|
|
97
|
+
.when(pl.col("date_only").dt.weekday().eq(6))
|
|
98
|
+
.then(pl.duration(days=2))
|
|
99
|
+
.otherwise(pl.duration(days=1))
|
|
100
|
+
)
|
|
101
|
+
.alias("next_day")
|
|
102
|
+
)
|
|
103
|
+
.select(
|
|
104
|
+
[
|
|
105
|
+
"date",
|
|
106
|
+
"next_day",
|
|
107
|
+
"date_only",
|
|
108
|
+
"underlying_price",
|
|
109
|
+
"symbol",
|
|
110
|
+
"close",
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
.rename({"close": "entry_price", "date": "entry_time"})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
market_data = (
|
|
117
|
+
opt_data.filter(pl.col("date").dt.time().eq(pl.time(9, 30, 0)))
|
|
118
|
+
.select(["symbol", "date", "close"])
|
|
119
|
+
.with_columns(pl.col("date").dt.date().alias("next_day"))
|
|
120
|
+
.rename({"close": "exit_price", "date": "exit_time"})
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
bt_trades = trades.join(market_data, on=["next_day", "symbol"], how="left")
|
|
124
|
+
|
|
125
|
+
bt_trades = bt_trades.with_columns(
|
|
126
|
+
(pl.col("exit_price").sub(pl.col("entry_price")).alias("pnl"))
|
|
127
|
+
).select(
|
|
128
|
+
[
|
|
129
|
+
"symbol",
|
|
130
|
+
"entry_time",
|
|
131
|
+
"exit_time",
|
|
132
|
+
"entry_price",
|
|
133
|
+
"exit_price",
|
|
134
|
+
"pnl",
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
print(bt_trades)
|
|
139
|
+
print(bt_trades.select(pl.col("exit_price").sub(pl.col("entry_price"))).sum())
|
|
140
|
+
|
|
141
|
+
bt_trades.write_parquet(f"./out/{self.name}.parquet")
|
|
142
|
+
|
|
143
|
+
def calculate_performance(self, data: pl.DataFrame) -> pl.DataFrame:
|
|
144
|
+
"""
|
|
145
|
+
Calculate performance metrics based on signals.vv
|
|
146
|
+
|
|
147
|
+
Parameters:
|
|
148
|
+
data (pl.DataFrame): DataFrame with signals.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
pl.DataFrame: DataFrame with performance metrics.
|
|
152
|
+
"""
|
|
153
|
+
# Implement performance calculation
|
|
154
|
+
raise NotImplementedError("Please implement the calculate_performance method.")
|
|
155
|
+
|
|
156
|
+
def load_data(self) -> None:
|
|
157
|
+
self.logger.info("Loading data")
|
|
158
|
+
# Load data based on the provided data format
|
|
159
|
+
|
|
160
|
+
index_data = pl.read_parquet(
|
|
161
|
+
f"{self.data_source}/NSE/INDICES/minute/{self.index}.parquet"
|
|
162
|
+
)
|
|
163
|
+
underlying_name = self.underlying_map[self.index]
|
|
164
|
+
opt_data = pl.read_parquet(
|
|
165
|
+
f"{self.data_source}/RAW/OPT/{underlying_name}.parquet"
|
|
166
|
+
)
|
|
167
|
+
self.data = {"index_data": index_data, "opt_data": opt_data}
|
|
168
|
+
self.logger.info("Data loaded successfully ....")
|
|
169
|
+
|
|
170
|
+
def run_backtest(self, **kwargs: ...):
|
|
171
|
+
"""
|
|
172
|
+
Run backtest for a given strategy by loading data internally.
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
strategy (Strategy): An instance of a Strategy subclass.
|
|
176
|
+
**kwargs: Additional keyword arguments for data loading.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
pl.DataFrame: DataFrame with backtest results.
|
|
180
|
+
"""
|
|
181
|
+
# Set up logging
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
self.load_data()
|
|
185
|
+
trades = self.generate_signals(self.data)
|
|
186
|
+
self.backtest(trades)
|
|
187
|
+
|
|
188
|
+
self.logger.info("Backtest completed successfully.")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
self.logger.error(f"An error occurred during backtest: {e}")
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
def generate_signals(self, data: dict[str, pl.DataFrame]) -> pl.DataFrame: ...
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from py_vollib.black_scholes.implied_volatility import implied_volatility # type:ignore
|
|
2
|
+
from datetime import date, datetime, time
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImpliedVolatility:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def get_option_type(option_type: Literal["CE", "PE"]) -> Literal["c", "p"]:
|
|
9
|
+
if option_type == "CE":
|
|
10
|
+
return "c"
|
|
11
|
+
|
|
12
|
+
elif option_type == "PE":
|
|
13
|
+
return "p"
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def time_to_expiry(expiry: date, tick_time: float) -> float:
|
|
17
|
+
expiry_dt = datetime.combine(date=expiry, time=time(15, 30, 0, 0))
|
|
18
|
+
time_to_expiry = (expiry_dt.timestamp() - tick_time) / 31536000
|
|
19
|
+
|
|
20
|
+
return time_to_expiry
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def iv(
|
|
24
|
+
option_price: float,
|
|
25
|
+
strike: int,
|
|
26
|
+
option_type: Literal["CE", "PE"],
|
|
27
|
+
underlying_price: float,
|
|
28
|
+
tte: float,
|
|
29
|
+
) -> float:
|
|
30
|
+
rate_of_interest = 0.1
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
implied_volatility( # type:ignore
|
|
34
|
+
option_price, underlying_price, strike, tte, rate_of_interest, option_type
|
|
35
|
+
)
|
|
36
|
+
* 100
|
|
37
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from datetime import time
|
|
3
|
+
|
|
4
|
+
from quantplay.core.strategy import Strategy
|
|
5
|
+
from quantplay.indicator.iv import ImpliedVolatility
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IVSpike(Strategy):
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
super().__init__(name="iv_spike")
|
|
11
|
+
self.type = "intraday"
|
|
12
|
+
self.exit_time = "15:10"
|
|
13
|
+
self.index = "NIFTY BANK"
|
|
14
|
+
|
|
15
|
+
def generate_signals(self, data: dict[str, pl.DataFrame]) -> pl.DataFrame:
|
|
16
|
+
"""
|
|
17
|
+
Generate signals based on below logic
|
|
18
|
+
"""
|
|
19
|
+
index_data = data["index_data"]
|
|
20
|
+
opt_data = data["opt_data"]
|
|
21
|
+
|
|
22
|
+
index_data = index_data.with_columns(pl.col("close").alias("underlying_price"))
|
|
23
|
+
|
|
24
|
+
opt_data = opt_data.with_columns(pl.col("date").dt.date().alias("date_only"))
|
|
25
|
+
opt_data = self.add_days_to_expiry(opt_data)
|
|
26
|
+
|
|
27
|
+
opt_data = opt_data.filter(pl.col("days_to_expiry").lt(2))
|
|
28
|
+
opt_data = opt_data.join(
|
|
29
|
+
index_data["date", "underlying_price"],
|
|
30
|
+
on=["date"],
|
|
31
|
+
how="left",
|
|
32
|
+
)
|
|
33
|
+
opt_data = opt_data.filter(
|
|
34
|
+
pl.col("date").dt.time() <= pl.time(hour=14, minute=45)
|
|
35
|
+
)
|
|
36
|
+
opt_data = opt_data.filter(pl.col("date").dt.year() >= 2024)
|
|
37
|
+
opt_data = opt_data.filter(pl.col("date").dt.month() >= 8)
|
|
38
|
+
print(len(opt_data))
|
|
39
|
+
|
|
40
|
+
opt_data = opt_data.with_columns(
|
|
41
|
+
pl.col("symbol")
|
|
42
|
+
.str.extract(r"[A-Z]+[0-9]{2}.{3}([0-9]+)[P|C]E")
|
|
43
|
+
.cast(pl.Float64)
|
|
44
|
+
.alias("strike"),
|
|
45
|
+
pl.col("symbol")
|
|
46
|
+
.str.extract(r"[A-Z]+[0-9]{2}.{3}[0-9]+([P|C])E")
|
|
47
|
+
.str.to_lowercase()
|
|
48
|
+
.alias("option_type"),
|
|
49
|
+
(
|
|
50
|
+
(pl.col("expiry").dt.combine(time(15, 30, 0)).dt.timestamp("ms"))
|
|
51
|
+
.sub(pl.col("date").dt.timestamp("ms"))
|
|
52
|
+
.truediv(31536000000)
|
|
53
|
+
).alias("tte"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
opt_data = opt_data.with_columns(
|
|
57
|
+
pl.struct(
|
|
58
|
+
[
|
|
59
|
+
"close",
|
|
60
|
+
"strike",
|
|
61
|
+
"option_type",
|
|
62
|
+
"underlying_price",
|
|
63
|
+
"tte",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
.map_elements(
|
|
67
|
+
lambda x: int(
|
|
68
|
+
ImpliedVolatility.iv(
|
|
69
|
+
x["close"],
|
|
70
|
+
x["strike"],
|
|
71
|
+
x["option_type"],
|
|
72
|
+
x["underlying_price"],
|
|
73
|
+
x["tte"],
|
|
74
|
+
)
|
|
75
|
+
),
|
|
76
|
+
return_dtype=pl.Float64,
|
|
77
|
+
)
|
|
78
|
+
.alias("iv")
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
trades = opt_data
|
|
82
|
+
trades = trades.filter(pl.col("iv") > 40).filter(
|
|
83
|
+
pl.col("close").sub(30).abs().rank("dense").eq(1).over("date_only")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
trades = trades.with_columns(pl.lit("SELL").alias("transaction_type"))
|
|
87
|
+
print(trades)
|
|
88
|
+
return trades
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
self = IVSpike()
|
|
93
|
+
self.run_backtest()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
|
|
3
|
+
from quantplay.core.strategy import Strategy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OvernightOptionBuy(Strategy):
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
super().__init__(name="OBuy")
|
|
9
|
+
self.type = "overnight"
|
|
10
|
+
self.exit_time = "09:30"
|
|
11
|
+
self.index = "NIFTY BANK"
|
|
12
|
+
|
|
13
|
+
def generate_signals(self, data: dict[str, pl.DataFrame]) -> pl.DataFrame:
|
|
14
|
+
"""
|
|
15
|
+
Generate signals based on below logic
|
|
16
|
+
"""
|
|
17
|
+
index_data = data["index_data"]
|
|
18
|
+
opt_data = data["opt_data"]
|
|
19
|
+
|
|
20
|
+
index_data = index_data.filter(
|
|
21
|
+
pl.col("date").dt.time() >= pl.time(hour=9, minute=30)
|
|
22
|
+
)
|
|
23
|
+
index_data = index_data.with_columns(pl.col("date").dt.date().alias("date_only"))
|
|
24
|
+
index_data = index_data.with_columns(
|
|
25
|
+
pl.col("close").cum_max().over("date_only").alias("intraday_high")
|
|
26
|
+
)
|
|
27
|
+
index_data = index_data.filter(
|
|
28
|
+
(pl.col("date").dt.time() >= pl.time(hour=13, minute=30))
|
|
29
|
+
& (pl.col("close") >= pl.col("intraday_high"))
|
|
30
|
+
)
|
|
31
|
+
trades = index_data.group_by("date_only").agg(pl.all().first())
|
|
32
|
+
trades = trades.with_columns(pl.col("close").alias("underlying_price"))
|
|
33
|
+
|
|
34
|
+
trades = trades[["date", "date_only", "underlying_price"]].join(
|
|
35
|
+
opt_data,
|
|
36
|
+
on=["date"],
|
|
37
|
+
how="left",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
trades = self.add_days_to_expiry(trades)
|
|
41
|
+
|
|
42
|
+
trades = trades.filter(
|
|
43
|
+
(pl.col("days_to_expiry") < 7) & pl.col("days_to_expiry").gt(3)
|
|
44
|
+
).filter(pl.col("close").sub(700).abs().rank("dense").eq(1).over("date_only"))
|
|
45
|
+
|
|
46
|
+
return trades
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
self = OvernightOptionBuy()
|
|
51
|
+
self.run_backtest()
|
|
@@ -39,8 +39,8 @@ class Redis:
|
|
|
39
39
|
|
|
40
40
|
Redis.__instance = self
|
|
41
41
|
|
|
42
|
-
def get(self, key:
|
|
42
|
+
def get(self, key: int) -> None:
|
|
43
43
|
if self.error:
|
|
44
44
|
return None
|
|
45
45
|
|
|
46
|
-
return self.redis_client.get(
|
|
46
|
+
return self.redis_client.get(f"mkt:{key}") # type: ignore
|
|
@@ -43,14 +43,21 @@ quantplay/broker/xts_utils/Connect.py
|
|
|
43
43
|
quantplay/broker/xts_utils/Exception.py
|
|
44
44
|
quantplay/broker/xts_utils/InteractiveSocketClient.py
|
|
45
45
|
quantplay/broker/xts_utils/__init__.py
|
|
46
|
+
quantplay/core/__init__.py
|
|
47
|
+
quantplay/core/strategy.py
|
|
46
48
|
quantplay/exception/__init__.py
|
|
47
49
|
quantplay/exception/exceptions.py
|
|
50
|
+
quantplay/indicator/__init__.py
|
|
51
|
+
quantplay/indicator/iv.py
|
|
48
52
|
quantplay/model/__init__.py
|
|
49
53
|
quantplay/model/broker.py
|
|
50
54
|
quantplay/model/broker_response.py
|
|
51
55
|
quantplay/model/generics.py
|
|
52
56
|
quantplay/model/instrument_data.py
|
|
53
57
|
quantplay/model/order_event.py
|
|
58
|
+
quantplay/strategy/__init__.py
|
|
59
|
+
quantplay/strategy/iv_spike.py
|
|
60
|
+
quantplay/strategy/obuy.py
|
|
54
61
|
quantplay/utils/__init__.py
|
|
55
62
|
quantplay/utils/caching.py
|
|
56
63
|
quantplay/utils/constant.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quantplay-2.0.178 → quantplay-2.0.181}/quantplay/broker/xts_utils/InteractiveSocketClient.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|