├── .gitignore
├── config
├── system.json
├── server.json
├── brokerapp.json
└── holidays.json
├── src
├── models
│ ├── Direction.py
│ ├── ProductType.py
│ ├── Segment.py
│ ├── OrderType.py
│ ├── OrderStatus.py
│ ├── BrokerAppDetails.py
│ ├── TickData.py
│ └── Quote.py
├── trademgmt
│ ├── TradeEncoder.py
│ ├── TradeExitReason.py
│ ├── TradeState.py
│ ├── Trade.py
│ └── TradeManager.py
├── restapis
│ ├── BrokerLoginAPI.py
│ ├── HoldingsAPI.py
│ ├── PositionsAPI.py
│ ├── HomeAPI.py
│ └── StartAlgoAPI.py
├── ordermgmt
│ ├── OrderModifyParams.py
│ ├── BaseOrderManager.py
│ ├── OrderInputParams.py
│ ├── Order.py
│ └── ZerodhaOrderManager.py
├── loginmgmt
│ ├── BaseLogin.py
│ └── ZerodhaLogin.py
├── templates
│ ├── index.html
│ ├── index_algostarted.html
│ └── index_loggedin.html
├── core
│ ├── Controller.py
│ ├── Algo.py
│ └── Quotes.py
├── config
│ └── Config.py
├── ticker
│ ├── BaseTicker.py
│ └── ZerodhaTicker.py
├── main.py
├── Test.py
├── instruments
│ └── Instruments.py
├── strategies
│ ├── BNFORB30Min.py
│ ├── BaseStrategy.py
│ ├── SampleStrategy.py
│ ├── OptionSelling.py
│ └── ShortStraddleBNF.py
└── utils
│ └── Utils.py
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
--------------------------------------------------------------------------------
/config/system.json:
--------------------------------------------------------------------------------
1 | {
2 | "homeUrl": "http://localhost:8080"
3 | }
--------------------------------------------------------------------------------
/src/models/Direction.py:
--------------------------------------------------------------------------------
1 |
2 | class Direction:
3 | LONG = "LONG"
4 | SHORT = "SHORT"
5 |
--------------------------------------------------------------------------------
/src/models/ProductType.py:
--------------------------------------------------------------------------------
1 |
2 | class ProductType:
3 | MIS = "MIS"
4 | NRML = "NRML"
5 | CNC = "CNC"
--------------------------------------------------------------------------------
/src/models/Segment.py:
--------------------------------------------------------------------------------
1 |
2 | class Segment:
3 | EQUITY = "EQUITY"
4 | FNO = "FNO"
5 | CURRENCY = "CURRENCY"
6 | COMMADITY = "COMMADITY"
7 |
--------------------------------------------------------------------------------
/src/models/OrderType.py:
--------------------------------------------------------------------------------
1 |
2 | class OrderType:
3 | LIMIT = "LIMIT"
4 | MARKET = "MARKET"
5 | SL_MARKET = "SL_MARKET"
6 | SL_LIMIT = "SL_LIMIT"
7 |
--------------------------------------------------------------------------------
/src/trademgmt/TradeEncoder.py:
--------------------------------------------------------------------------------
1 |
2 | from json import JSONEncoder
3 |
4 | class TradeEncoder(JSONEncoder):
5 | def default(self, o):
6 | return o.__dict__
--------------------------------------------------------------------------------
/config/server.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": 8080,
3 | "enableSSL": false,
4 | "sslPort": 8443,
5 | "deployDir": "D:/temp/python-deploy",
6 | "logFileDir": "D:/temp/python-deploy/logs"
7 | }
--------------------------------------------------------------------------------
/config/brokerapp.json:
--------------------------------------------------------------------------------
1 | {
2 | "broker": "zerodha",
3 | "clientID": "dummy",
4 | "appKey": "dummy",
5 | "appSecret": "dummy",
6 | "redirectUrl": "http://localhost:8080/apis/broker/login/zerodha"
7 | }
--------------------------------------------------------------------------------
/src/trademgmt/TradeExitReason.py:
--------------------------------------------------------------------------------
1 |
2 | class TradeExitReason:
3 | SL_HIT = "SL HIT"
4 | TRAIL_SL_HIT = "TRAIL SL HIT"
5 | TARGET_HIT = "TARGET HIT"
6 | SQUARE_OFF = "SQUARE OFF"
7 | SL_CANCELLED = "SL CANCELLED"
8 | TARGET_CANCELLED = "TARGET CANCELLED"
9 |
--------------------------------------------------------------------------------
/config/holidays.json:
--------------------------------------------------------------------------------
1 | [
2 | "2021-01-26",
3 | "2021-03-11",
4 | "2021-03-29",
5 | "2021-04-02",
6 | "2021-04-14",
7 | "2021-04-21",
8 | "2021-05-13",
9 | "2021-07-21",
10 | "2021-08-19",
11 | "2021-09-10",
12 | "2021-10-15",
13 | "2021-11-04",
14 | "2021-11-05",
15 | "2021-11-19"
16 | ]
--------------------------------------------------------------------------------
/src/models/OrderStatus.py:
--------------------------------------------------------------------------------
1 |
2 | class OrderStatus:
3 | OPEN = "OPEN"
4 | COMPLETE = "COMPLETE"
5 | OPEN_PENDING = "OPEN PENDING"
6 | VALIDATION_PENDING = "VALIDATION PENDING"
7 | PUT_ORDER_REQ_RECEIVED = "PUT ORDER REQ RECEIVED"
8 | TRIGGER_PENDING = "TRIGGER PENDING"
9 | REJECTED = "REJECTED"
10 | CANCELLED = "CANCELLED"
11 |
--------------------------------------------------------------------------------
/src/restapis/BrokerLoginAPI.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from flask.views import MethodView
3 | from flask import request, redirect
4 |
5 | from core.Controller import Controller
6 |
7 | class BrokerLoginAPI(MethodView):
8 | def get(self):
9 | redirectUrl = Controller.handleBrokerLogin(request.args)
10 | return redirect(redirectUrl, code=302)
--------------------------------------------------------------------------------
/src/restapis/HoldingsAPI.py:
--------------------------------------------------------------------------------
1 | from flask.views import MethodView
2 | import json
3 | import logging
4 | from core.Controller import Controller
5 |
6 | class HoldingsAPI(MethodView):
7 | def get(self):
8 | brokerHandle = Controller.getBrokerLogin().getBrokerHandle()
9 | holdings = brokerHandle.holdings()
10 | logging.info('User holdings => %s', holdings)
11 | return json.dumps(holdings)
12 |
--------------------------------------------------------------------------------
/src/restapis/PositionsAPI.py:
--------------------------------------------------------------------------------
1 | from flask.views import MethodView
2 | import json
3 | import logging
4 | from core.Controller import Controller
5 |
6 | class PositionsAPI(MethodView):
7 | def get(self):
8 | brokerHandle = Controller.getBrokerLogin().getBrokerHandle()
9 | positions = brokerHandle.positions()
10 | logging.info('User positions => %s', positions)
11 | return json.dumps(positions)
12 |
--------------------------------------------------------------------------------
/src/models/BrokerAppDetails.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class BrokerAppDetails:
4 | def __init__(self, broker):
5 | self.broker = broker
6 | self.appKey = None
7 | self.appSecret = None
8 |
9 | def setClientID(self, clientID):
10 | self.clientID = clientID
11 |
12 | def setAppKey(self, appKey):
13 | self.appKey = appKey
14 |
15 | def setAppSecret(self, appSecret):
16 | self.appSecret = appSecret
17 |
18 |
--------------------------------------------------------------------------------
/src/models/TickData.py:
--------------------------------------------------------------------------------
1 |
2 | class TickData:
3 | def __init__(self, tradingSymbol):
4 | self.tradingSymbol = tradingSymbol
5 | self.lastTradedPrice = 0
6 | self.lastTradedQuantity = 0
7 | self.avgTradedPrice = 0
8 | self.volume = 0
9 | self.totalBuyQuantity = 0
10 | self.totalSellQuantity = 0
11 | self.open = 0
12 | self.high = 0
13 | self.low = 0
14 | self.close = 0
15 | self.change = 0
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sdoosa-algo-trade-python
2 |
3 | This project is mainly for newbies into algo trading who are interested in learning to code their own trading algo using python interpreter.
4 |
5 | This is in the context of Indian stock exchanges and brokers.
6 |
7 | Here is the youtube playlist with 10+ videos explaining the whole project with live trading demo.
8 |
9 | https://www.youtube.com/playlist?list=PL-yBsBIJqqBn6oMMgLjvhCTNT-zLXYmoW
10 |
--------------------------------------------------------------------------------
/src/trademgmt/TradeState.py:
--------------------------------------------------------------------------------
1 |
2 | class TradeState:
3 | CREATED = 'created' # Trade created but not yet order placed, might have not triggered
4 | ACTIVE = 'active' # order placed and trade is active
5 | COMPLETED = 'completed' # completed when exits due to SL/Target/SquareOff
6 | CANCELLED = 'cancelled' # cancelled/rejected comes under this state only
7 | DISABLED = 'disabled' # disable trade if not triggered within the time limits or for any other reason
--------------------------------------------------------------------------------
/src/restapis/HomeAPI.py:
--------------------------------------------------------------------------------
1 | from flask.views import MethodView
2 | from flask import render_template, request
3 |
4 | class HomeAPI(MethodView):
5 | def get(self):
6 | if 'loggedIn' in request.args and request.args['loggedIn'] == 'true':
7 | return render_template('index_loggedin.html')
8 | elif 'algoStarted' in request.args and request.args['algoStarted'] == 'true':
9 | return render_template('index_algostarted.html')
10 | else:
11 | return render_template('index.html')
12 |
--------------------------------------------------------------------------------
/src/models/Quote.py:
--------------------------------------------------------------------------------
1 |
2 | class Quote:
3 | def __init__(self, tradingSymbol):
4 | self.tradingSymbol = tradingSymbol
5 | self.lastTradedPrice = 0
6 | self.lastTradedQuantity = 0
7 | self.avgTradedPrice = 0
8 | self.volume = 0
9 | self.totalBuyQuantity = 0
10 | self.totalSellQuantity = 0
11 | self.open = 0
12 | self.high = 0
13 | self.low = 0
14 | self.close = 0
15 | self.change = 0
16 | self.oiDayHigh = 0
17 | self.oiDayLow = 0
18 | self.lowerCiruitLimit = 0
19 | self.upperCircuitLimit = 0
--------------------------------------------------------------------------------
/src/ordermgmt/OrderModifyParams.py:
--------------------------------------------------------------------------------
1 |
2 | class OrderModifyParams:
3 | def __init__(self):
4 | self.newPrice = 0
5 | self.newTriggerPrice = 0 # Applicable in case of SL order
6 | self.newQty = 0
7 | self.newOrderType = None # Ex: Can change LIMIT order to SL order or vice versa. Not supported by all brokers
8 |
9 | def __str__(self):
10 | return "newPrice=" + str(self.newPrice) + ", newTriggerPrice=" + str(self.newTriggerPrice) \
11 | + ", newQty=" + str(self.newQty) + ", newOrderType=" + str(self.newOrderType)
12 |
--------------------------------------------------------------------------------
/src/restapis/StartAlgoAPI.py:
--------------------------------------------------------------------------------
1 | from flask.views import MethodView
2 | import json
3 | import logging
4 | import threading
5 | from config.Config import getSystemConfig
6 | from core.Algo import Algo
7 |
8 | class StartAlgoAPI(MethodView):
9 | def post(self):
10 | # start algo in a separate thread
11 | x = threading.Thread(target=Algo.startAlgo)
12 | x.start()
13 | systemConfig = getSystemConfig()
14 | homeUrl = systemConfig['homeUrl'] + '?algoStarted=true'
15 | logging.info('Sending redirect url %s in response', homeUrl)
16 | respData = { 'redirect': homeUrl }
17 | return json.dumps(respData)
18 |
--------------------------------------------------------------------------------
/src/loginmgmt/BaseLogin.py:
--------------------------------------------------------------------------------
1 |
2 | class BaseLogin:
3 |
4 | def __init__(self, brokerAppDetails):
5 | self.brokerAppDetails = brokerAppDetails
6 | self.broker = brokerAppDetails.broker
7 |
8 | # Derived class should implement login function and return redirect url
9 | def login(self, args):
10 | pass
11 |
12 | def setBrokerHandle(self, brokerHandle):
13 | self.brokerHandle = brokerHandle
14 |
15 | def setAccessToken(self, accessToken):
16 | self.accessToken = accessToken
17 |
18 | def getBrokerAppDetails(self):
19 | return self.brokerAppDetails
20 |
21 | def getAccessToken(self):
22 | return self.accessToken
23 |
24 | def getBrokerHandle(self):
25 | return self.brokerHandle
--------------------------------------------------------------------------------
/src/ordermgmt/BaseOrderManager.py:
--------------------------------------------------------------------------------
1 |
2 | from core.Controller import Controller
3 |
4 | class BaseOrderManager:
5 | def __init__(self, broker):
6 | self.broker = broker
7 | self.brokerHandle = Controller.getBrokerLogin().getBrokerHandle()
8 |
9 | def placeOrder(self, orderInputParams):
10 | pass
11 |
12 | def modifyOrder(self, order, orderModifyParams):
13 | pass
14 |
15 | def modifyOrderToMarket(self, order):
16 | pass
17 |
18 | def cancelOrder(self, order):
19 | pass
20 |
21 | def fetchAndUpdateAllOrderDetails(self, orders):
22 | pass
23 |
24 | def convertToBrokerProductType(self, productType):
25 | return productType
26 |
27 | def convertToBrokerOrderType(self, orderType):
28 | return orderType
29 |
30 | def convertToBrokerDirection(self, direction):
31 | return direction
32 |
--------------------------------------------------------------------------------
/src/ordermgmt/OrderInputParams.py:
--------------------------------------------------------------------------------
1 |
2 | from models.Segment import Segment
3 | from models.ProductType import ProductType
4 |
5 | class OrderInputParams:
6 | def __init__(self, tradingSymbol):
7 | self.exchange = "NSE" # default
8 | self.isFnO = False
9 | self.segment = Segment.EQUITY # default
10 | self.productType = ProductType.MIS # default
11 | self.tradingSymbol = tradingSymbol
12 | self.direction = ""
13 | self.orderType = "" # One of the values of ordermgmt.OrderType
14 | self.qty = 0
15 | self.price = 0
16 | self.triggerPrice = 0 # Applicable in case of SL order
17 |
18 | def __str__(self):
19 | return "symbol=" + str(self.tradingSymbol) + ", exchange=" + self.exchange \
20 | + ", productType=" + self.productType + ", segment=" + self.segment \
21 | + ", direction=" + self.direction + ", orderType=" + self.orderType \
22 | + ", qty=" + str(self.qty) + ", price=" + str(self.price) + ", triggerPrice=" + str(self.triggerPrice) \
23 | + ", isFnO=" + str(self.isFnO)
24 |
--------------------------------------------------------------------------------
/src/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Algo Trading - Python
9 |
25 |
30 |
31 |
32 |
33 |
34 | Welcome to algo trading in python
35 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/core/Controller.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from config.Config import getBrokerAppConfig
4 | from models.BrokerAppDetails import BrokerAppDetails
5 | from loginmgmt.ZerodhaLogin import ZerodhaLogin
6 |
7 | class Controller:
8 | brokerLogin = None # static variable
9 | brokerName = None # static variable
10 |
11 | def handleBrokerLogin(args):
12 | brokerAppConfig = getBrokerAppConfig()
13 |
14 | brokerAppDetails = BrokerAppDetails(brokerAppConfig['broker'])
15 | brokerAppDetails.setClientID(brokerAppConfig['clientID'])
16 | brokerAppDetails.setAppKey(brokerAppConfig['appKey'])
17 | brokerAppDetails.setAppSecret(brokerAppConfig['appSecret'])
18 |
19 | logging.info('handleBrokerLogin appKey %s', brokerAppDetails.appKey)
20 | Controller.brokerName = brokerAppDetails.broker
21 | if Controller.brokerName == 'zerodha':
22 | Controller.brokerLogin = ZerodhaLogin(brokerAppDetails)
23 | # Other brokers - not implemented
24 | #elif Controller.brokerName == 'fyers':
25 | #Controller.brokerLogin = FyersLogin(brokerAppDetails)
26 |
27 | redirectUrl = Controller.brokerLogin.login(args)
28 | return redirectUrl
29 |
30 | def getBrokerLogin():
31 | return Controller.brokerLogin
32 |
33 | def getBrokerName():
34 | return Controller.brokerName
35 |
--------------------------------------------------------------------------------
/src/config/Config.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | def getServerConfig():
5 | with open('../config/server.json', 'r') as server:
6 | jsonServerData = json.load(server)
7 | return jsonServerData
8 |
9 | def getSystemConfig():
10 | with open('../config/system.json', 'r') as system:
11 | jsonSystemData = json.load(system)
12 | return jsonSystemData
13 |
14 | def getBrokerAppConfig():
15 | with open('../config/brokerapp.json', 'r') as brokerapp:
16 | jsonUserData = json.load(brokerapp)
17 | return jsonUserData
18 |
19 | def getHolidays():
20 | with open('../config/holidays.json', 'r') as holidays:
21 | holidaysData = json.load(holidays)
22 | return holidaysData
23 |
24 | def getTimestampsData():
25 | serverConfig = getServerConfig()
26 | timestampsFilePath = os.path.join(serverConfig['deployDir'], 'timestamps.json')
27 | if os.path.exists(timestampsFilePath) == False:
28 | return {}
29 | timestampsFile = open(timestampsFilePath, 'r')
30 | timestamps = json.loads(timestampsFile.read())
31 | return timestamps
32 |
33 | def saveTimestampsData(timestamps = {}):
34 | serverConfig = getServerConfig()
35 | timestampsFilePath = os.path.join(serverConfig['deployDir'], 'timestamps.json')
36 | with open(timestampsFilePath, 'w') as timestampsFile:
37 | json.dump(timestamps, timestampsFile, indent=2)
38 | print("saved timestamps data to file " + timestampsFilePath)
39 |
--------------------------------------------------------------------------------
/src/core/Algo.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import threading
3 | import time
4 |
5 | from instruments.Instruments import Instruments
6 | from trademgmt.TradeManager import TradeManager
7 |
8 | from strategies.SampleStrategy import SampleStrategy
9 | from strategies.BNFORB30Min import BNFORB30Min
10 | from strategies.OptionSelling import OptionSelling
11 | from strategies.ShortStraddleBNF import ShortStraddleBNF
12 |
13 | #from Test import Test
14 |
15 | class Algo:
16 | isAlgoRunning = None
17 |
18 | @staticmethod
19 | def startAlgo():
20 | if Algo.isAlgoRunning == True:
21 | logging.info("Algo has already started..")
22 | return
23 |
24 | logging.info("Starting Algo...")
25 | Instruments.fetchInstruments()
26 |
27 | # start trade manager in a separate thread
28 | tm = threading.Thread(target=TradeManager.run)
29 | tm.start()
30 |
31 | # sleep for 2 seconds for TradeManager to get initialized
32 | time.sleep(2)
33 |
34 | # start running strategies: Run each strategy in a separate thread
35 | #threading.Thread(target=SampleStrategy.getInstance().run).start()
36 | #threading.Thread(target=BNFORB30Min.getInstance().run).start()
37 | #threading.Thread(target=OptionSelling.getInstance().run).start()
38 | threading.Thread(target=ShortStraddleBNF.getInstance().run).start()
39 |
40 | Algo.isAlgoRunning = True
41 | logging.info("Algo started.")
42 |
--------------------------------------------------------------------------------
/src/templates/index_algostarted.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Algo Trading - Python
9 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
43 | Welcome to algo trading in python
44 |
45 |
48 |
Algo is running...
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/templates/index_loggedin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Algo Trading - Python
9 |
25 |
26 |
27 |
43 |
44 |
45 |
46 |
47 | Welcome to algo trading in python
48 |
49 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/loginmgmt/ZerodhaLogin.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from kiteconnect import KiteConnect
3 |
4 | from config.Config import getSystemConfig
5 | from loginmgmt.BaseLogin import BaseLogin
6 |
7 | class ZerodhaLogin(BaseLogin):
8 | def __init__(self, brokerAppDetails):
9 | BaseLogin.__init__(self, brokerAppDetails)
10 |
11 | def login(self, args):
12 | logging.info('==> ZerodhaLogin .args => %s', args);
13 | systemConfig = getSystemConfig()
14 | brokerHandle = KiteConnect(api_key=self.brokerAppDetails.appKey)
15 | redirectUrl = None
16 | if 'request_token' in args:
17 | requestToken = args['request_token']
18 | logging.info('Zerodha requestToken = %s', requestToken)
19 | session = brokerHandle.generate_session(requestToken, api_secret=self.brokerAppDetails.appSecret)
20 |
21 | accessToken = session['access_token']
22 | accessToken = accessToken
23 | logging.info('Zerodha accessToken = %s', accessToken)
24 | brokerHandle.set_access_token(accessToken)
25 |
26 | logging.info('Zerodha Login successful. accessToken = %s', accessToken)
27 |
28 | # set broker handle and access token to the instance
29 | self.setBrokerHandle(brokerHandle)
30 | self.setAccessToken(accessToken)
31 |
32 | # redirect to home page with query param loggedIn=true
33 | homeUrl = systemConfig['homeUrl'] + '?loggedIn=true'
34 | logging.info('Zerodha Redirecting to home page %s', homeUrl)
35 | redirectUrl = homeUrl
36 | else:
37 | loginUrl = brokerHandle.login_url()
38 | logging.info('Redirecting to zerodha login url = %s', loginUrl)
39 | redirectUrl = loginUrl
40 |
41 | return redirectUrl
42 |
43 |
--------------------------------------------------------------------------------
/src/ticker/BaseTicker.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from core.Controller import Controller
4 |
5 | class BaseTicker:
6 | def __init__(self, broker):
7 | self.broker = broker
8 | self.brokerLogin = Controller.getBrokerLogin()
9 | self.ticker = None
10 | self.tickListeners = []
11 |
12 | def startTicker(self):
13 | pass
14 |
15 | def stopTicker(self):
16 | pass
17 |
18 | def registerListener(self, listener):
19 | # All registered tick listeners will be notified on new ticks
20 | self.tickListeners.append(listener)
21 |
22 | def registerSymbols(self, symbols):
23 | pass
24 |
25 | def unregisterSymbols(self, symbols):
26 | pass
27 |
28 | def onNewTicks(self, ticks):
29 | # logging.info('New ticks received %s', ticks)
30 | for tick in ticks:
31 | for listener in self.tickListeners:
32 | try:
33 | listener(tick)
34 | except Exception as e:
35 | logging.error('BaseTicker: Exception from listener callback function. Error => %s', str(e))
36 |
37 | def onConnect(self):
38 | logging.info('Ticker connection successful.')
39 |
40 | def onDisconnect(self, code, reason):
41 | logging.error('Ticker got disconnected. code = %d, reason = %s', code, reason)
42 |
43 | def onError(self, code, reason):
44 | logging.error('Ticker errored out. code = %d, reason = %s', code, reason)
45 |
46 | def onReconnect(self, attemptsCount):
47 | logging.warn('Ticker reconnecting.. attemptsCount = %d', attemptsCount)
48 |
49 | def onMaxReconnectsAttempt(self):
50 | logging.error('Ticker max auto reconnects attempted and giving up..')
51 |
52 | def onOrderUpdate(self, data):
53 | #logging.info('Ticker: order update %s', data)
54 | pass
55 |
--------------------------------------------------------------------------------
/src/core/Quotes.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from core.Controller import Controller
4 | from models.Quote import Quote
5 |
6 | class Quotes:
7 | @staticmethod
8 | def getQuote(tradingSymbol, isFnO = False):
9 | broker = Controller.getBrokerName()
10 | brokerHandle = Controller.getBrokerLogin().getBrokerHandle()
11 | quote = None
12 | if broker == "zerodha":
13 | key = ('NFO:' + tradingSymbol) if isFnO == True else ('NSE:' + tradingSymbol)
14 | bQuoteResp = brokerHandle.quote(key)
15 | bQuote = bQuoteResp[key]
16 | # convert broker quote to our system quote
17 | quote = Quote(tradingSymbol)
18 | quote.tradingSymbol = tradingSymbol
19 | quote.lastTradedPrice = bQuote['last_price']
20 | quote.lastTradedQuantity = bQuote['last_quantity']
21 | quote.avgTradedPrice = bQuote['average_price']
22 | quote.volume = bQuote['volume']
23 | quote.totalBuyQuantity = bQuote['buy_quantity']
24 | quote.totalSellQuantity = bQuote['sell_quantity']
25 | ohlc = bQuote['ohlc']
26 | quote.open = ohlc['open']
27 | quote.high = ohlc['high']
28 | quote.low = ohlc['low']
29 | quote.close = ohlc['close']
30 | quote.change = bQuote['net_change']
31 | quote.oiDayHigh = bQuote['oi_day_high']
32 | quote.oiDayLow = bQuote['oi_day_low']
33 | quote.lowerCiruitLimit = bQuote['lower_circuit_limit']
34 | quote.upperCircuitLimit = bQuote['upper_circuit_limit']
35 | else:
36 | # The logic may be different for other brokers
37 | quote = None
38 | return quote
39 |
40 | @staticmethod
41 | def getCMP(tradingSymbol):
42 | quote = Quotes.getQuote(tradingSymbol)
43 | if quote:
44 | return quote.lastTradedPrice
45 | else:
46 | return 0
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | from flask import Flask
4 |
5 | from config.Config import getBrokerAppConfig, getServerConfig, getSystemConfig
6 | from restapis.HomeAPI import HomeAPI
7 | from restapis.BrokerLoginAPI import BrokerLoginAPI
8 | from restapis.StartAlgoAPI import StartAlgoAPI
9 | from restapis.PositionsAPI import PositionsAPI
10 | from restapis.HoldingsAPI import HoldingsAPI
11 |
12 | app = Flask(__name__)
13 | app.config['DEBUG'] = True
14 |
15 | app.add_url_rule("/", view_func=HomeAPI.as_view("home_api"))
16 | app.add_url_rule("/apis/broker/login/zerodha", view_func=BrokerLoginAPI.as_view("broker_login_api"))
17 | app.add_url_rule("/apis/algo/start", view_func=StartAlgoAPI.as_view("start_algo_api"))
18 | app.add_url_rule("/positions", view_func=PositionsAPI.as_view("positions_api"))
19 | app.add_url_rule("/holdings", view_func=HoldingsAPI.as_view("holdings_api"))
20 |
21 | def initLoggingConfg(filepath):
22 | format = "%(asctime)s: %(message)s"
23 | logging.basicConfig(filename=filepath, format=format, level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S")
24 |
25 | # Execution starts here
26 | serverConfig = getServerConfig()
27 |
28 | deployDir = serverConfig['deployDir']
29 | if os.path.exists(deployDir) == False:
30 | print("Deploy Directory " + deployDir + " does not exist. Exiting the app.")
31 | exit(-1)
32 |
33 | logFileDir = serverConfig['logFileDir']
34 | if os.path.exists(logFileDir) == False:
35 | print("LogFile Directory " + logFileDir + " does not exist. Exiting the app.")
36 | exit(-1)
37 |
38 | print("Deploy Directory = " + deployDir)
39 | print("LogFile Directory = " + logFileDir)
40 | initLoggingConfg(logFileDir + "/app.log")
41 |
42 | logging.info('serverConfig => %s', serverConfig)
43 |
44 | brokerAppConfig = getBrokerAppConfig()
45 | logging.info('brokerAppConfig => %s', brokerAppConfig)
46 |
47 | port = serverConfig['port']
48 |
49 | app.run('localhost', port)
--------------------------------------------------------------------------------
/src/ordermgmt/Order.py:
--------------------------------------------------------------------------------
1 |
2 | class Order:
3 | def __init__(self, orderInputParams = None):
4 | self.tradingSymbol = orderInputParams.tradingSymbol if orderInputParams != None else ""
5 | self.exchange = orderInputParams.exchange if orderInputParams != None else "NSE"
6 | self.productType = orderInputParams.productType if orderInputParams != None else ""
7 | self.orderType = orderInputParams.orderType if orderInputParams != None else "" # LIMIT/MARKET/SL-LIMIT/SL-MARKET
8 | self.price = orderInputParams.price if orderInputParams != None else 0
9 | self.triggerPrice = orderInputParams.triggerPrice if orderInputParams != None else 0 # Applicable in case of SL orders
10 | self.qty = orderInputParams.qty if orderInputParams != None else 0
11 | self.orderId = None # The order id received from broker after placing the order
12 | self.orderStatus = None # One of the status defined in ordermgmt.OrderStatus
13 | self.averagePrice = 0 # Average price at which the order is filled
14 | self.filledQty = 0 # Filled quantity
15 | self.pendingQty = 0 # Qty - Filled quantity
16 | self.orderPlaceTimestamp = None # Timestamp when the order is placed
17 | self.lastOrderUpdateTimestamp = None # Applicable if you modify the order Ex: Trailing SL
18 | self.message = None # In case any order rejection or any other error save the response from broker in this field
19 |
20 | def __str__(self):
21 | return "orderId=" + str(self.orderId) + ", orderStatus=" + str(self.orderStatus) \
22 | + ", symbol=" + str(self.tradingSymbol) + ", productType=" + str(self.productType) \
23 | + ", orderType=" + str(self.orderType) + ", price=" + str(self.price) \
24 | + ", triggerPrice=" + str(self.triggerPrice) + ", qty=" + str(self.qty) \
25 | + ", filledQty=" + str(self.filledQty) + ", pendingQty=" + str(self.pendingQty) \
26 | + ", averagePrice=" + str(self.averagePrice)
27 |
--------------------------------------------------------------------------------
/src/Test.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 |
4 | from core.Controller import Controller
5 | from ticker.ZerodhaTicker import ZerodhaTicker
6 | from ordermgmt.ZerodhaOrderManager import ZerodhaOrderManager
7 | from ordermgmt.Order import Order
8 | from core.Quotes import Quotes
9 | from utils.Utils import Utils
10 |
11 | class Test:
12 | def testTicker():
13 | ticker = ZerodhaTicker()
14 | ticker.startTicker()
15 | ticker.registerListener(Test.tickerListener)
16 |
17 | # sleep for 5 seconds and register trading symbols to receive ticks
18 | time.sleep(5)
19 | ticker.registerSymbols(['SBIN', 'RELIANCE'])
20 |
21 | # wait for 60 seconds and stop ticker service
22 | time.sleep(60)
23 | logging.info('Going to stop ticker')
24 | ticker.stopTicker()
25 |
26 | def tickerListener(tick):
27 | logging.info('tickerLister: onNewTick %s', vars(tick));
28 |
29 | def testOrders():
30 | orderManager = ZerodhaOrderManager()
31 | exchange = 'NSE';
32 | tradingSymbol = 'SBIN'
33 | lastTradedPrice = Quotes.getCMP(exchange + ':' + tradingSymbol)
34 | logging.info(tradingSymbol + ' CMP = %f', lastTradedPrice)
35 |
36 | limitPrice = lastTradedPrice - lastTradedPrice * 1 / 100
37 | limitPrice = Utils.roundToNSEPrice(limitPrice)
38 | qty = 1
39 | direction = 'LONG'
40 |
41 | # place order
42 | origOrderId = orderManager.placeOrder(tradingSymbol, limitPrice, qty, direction)
43 | logging.info('Original order Id %s', origOrderId)
44 |
45 | # sleep for 10 seconds then modify order
46 | time.sleep(10)
47 | newPrice = lastTradedPrice
48 | if origOrderId:
49 | orderManager.modifyOrder(origOrderId, newPrice)
50 |
51 | # sleep for 10 seconds and then place SL order
52 | time.sleep(10)
53 | slPrice = newPrice - newPrice * 1 / 100
54 | slPrice = Utils.roundToNSEPrice(slPrice)
55 | slDirection = 'SHORT' if direction == 'LONG' else 'LONG'
56 | slOrderId = orderManager.placeSLOrder(tradingSymbol, slPrice, qty, slDirection)
57 | logging.info('SL order Id %s', slOrderId)
58 |
59 | # sleep for 10 seconds and then place target order
60 | time.sleep(10)
61 | targetPrice = newPrice + newPrice * 2 / 100
62 | targetPrice = Utils.roundToNSEPrice(targetPrice)
63 | targetDirection = 'SHORT' if direction == 'LONG' else 'LONG'
64 | targetOrderId = orderManager.placeOrder(tradingSymbol, targetPrice, qty, targetDirection)
65 | logging.info('Target order Id %s', targetOrderId)
66 |
67 | # sleep for 10 seconds and cancel target order
68 | time.sleep(10)
69 | if targetOrderId:
70 | orderManager.cancelOrder(targetOrderId)
71 | logging.info('Cancelled Target order Id %s', targetOrderId)
72 |
73 | logging.info("Algo done executing all orders. Check ur orders and positions in broker terminal.")
74 |
75 | def testMisc():
76 | orderManager = ZerodhaOrderManager()
77 | sampleOrder = Order(orderInputParams=None)
78 | sampleOrder.orderId='210505200078243'
79 | orders = []
80 | orders.append(sampleOrder)
81 | orderManager.fetchAndUpdateAllOrderDetails(orders)
--------------------------------------------------------------------------------
/src/trademgmt/Trade.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from trademgmt.TradeState import TradeState
4 | from models.ProductType import ProductType
5 |
6 | from utils.Utils import Utils
7 |
8 | class Trade:
9 | def __init__(self, tradingSymbol = None):
10 | self.exchange = "NSE"
11 | self.tradeID = Utils.generateTradeID() # Unique ID for each trade
12 | self.tradingSymbol = tradingSymbol
13 | self.strategy = ""
14 | self.direction = ""
15 | self.productType = ProductType.MIS
16 | self.isFutures = False # Futures trade
17 | self.isOptions = False # Options trade
18 | self.optionType = None # CE/PE. Applicable only if isOptions is True
19 | self.placeMarketOrder = False # True means place the entry order with Market Order Type
20 | self.intradaySquareOffTimestamp = None # Can be strategy specific. Some can square off at 15:00:00 some can at 15:15:00 etc.
21 | self.requestedEntry = 0 # Requested entry
22 | self.entry = 0 # Actual entry. This will be different from requestedEntry if the order placed is Market order
23 | self.qty = 0 # Requested quantity
24 | self.filledQty = 0 # In case partial fill qty is not equal to filled quantity
25 | self.initialStopLoss = 0 # Initial stop loss
26 | self.stopLoss = 0 # This is the current stop loss. In case of trailing SL the current stopLoss and initialStopLoss will be different after some time
27 | self.target = 0 # Target price if applicable
28 | self.cmp = 0 # Last traded price
29 |
30 | self.tradeState = TradeState.CREATED # state of the trade
31 | self.timestamp = None # Set this timestamp to strategy timestamp if you are not sure what to set
32 | self.createTimestamp = Utils.getEpoch() # Timestamp when the trade is created (Not triggered)
33 | self.startTimestamp = None # Timestamp when the trade gets triggered and order placed
34 | self.endTimestamp = None # Timestamp when the trade ended
35 | self.pnl = 0 # Profit loss of the trade. If trade is Active this shows the unrealized pnl else realized pnl
36 | self.pnlPercentage = 0 # Profit Loss in percentage terms
37 | self.exit = 0 # Exit price of the trade
38 | self.exitReason = None # SL/Target/SquareOff/Any Other
39 |
40 | self.entryOrder = None # Object of Type ordermgmt.Order
41 | self.slOrder = None # Object of Type ordermgmt.Order
42 | self.targetOrder = None # Object of Type ordermgmt.Order
43 |
44 | def equals(self, trade): # compares to trade objects and returns True if equals
45 | if trade == None:
46 | return False
47 | if self.tradeID == trade.tradeID:
48 | return True
49 | if self.tradingSymbol != trade.tradingSymbol:
50 | return False
51 | if self.strategy != trade.strategy:
52 | return False
53 | if self.direction != trade.direction:
54 | return False
55 | if self.productType != trade.productType:
56 | return False
57 | if self.requestedEntry != trade.requestedEntry:
58 | return False
59 | if self.qty != trade.qty:
60 | return False
61 | if self.timestamp != trade.timestamp:
62 | return False
63 | return True
64 |
65 | def __str__(self):
66 | return "ID=" + str(self.tradeID) + ", state=" + self.tradeState + ", symbol=" + self.tradingSymbol \
67 | + ", strategy=" + self.strategy + ", direction=" + self.direction \
68 | + ", productType=" + self.productType + ", reqEntry=" + str(self.requestedEntry) \
69 | + ", stopLoss=" + str(self.stopLoss) + ", target=" + str(self.target) \
70 | + ", entry=" + str(self.entry) + ", exit=" + str(self.exit) \
71 | + ", profitLoss" + str(self.pnl)
72 |
73 |
--------------------------------------------------------------------------------
/src/ticker/ZerodhaTicker.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json
3 |
4 | from kiteconnect import KiteTicker
5 |
6 | from ticker.BaseTicker import BaseTicker
7 | from instruments.Instruments import Instruments
8 | from models.TickData import TickData
9 |
10 | class ZerodhaTicker(BaseTicker):
11 | def __init__(self):
12 | super().__init__("zerodha")
13 |
14 | def startTicker(self):
15 | brokerAppDetails = self.brokerLogin.getBrokerAppDetails()
16 | accessToken = self.brokerLogin.getAccessToken()
17 | if accessToken == None:
18 | logging.error('ZerodhaTicker startTicker: Cannot start ticker as accessToken is empty')
19 | return
20 |
21 | ticker = KiteTicker(brokerAppDetails.appKey, accessToken)
22 | ticker.on_connect = self.on_connect
23 | ticker.on_close = self.on_close
24 | ticker.on_error = self.on_error
25 | ticker.on_reconnect = self.on_reconnect
26 | ticker.on_noreconnect = self.on_noreconnect
27 | ticker.on_ticks = self.on_ticks
28 | ticker.on_order_update = self.on_order_update
29 |
30 | logging.info('ZerodhaTicker: Going to connect..')
31 | self.ticker = ticker
32 | self.ticker.connect(threaded=True)
33 |
34 | def stopTicker(self):
35 | logging.info('ZerodhaTicker: stopping..')
36 | self.ticker.close(1000, "Manual close")
37 |
38 | def registerSymbols(self, symbols):
39 | tokens = []
40 | for symbol in symbols:
41 | isd = Instruments.getInstrumentDataBySymbol(symbol)
42 | token = isd['instrument_token']
43 | logging.info('ZerodhaTicker registerSymbol: %s token = %s', symbol, token)
44 | tokens.append(token)
45 |
46 | logging.info('ZerodhaTicker Subscribing tokens %s', tokens)
47 | self.ticker.subscribe(tokens)
48 |
49 | def unregisterSymbols(self, symbols):
50 | tokens = []
51 | for symbol in symbols:
52 | isd = Instruments.getInstrumentDataBySymbol(symbol)
53 | token = isd['instrument_token']
54 | logging.info('ZerodhaTicker unregisterSymbols: %s token = %s', symbol, token)
55 | tokens.append(token)
56 |
57 | logging.info('ZerodhaTicker Unsubscribing tokens %s', tokens)
58 | self.ticker.unsubscribe(tokens)
59 |
60 | def on_ticks(self, ws, brokerTicks):
61 | # convert broker specific Ticks to our system specific Ticks (models.TickData) and pass to super class function
62 | ticks = []
63 | for bTick in brokerTicks:
64 | isd = Instruments.getInstrumentDataByToken(bTick['instrument_token'])
65 | tradingSymbol = isd['tradingsymbol']
66 | tick = TickData(tradingSymbol)
67 | tick.lastTradedPrice = bTick['last_price']
68 | tick.lastTradedQuantity = bTick['last_quantity']
69 | tick.avgTradedPrice = bTick['average_price']
70 | tick.volume = bTick['volume']
71 | tick.totalBuyQuantity = bTick['buy_quantity']
72 | tick.totalSellQuantity = bTick['sell_quantity']
73 | tick.open = bTick['ohlc']['open']
74 | tick.high = bTick['ohlc']['high']
75 | tick.low = bTick['ohlc']['low']
76 | tick.close = bTick['ohlc']['close']
77 | tick.change = bTick['change']
78 | ticks.append(tick)
79 |
80 | self.onNewTicks(ticks)
81 |
82 | def on_connect(self, ws, response):
83 | self.onConnect()
84 |
85 | def on_close(self, ws, code, reason):
86 | self.onDisconnect(code, reason)
87 |
88 | def on_error(self, ws, code, reason):
89 | self.onError(code, reason)
90 |
91 | def on_reconnect(self, ws, attemptsCount):
92 | self.onReconnect(attemptsCount)
93 |
94 | def on_noreconnect(self, ws):
95 | self.onMaxReconnectsAttempt()
96 |
97 | def on_order_update(self, ws, data):
98 | self.onOrderUpdate(data)
99 |
--------------------------------------------------------------------------------
/src/instruments/Instruments.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import json
4 |
5 | from config.Config import getServerConfig, getTimestampsData, saveTimestampsData
6 | from core.Controller import Controller
7 | from utils.Utils import Utils
8 |
9 | class Instruments:
10 | instrumentsList = None
11 | symbolToInstrumentMap = None
12 | tokenToInstrumentMap = None
13 |
14 | @staticmethod
15 | def shouldFetchFromServer():
16 | timestamps = getTimestampsData()
17 | if 'instrumentsLastSavedAt' not in timestamps:
18 | return True
19 | lastSavedTimestamp = timestamps['instrumentsLastSavedAt']
20 | nowEpoch = Utils.getEpoch()
21 | if nowEpoch - lastSavedTimestamp >= 24 * 60* 60:
22 | logging.info("Instruments: shouldFetchFromServer() returning True as its been 24 hours since last fetch.")
23 | return True
24 | return False
25 |
26 | @staticmethod
27 | def updateLastSavedTimestamp():
28 | timestamps = getTimestampsData()
29 | timestamps['instrumentsLastSavedAt'] = Utils.getEpoch()
30 | saveTimestampsData(timestamps)
31 |
32 | @staticmethod
33 | def loadInstruments():
34 | serverConfig = getServerConfig()
35 | instrumentsFilepath = os.path.join(serverConfig['deployDir'], 'instruments.json')
36 | if os.path.exists(instrumentsFilepath) == False:
37 | logging.warn('Instruments: instrumentsFilepath %s does not exist', instrumentsFilepath)
38 | return [] # returns empty list
39 |
40 | isdFile = open(instrumentsFilepath, 'r')
41 | instruments = json.loads(isdFile.read())
42 | logging.info('Instruments: loaded %d instruments from file %s', len(instruments), instrumentsFilepath)
43 | return instruments
44 |
45 | @staticmethod
46 | def saveInstruments(instruments = []):
47 | serverConfig = getServerConfig()
48 | instrumentsFilepath = os.path.join(serverConfig['deployDir'], 'instruments.json')
49 | with open(instrumentsFilepath, 'w') as isdFile:
50 | json.dump(instruments, isdFile, indent=2, default=str)
51 | logging.info('Instruments: Saved %d instruments to file %s', len(instruments), instrumentsFilepath)
52 | # Update last save timestamp
53 | Instruments.updateLastSavedTimestamp()
54 |
55 | @staticmethod
56 | def fetchInstrumentsFromServer():
57 | instrumentsList = []
58 | try:
59 | brokerHandle = Controller.getBrokerLogin().getBrokerHandle()
60 | logging.info('Going to fetch instruments from server...')
61 | instrumentsList = brokerHandle.instruments('NSE')
62 | instrumentsListFnO = brokerHandle.instruments('NFO')
63 | # Add FnO instrument list to the main list
64 | instrumentsList.extend(instrumentsListFnO)
65 | logging.info('Fetched %d instruments from server.', len(instrumentsList))
66 | except Exception as e:
67 | logging.exception("Exception while fetching instruments from server")
68 | return instrumentsList
69 |
70 | @staticmethod
71 | def fetchInstruments():
72 | if Instruments.instrumentsList:
73 | return Instruments.instrumentsList
74 |
75 | instrumentsList = Instruments.loadInstruments()
76 | if len(instrumentsList) == 0 or Instruments.shouldFetchFromServer() == True:
77 | instrumentsList = Instruments.fetchInstrumentsFromServer()
78 | # Save instruments to file locally
79 | if len(instrumentsList) > 0:
80 | Instruments.saveInstruments(instrumentsList)
81 |
82 | if len(instrumentsList) == 0:
83 | print("Could not fetch/load instruments data. Hence exiting the app.")
84 | logging.error("Could not fetch/load instruments data. Hence exiting the app.");
85 | exit(-2)
86 |
87 | Instruments.symbolToInstrumentMap = {}
88 | Instruments.tokenToInstrumentMap = {}
89 | for isd in instrumentsList:
90 | tradingSymbol = isd['tradingsymbol']
91 | instrumentToken = isd['instrument_token']
92 | # logging.info('%s = %d', tradingSymbol, instrumentToken)
93 | Instruments.symbolToInstrumentMap[tradingSymbol] = isd
94 | Instruments.tokenToInstrumentMap[instrumentToken] = isd
95 |
96 | logging.info('Fetching instruments done. Instruments count = %d', len(instrumentsList))
97 | Instruments.instrumentsList = instrumentsList # assign the list to static variable
98 | return instrumentsList
99 |
100 | @staticmethod
101 | def getInstrumentDataBySymbol(tradingSymbol):
102 | return Instruments.symbolToInstrumentMap[tradingSymbol]
103 |
104 | @staticmethod
105 | def getInstrumentDataByToken(instrumentToken):
106 | return Instruments.tokenToInstrumentMap[instrumentToken]
107 |
--------------------------------------------------------------------------------
/src/strategies/BNFORB30Min.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 |
4 | from instruments.Instruments import Instruments
5 | from models.Direction import Direction
6 | from models.ProductType import ProductType
7 | from strategies.BaseStrategy import BaseStrategy
8 | from utils.Utils import Utils
9 | from trademgmt.Trade import Trade
10 | from trademgmt.TradeManager import TradeManager
11 |
12 | # Each strategy has to be derived from BaseStrategy
13 | class BNFORB30Min(BaseStrategy):
14 | __instance = None
15 |
16 | @staticmethod
17 | def getInstance(): # singleton class
18 | if BNFORB30Min.__instance == None:
19 | BNFORB30Min()
20 | return BNFORB30Min.__instance
21 |
22 | def __init__(self):
23 | if BNFORB30Min.__instance != None:
24 | raise Exception("This class is a singleton!")
25 | else:
26 | BNFORB30Min.__instance = self
27 | # Call Base class constructor
28 | super().__init__("BNFORB30Min")
29 | # Initialize all the properties specific to this strategy
30 | self.productType = ProductType.MIS
31 | self.symbols = []
32 | self.slPercentage = 0
33 | self.targetPercentage = 0
34 | self.startTimestamp = Utils.getTimeOfToDay(9, 45, 0) # When to start the strategy. Default is Market start time
35 | self.stopTimestamp = Utils.getTimeOfToDay(14, 30, 0) # This is not square off timestamp. This is the timestamp after which no new trades will be placed under this strategy but existing trades continue to be active.
36 | self.squareOffTimestamp = Utils.getTimeOfToDay(15, 0, 0) # Square off time
37 | self.capital = 100000 # Capital to trade (This is the margin you allocate from your broker account for this strategy)
38 | self.leverage = 0
39 | self.maxTradesPerDay = 1 # Max number of trades per day under this strategy
40 | self.isFnO = True # Does this strategy trade in FnO or not
41 | self.capitalPerSet = 100000 # Applicable if isFnO is True (1 set means 1CE/1PE or 2CE/2PE etc based on your strategy logic)
42 |
43 | def process(self):
44 | now = datetime.now()
45 | processEndTime = Utils.getTimeOfToDay(9, 50, 0)
46 | if now < self.startTimestamp:
47 | return
48 | if now > processEndTime:
49 | # We are interested in creating the symbol only between 09:45 and 09:50
50 | # since we are not using historical candles so not aware of exact high and low of the first 30 mins
51 | return
52 |
53 | if len(self.trades) >= 2:
54 | return
55 |
56 | symbol = Utils.prepareMonthlyExpiryFuturesSymbol('BANKNIFTY')
57 | quote = self.getQuote(symbol)
58 | if quote == None:
59 | logging.error('%s: Could not get quote for %s', self.getName(), symbol)
60 | return
61 |
62 | logging.info('%s: %s => lastTradedPrice = %f', self.getName(), symbol, quote.lastTradedPrice)
63 | self.generateTrade(symbol, Direction.LONG, quote.high, quote.low)
64 | self.generateTrade(symbol, Direction.SHORT, quote.high, quote.low)
65 |
66 |
67 | def generateTrade(self, tradingSymbol, direction, high, low):
68 | trade = Trade(tradingSymbol)
69 | trade.strategy = self.getName()
70 | trade.isFutures = True
71 | trade.direction = direction
72 | trade.productType = self.productType
73 | trade.placeMarketOrder = True
74 | trade.requestedEntry = high if direction == Direction.LONG else low
75 | trade.timestamp = Utils.getEpoch(self.startTimestamp) # setting this to strategy timestamp
76 | # Calculate lots
77 | numLots = self.calculateLotsPerTrade()
78 | isd = Instruments.getInstrumentDataBySymbol(tradingSymbol) # Get instrument data to know qty per lot
79 | trade.qty = isd['lot_size'] * numLots
80 |
81 | trade.stopLoss = low if direction == Direction.LONG else high
82 | slDiff = high - low
83 | # target is 1.5 times of SL
84 | if direction == 'LONG':
85 | trade.target = Utils.roundToNSEPrice(trade.requestedEntry + 1.5 * slDiff)
86 | else:
87 | trade.target = Utils.roundToNSEPrice(trade.requestedEntry - 1.5 * slDiff)
88 |
89 | trade.intradaySquareOffTimestamp = Utils.getEpoch(self.squareOffTimestamp)
90 | # Hand over the trade to TradeManager
91 | TradeManager.addNewTrade(trade)
92 |
93 | def shouldPlaceTrade(self, trade, tick):
94 | # First call base class implementation and if it returns True then only proceed
95 | if super().shouldPlaceTrade(trade, tick) == False:
96 | return False
97 |
98 | if tick == None:
99 | return False
100 |
101 | if trade.direction == Direction.LONG and tick.lastTradedPrice > trade.requestedEntry:
102 | return True
103 | elif trade.direction == Direction.SHORT and tick.lastTradedPrice < trade.requestedEntry:
104 | return True
105 | return False
106 |
--------------------------------------------------------------------------------
/src/strategies/BaseStrategy.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from datetime import datetime
4 |
5 | from models.ProductType import ProductType
6 | from core.Quotes import Quotes
7 | from trademgmt.TradeManager import TradeManager
8 |
9 | from utils.Utils import Utils
10 |
11 | class BaseStrategy:
12 | def __init__(self, name):
13 | # NOTE: All the below properties should be set by the Derived Class (Specific to each strategy)
14 | self.name = name # strategy name
15 | self.enabled = True # Strategy will be run only when it is enabled
16 | self.productType = ProductType.MIS # MIS/NRML/CNC etc
17 | self.symbols = [] # List of stocks to be traded under this strategy
18 | self.slPercentage = 0
19 | self.targetPercentage = 0
20 | self.startTimestamp = Utils.getMarketStartTime() # When to start the strategy. Default is Market start time
21 | self.stopTimestamp = None # This is not square off timestamp. This is the timestamp after which no new trades will be placed under this strategy but existing trades continue to be active.
22 | self.squareOffTimestamp = None # Square off time
23 | self.capital = 10000 # Capital to trade (This is the margin you allocate from your broker account for this strategy)
24 | self.leverage = 1 # 2x, 3x Etc
25 | self.maxTradesPerDay = 1 # Max number of trades per day under this strategy
26 | self.isFnO = False # Does this strategy trade in FnO or not
27 | self.capitalPerSet = 0 # Applicable if isFnO is True (Set means 1CE/1PE or 2CE/2PE etc based on your strategy logic)
28 | # Register strategy with trade manager
29 | TradeManager.registerStrategy(self)
30 | # Load all trades of this strategy into self.trades on restart of app
31 | self.trades = TradeManager.getAllTradesByStrategy(self.name)
32 |
33 | def getName(self):
34 | return self.name
35 |
36 | def isEnabled(self):
37 | return self.enabled
38 |
39 | def setDisabled(self):
40 | self.enabled = False
41 |
42 | def process(self):
43 | # Implementation is specific to each strategy - To defined in derived class
44 | logging.info("BaseStrategy process is called.")
45 | pass
46 |
47 | def calculateCapitalPerTrade(self):
48 | leverage = self.leverage if self.leverage > 0 else 1
49 | capitalPerTrade = int(self.capital * leverage / self.maxTradesPerDay)
50 | return capitalPerTrade
51 |
52 | def calculateLotsPerTrade(self):
53 | if self.isFnO == False:
54 | return 0
55 | # Applicable only for fno
56 | return int(self.capital / self.capitalPerSet)
57 |
58 | def canTradeToday(self):
59 | # Derived class should override the logic if the strategy to be traded only on specific days of the week
60 | return True
61 |
62 | def run(self):
63 | # NOTE: This should not be overriden in Derived class
64 | if self.enabled == False:
65 | logging.warn("%s: Not going to run strategy as its not enabled.", self.getName())
66 | return
67 |
68 | if Utils.isMarketClosedForTheDay():
69 | logging.warn("%s: Not going to run strategy as market is closed.", self.getName())
70 | return
71 |
72 | now = datetime.now()
73 | if now < Utils.getMarketStartTime():
74 | Utils.waitTillMarketOpens(self.getName())
75 |
76 | if self.canTradeToday() == False:
77 | logging.warn("%s: Not going to run strategy as it cannot be traded today.", self.getName())
78 | return
79 |
80 | now = datetime.now()
81 | if now < self.startTimestamp:
82 | waitSeconds = Utils.getEpoch(self.startTimestamp) - Utils.getEpoch(now)
83 | logging.info("%s: Waiting for %d seconds till startegy start timestamp reaches...", self.getName(), waitSeconds)
84 | if waitSeconds > 0:
85 | time.sleep(waitSeconds)
86 |
87 | # Run in an loop and keep processing
88 | while True:
89 | if Utils.isMarketClosedForTheDay():
90 | logging.warn("%s: Exiting the strategy as market closed.", self.getName())
91 | break
92 |
93 | # Derived class specific implementation will be called when process() is called
94 | self.process()
95 |
96 | # Sleep and wake up on every 30th second
97 | now = datetime.now()
98 | waitSeconds = 30 - (now.second % 30)
99 | time.sleep(waitSeconds)
100 |
101 | def shouldPlaceTrade(self, trade, tick):
102 | # Each strategy should call this function from its own shouldPlaceTrade() method before working on its own logic
103 | if trade == None:
104 | return False
105 | if trade.qty == 0:
106 | TradeManager.disableTrade(trade, 'InvalidQuantity')
107 | return False
108 |
109 | now = datetime.now()
110 | if now > self.stopTimestamp:
111 | TradeManager.disableTrade(trade, 'NoNewTradesCutOffTimeReached')
112 | return False
113 |
114 | numOfTradesPlaced = TradeManager.getNumberOfTradesPlacedByStrategy(self.getName())
115 | if numOfTradesPlaced >= self.maxTradesPerDay:
116 | TradeManager.disableTrade(trade, 'MaxTradesPerDayReached')
117 | return False
118 |
119 | return True
120 |
121 | def addTradeToList(self, trade):
122 | if trade != None:
123 | self.trades.append(trade)
124 |
125 | def getQuote(self, tradingSymbol):
126 | return Quotes.getQuote(tradingSymbol, self.isFnO)
127 |
128 | def getTrailingSL(self, trade):
129 | return 0
--------------------------------------------------------------------------------
/src/strategies/SampleStrategy.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from models.Direction import Direction
4 | from models.ProductType import ProductType
5 | from strategies.BaseStrategy import BaseStrategy
6 | from utils.Utils import Utils
7 | from trademgmt.Trade import Trade
8 | from trademgmt.TradeManager import TradeManager
9 |
10 | # Each strategy has to be derived from BaseStrategy
11 | class SampleStrategy(BaseStrategy):
12 | __instance = None
13 |
14 | @staticmethod
15 | def getInstance(): # singleton class
16 | if SampleStrategy.__instance == None:
17 | SampleStrategy()
18 | return SampleStrategy.__instance
19 |
20 | def __init__(self):
21 | if SampleStrategy.__instance != None:
22 | raise Exception("This class is a singleton!")
23 | else:
24 | SampleStrategy.__instance = self
25 | # Call Base class constructor
26 | super().__init__("SAMPLE")
27 | # Initialize all the properties specific to this strategy
28 | self.productType = ProductType.MIS
29 | self.symbols = ["SBIN", "INFY", "TATASTEEL", "RELIANCE", "HDFCBANK", "CIPLA"]
30 | self.slPercentage = 1.1
31 | self.targetPercentage = 2.2
32 | self.startTimestamp = Utils.getTimeOfToDay(9, 30, 0) # When to start the strategy. Default is Market start time
33 | self.stopTimestamp = Utils.getTimeOfToDay(14, 30, 0) # This is not square off timestamp. This is the timestamp after which no new trades will be placed under this strategy but existing trades continue to be active.
34 | self.squareOffTimestamp = Utils.getTimeOfToDay(15, 0, 0) # Square off time
35 | self.capital = 3000 # Capital to trade (This is the margin you allocate from your broker account for this strategy)
36 | self.leverage = 2 # 2x, 3x Etc
37 | self.maxTradesPerDay = 3 # Max number of trades per day under this strategy
38 | self.isFnO = False # Does this strategy trade in FnO or not
39 | self.capitalPerSet = 0 # Applicable if isFnO is True (1 set means 1CE/1PE or 2CE/2PE etc based on your strategy logic)
40 |
41 | def process(self):
42 | if len(self.trades) >= self.maxTradesPerDay:
43 | return
44 | # This is a sample strategy with the following logic:
45 | # 1. If current market price > 0.5% from previous day close then create LONG trade
46 | # 2. If current market price < 0.5% from previous day close then create SHORT trade
47 | for symbol in self.symbols:
48 | quote = self.getQuote(symbol)
49 | if quote == None:
50 | logging.error('%s: Could not get quote for %s', self.getName(), symbol)
51 | continue
52 | longBreakoutPrice = Utils.roundToNSEPrice(quote.close + quote.close * 0.5 / 100)
53 | shortBreakoutPrice = Utils.roundToNSEPrice(quote.close - quote.close * 0.5 / 100)
54 | cmp = quote.lastTradedPrice
55 | logging.info('%s: %s => long = %f, short = %f, CMP = %f', self.getName(), symbol, longBreakoutPrice, shortBreakoutPrice, cmp)
56 |
57 | direction = None
58 | breakoutPrice = 0
59 | if cmp > longBreakoutPrice:
60 | direction = 'LONG'
61 | breakoutPrice = longBreakoutPrice
62 | elif cmp < shortBreakoutPrice:
63 | direction = 'SHORT'
64 | breakoutPrice = shortBreakoutPrice
65 | if direction == None:
66 | continue
67 |
68 | self.generateTrade(symbol, direction, breakoutPrice, cmp)
69 |
70 |
71 | def generateTrade(self, tradingSymbol, direction, breakoutPrice, cmp):
72 | trade = Trade(tradingSymbol)
73 | trade.strategy = self.getName()
74 | trade.direction = direction
75 | trade.productType = self.productType
76 | trade.placeMarketOrder = True
77 | trade.requestedEntry = breakoutPrice
78 | trade.timestamp = Utils.getEpoch(self.startTimestamp) # setting this to strategy timestamp
79 | trade.qty = int(self.calculateCapitalPerTrade() / breakoutPrice)
80 | if trade.qty == 0:
81 | trade.qty = 1 # Keep min 1 qty
82 | if direction == 'LONG':
83 | trade.stopLoss = Utils.roundToNSEPrice(breakoutPrice - breakoutPrice * self.slPercentage / 100)
84 | if cmp < trade.stopLoss:
85 | trade.stopLoss = Utils.roundToNSEPrice(cmp - cmp * 1 / 100)
86 | else:
87 | trade.stopLoss = Utils.roundToNSEPrice(breakoutPrice + breakoutPrice * self.slPercentage / 100)
88 | if cmp > trade.stopLoss:
89 | trade.stopLoss = Utils.roundToNSEPrice(cmp + cmp * 1 / 100)
90 |
91 | if direction == 'LONG':
92 | trade.target = Utils.roundToNSEPrice(breakoutPrice + breakoutPrice * self.targetPercentage / 100)
93 | else:
94 | trade.target = Utils.roundToNSEPrice(breakoutPrice - breakoutPrice * self.targetPercentage / 100)
95 |
96 | trade.intradaySquareOffTimestamp = Utils.getEpoch(self.squareOffTimestamp)
97 | # Hand over the trade to TradeManager
98 | TradeManager.addNewTrade(trade)
99 |
100 | def shouldPlaceTrade(self, trade, tick):
101 | # First call base class implementation and if it returns True then only proceed
102 | if super().shouldPlaceTrade(trade, tick) == False:
103 | return False
104 |
105 | if tick == None:
106 | return False
107 |
108 | if trade.direction == Direction.LONG and tick.lastTradedPrice > trade.requestedEntry:
109 | return True
110 | elif trade.direction == Direction.SHORT and tick.lastTradedPrice < trade.requestedEntry:
111 | return True
112 | return False
113 |
--------------------------------------------------------------------------------
/src/strategies/OptionSelling.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 |
4 | from instruments.Instruments import Instruments
5 | from models.Direction import Direction
6 | from models.ProductType import ProductType
7 | from strategies.BaseStrategy import BaseStrategy
8 | from utils.Utils import Utils
9 | from trademgmt.Trade import Trade
10 | from trademgmt.TradeManager import TradeManager
11 |
12 | # Each strategy has to be derived from BaseStrategy
13 | class OptionSelling(BaseStrategy):
14 | __instance = None
15 |
16 | @staticmethod
17 | def getInstance(): # singleton class
18 | if OptionSelling.__instance == None:
19 | OptionSelling()
20 | return OptionSelling.__instance
21 |
22 | def __init__(self):
23 | if OptionSelling.__instance != None:
24 | raise Exception("This class is a singleton!")
25 | else:
26 | OptionSelling.__instance = self
27 | # Call Base class constructor
28 | super().__init__("OptionSelling")
29 | # Initialize all the properties specific to this strategy
30 | self.productType = ProductType.MIS
31 | self.symbols = []
32 | self.slPercentage = 50
33 | self.targetPercentage = 0
34 | self.startTimestamp = Utils.getTimeOfToDay(9, 30, 0) # When to start the strategy. Default is Market start time
35 | self.stopTimestamp = Utils.getTimeOfToDay(14, 30, 0) # This is not square off timestamp. This is the timestamp after which no new trades will be placed under this strategy but existing trades continue to be active.
36 | self.squareOffTimestamp = Utils.getTimeOfToDay(15, 15, 0) # Square off time
37 | self.capital = 100000 # Capital to trade (This is the margin you allocate from your broker account for this strategy)
38 | self.leverage = 0
39 | self.maxTradesPerDay = 2 # (1 CE + 1 PE) Max number of trades per day under this strategy
40 | self.isFnO = True # Does this strategy trade in FnO or not
41 | self.capitalPerSet = 100000 # Applicable if isFnO is True (1 set means 1CE/1PE or 2CE/2PE etc based on your strategy logic)
42 |
43 | def canTradeToday(self):
44 | if Utils.isTodayOneDayBeforeWeeklyExpiryDay() == True:
45 | logging.info('%s: Today is one day before weekly expiry date hence going to trade this strategy', self.getName())
46 | return True
47 | if Utils.isTodayWeeklyExpiryDay() == True:
48 | logging.info('%s: Today is weekly expiry day hence going to trade this strategy today', self.getName())
49 | return True
50 | logging.info('%s: Today is neither day before expiry nor expiry day. Hence NOT going to trade this strategy today', self.getName())
51 | return False
52 |
53 | def process(self):
54 | now = datetime.now()
55 | if now < self.startTimestamp:
56 | return
57 | if len(self.trades) >= self.maxTradesPerDay:
58 | return
59 |
60 | # Get current market price of Nifty Future
61 | futureSymbol = Utils.prepareMonthlyExpiryFuturesSymbol('NIFTY')
62 | quote = self.getQuote(futureSymbol)
63 | if quote == None:
64 | logging.error('%s: Could not get quote for %s', self.getName(), futureSymbol)
65 | return
66 |
67 | ATMStrike = Utils.getNearestStrikePrice(quote.lastTradedPrice, 50)
68 | logging.info('%s: Nifty CMP = %f, ATMStrike = %d', self.getName(), quote.lastTradedPrice, ATMStrike)
69 |
70 | ATMPlus50CESymbol = Utils.prepareWeeklyOptionsSymbol("NIFTY", ATMStrike + 50, 'CE')
71 | ATMMinus50PESymbol = Utils.prepareWeeklyOptionsSymbol("NIFTY", ATMStrike - 50, 'PE')
72 | logging.info('%s: ATMPlus50CE = %s, ATMMinus50PE = %s', self.getName(), ATMPlus50CESymbol, ATMMinus50PESymbol)
73 | # create trades
74 | self.generateTrades(ATMPlus50CESymbol, ATMMinus50PESymbol)
75 |
76 | def generateTrades(self, ATMPlus50CESymbol, ATMMinus50PESymbol):
77 | numLots = self.calculateLotsPerTrade()
78 | quoteATMPlus50CESymbol = self.getQuote(ATMPlus50CESymbol)
79 | quoteATMMinus50PESymbol = self.getQuote(ATMMinus50PESymbol)
80 | if quoteATMPlus50CESymbol == None or quoteATMMinus50PESymbol == None:
81 | logging.error('%s: Could not get quotes for option symbols', self.getName())
82 | return
83 |
84 | self.generateTrade(ATMPlus50CESymbol, numLots, quoteATMPlus50CESymbol.lastTradedPrice)
85 | self.generateTrade(ATMMinus50PESymbol, numLots, quoteATMMinus50PESymbol.lastTradedPrice)
86 | logging.info('%s: Trades generated.', self.getName())
87 |
88 | def generateTrade(self, optionSymbol, numLots, lastTradedPrice):
89 | trade = Trade(optionSymbol)
90 | trade.strategy = self.getName()
91 | trade.isOptions = True
92 | trade.direction = Direction.SHORT # Always short here as option selling only
93 | trade.productType = self.productType
94 | trade.placeMarketOrder = True
95 | trade.requestedEntry = lastTradedPrice
96 | trade.timestamp = Utils.getEpoch(self.startTimestamp) # setting this to strategy timestamp
97 |
98 | isd = Instruments.getInstrumentDataBySymbol(optionSymbol) # Get instrument data to know qty per lot
99 | trade.qty = isd['lot_size'] * numLots
100 |
101 | trade.stopLoss = Utils.roundToNSEPrice(trade.requestedEntry + trade.requestedEntry * self.slPercentage / 100)
102 | trade.target = 0 # setting to 0 as no target is applicable for this trade
103 |
104 | trade.intradaySquareOffTimestamp = Utils.getEpoch(self.squareOffTimestamp)
105 | # Hand over the trade to TradeManager
106 | TradeManager.addNewTrade(trade)
107 |
108 | def shouldPlaceTrade(self, trade, tick):
109 | # First call base class implementation and if it returns True then only proceed
110 | if super().shouldPlaceTrade(trade, tick) == False:
111 | return False
112 | # We dont have any condition to be checked here for this strategy just return True
113 | return True
114 |
--------------------------------------------------------------------------------
/src/strategies/ShortStraddleBNF.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from datetime import datetime
3 |
4 | from instruments.Instruments import Instruments
5 | from models.Direction import Direction
6 | from models.ProductType import ProductType
7 | from strategies.BaseStrategy import BaseStrategy
8 | from utils.Utils import Utils
9 | from trademgmt.Trade import Trade
10 | from trademgmt.TradeManager import TradeManager
11 |
12 | # Each strategy has to be derived from BaseStrategy
13 | class ShortStraddleBNF(BaseStrategy):
14 | __instance = None
15 |
16 | @staticmethod
17 | def getInstance(): # singleton class
18 | if ShortStraddleBNF.__instance == None:
19 | ShortStraddleBNF()
20 | return ShortStraddleBNF.__instance
21 |
22 | def __init__(self):
23 | if ShortStraddleBNF.__instance != None:
24 | raise Exception("This class is a singleton!")
25 | else:
26 | ShortStraddleBNF.__instance = self
27 | # Call Base class constructor
28 | super().__init__("ShortStraddleBNF")
29 | # Initialize all the properties specific to this strategy
30 | self.productType = ProductType.MIS
31 | self.symbols = []
32 | self.slPercentage = 30
33 | self.targetPercentage = 0
34 | self.startTimestamp = Utils.getTimeOfToDay(11, 0, 0) # When to start the strategy. Default is Market start time
35 | self.stopTimestamp = Utils.getTimeOfToDay(14, 0, 0) # This is not square off timestamp. This is the timestamp after which no new trades will be placed under this strategy but existing trades continue to be active.
36 | self.squareOffTimestamp = Utils.getTimeOfToDay(14, 30, 0) # Square off time
37 | self.capital = 100000 # Capital to trade (This is the margin you allocate from your broker account for this strategy)
38 | self.leverage = 0
39 | self.maxTradesPerDay = 2 # (1 CE + 1 PE) Max number of trades per day under this strategy
40 | self.isFnO = True # Does this strategy trade in FnO or not
41 | self.capitalPerSet = 100000 # Applicable if isFnO is True (1 set means 1CE/1PE or 2CE/2PE etc based on your strategy logic)
42 |
43 | def canTradeToday(self):
44 | # Even if you remove this function canTradeToday() completely its same as allowing trade every day
45 | return True
46 |
47 | def process(self):
48 | now = datetime.now()
49 | if now < self.startTimestamp:
50 | return
51 | if len(self.trades) >= self.maxTradesPerDay:
52 | return
53 |
54 | # Get current market price of Nifty Future
55 | futureSymbol = Utils.prepareMonthlyExpiryFuturesSymbol('BANKNIFTY')
56 | quote = self.getQuote(futureSymbol)
57 | if quote == None:
58 | logging.error('%s: Could not get quote for %s', self.getName(), futureSymbol)
59 | return
60 |
61 | ATMStrike = Utils.getNearestStrikePrice(quote.lastTradedPrice, 100)
62 | logging.info('%s: Nifty CMP = %f, ATMStrike = %d', self.getName(), quote.lastTradedPrice, ATMStrike)
63 |
64 | ATMCESymbol = Utils.prepareWeeklyOptionsSymbol("BANKNIFTY", ATMStrike, 'CE')
65 | ATMPESymbol = Utils.prepareWeeklyOptionsSymbol("BANKNIFTY", ATMStrike, 'PE')
66 | logging.info('%s: ATMCESymbol = %s, ATMPESymbol = %s', self.getName(), ATMCESymbol, ATMPESymbol)
67 | # create trades
68 | self.generateTrades(ATMCESymbol, ATMPESymbol)
69 |
70 | def generateTrades(self, ATMCESymbol, ATMPESymbol):
71 | numLots = self.calculateLotsPerTrade()
72 | quoteATMCESymbol = self.getQuote(ATMCESymbol)
73 | quoteATMPESymbol = self.getQuote(ATMPESymbol)
74 | if quoteATMCESymbol == None or quoteATMPESymbol == None:
75 | logging.error('%s: Could not get quotes for option symbols', self.getName())
76 | return
77 |
78 | self.generateTrade(ATMCESymbol, numLots, quoteATMCESymbol.lastTradedPrice)
79 | self.generateTrade(ATMPESymbol, numLots, quoteATMPESymbol.lastTradedPrice)
80 | logging.info('%s: Trades generated.', self.getName())
81 |
82 | def generateTrade(self, optionSymbol, numLots, lastTradedPrice):
83 | trade = Trade(optionSymbol)
84 | trade.strategy = self.getName()
85 | trade.isOptions = True
86 | trade.direction = Direction.SHORT # Always short here as option selling only
87 | trade.productType = self.productType
88 | trade.placeMarketOrder = True
89 | trade.requestedEntry = lastTradedPrice
90 | trade.timestamp = Utils.getEpoch(self.startTimestamp) # setting this to strategy timestamp
91 |
92 | isd = Instruments.getInstrumentDataBySymbol(optionSymbol) # Get instrument data to know qty per lot
93 | trade.qty = isd['lot_size'] * numLots
94 |
95 | trade.stopLoss = Utils.roundToNSEPrice(trade.requestedEntry + trade.requestedEntry * self.slPercentage / 100)
96 | trade.target = 0 # setting to 0 as no target is applicable for this trade
97 |
98 | trade.intradaySquareOffTimestamp = Utils.getEpoch(self.squareOffTimestamp)
99 | # Hand over the trade to TradeManager
100 | TradeManager.addNewTrade(trade)
101 |
102 | def shouldPlaceTrade(self, trade, tick):
103 | # First call base class implementation and if it returns True then only proceed
104 | if super().shouldPlaceTrade(trade, tick) == False:
105 | return False
106 | # We dont have any condition to be checked here for this strategy just return True
107 | return True
108 |
109 | def getTrailingSL(self, trade):
110 | if trade == None:
111 | return 0
112 | if trade.entry == 0:
113 | return 0
114 | lastTradedPrice = TradeManager.getLastTradedPrice(trade.tradingSymbol)
115 | if lastTradedPrice == 0:
116 | return 0
117 |
118 | trailSL = 0
119 | profitPoints = int(trade.entry - lastTradedPrice)
120 | if profitPoints >= 5:
121 | factor = int(profitPoints / 5)
122 | trailSL = Utils.roundToNSEPrice(trade.initialStopLoss - factor * 5)
123 | logging.info('%s: %s Returning trail SL %f', self.getName(), trade.tradingSymbol, trailSL)
124 | return trailSL
125 |
126 |
--------------------------------------------------------------------------------
/src/ordermgmt/ZerodhaOrderManager.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from ordermgmt.BaseOrderManager import BaseOrderManager
4 | from ordermgmt.Order import Order
5 |
6 | from models.ProductType import ProductType
7 | from models.OrderType import OrderType
8 | from models.Direction import Direction
9 | from models.OrderStatus import OrderStatus
10 |
11 | from utils.Utils import Utils
12 |
13 | class ZerodhaOrderManager(BaseOrderManager):
14 | def __init__(self):
15 | super().__init__("zerodha")
16 |
17 | def placeOrder(self, orderInputParams):
18 | logging.info('%s: Going to place order with params %s', self.broker, orderInputParams)
19 | kite = self.brokerHandle
20 | try:
21 | orderId = kite.place_order(
22 | variety=kite.VARIETY_REGULAR,
23 | exchange=kite.EXCHANGE_NFO if orderInputParams.isFnO == True else kite.EXCHANGE_NSE,
24 | tradingsymbol=orderInputParams.tradingSymbol,
25 | transaction_type=self.convertToBrokerDirection(orderInputParams.direction),
26 | quantity=orderInputParams.qty,
27 | price=orderInputParams.price,
28 | trigger_price=orderInputParams.triggerPrice,
29 | product=self.convertToBrokerProductType(orderInputParams.productType),
30 | order_type=self.convertToBrokerOrderType(orderInputParams.orderType))
31 |
32 | logging.info('%s: Order placed successfully, orderId = %s', self.broker, orderId)
33 | order = Order(orderInputParams)
34 | order.orderId = orderId
35 | order.orderPlaceTimestamp = Utils.getEpoch()
36 | order.lastOrderUpdateTimestamp = Utils.getEpoch()
37 | return order
38 | except Exception as e:
39 | logging.info('%s Order placement failed: %s', self.broker, str(e))
40 | raise Exception(str(e))
41 |
42 | def modifyOrder(self, order, orderModifyParams):
43 | logging.info('%s: Going to modify order with params %s', self.broker, orderModifyParams)
44 | kite = self.brokerHandle
45 | try:
46 | orderId = kite.modify_order(
47 | variety=kite.VARIETY_REGULAR,
48 | order_id=order.orderId,
49 | quantity=orderModifyParams.newQty if orderModifyParams.newQty > 0 else None,
50 | price=orderModifyParams.newPrice if orderModifyParams.newPrice > 0 else None,
51 | trigger_price=orderModifyParams.newTriggerPrice if orderModifyParams.newTriggerPrice > 0 else None,
52 | order_type=orderModifyParams.newOrderType if orderModifyParams.newOrderType != None else None)
53 |
54 | logging.info('%s Order modified successfully for orderId = %s', self.broker, orderId)
55 | order.lastOrderUpdateTimestamp = Utils.getEpoch()
56 | return order
57 | except Exception as e:
58 | logging.info('%s Order modify failed: %s', self.broker, str(e))
59 | raise Exception(str(e))
60 |
61 | def modifyOrderToMarket(self, order):
62 | logging.info('%s: Going to modify order with params %s', self.broker)
63 | kite = self.brokerHandle
64 | try:
65 | orderId = kite.modify_order(
66 | variety=kite.VARIETY_REGULAR,
67 | order_id=order.orderId,
68 | order_type=kite.ORDER_TYPE_MARKET)
69 |
70 | logging.info('%s Order modified successfully to MARKET for orderId = %s', self.broker, orderId)
71 | order.lastOrderUpdateTimestamp = Utils.getEpoch()
72 | return order
73 | except Exception as e:
74 | logging.info('%s Order modify to market failed: %s', self.broker, str(e))
75 | raise Exception(str(e))
76 |
77 | def cancelOrder(self, order):
78 | logging.info('%s Going to cancel order %s', self.broker, order.orderId)
79 | kite = self.brokerHandle
80 | try:
81 | orderId = kite.cancel_order(
82 | variety=kite.VARIETY_REGULAR,
83 | order_id=order.orderId)
84 |
85 | logging.info('%s Order cancelled successfully, orderId = %s', self.broker, orderId)
86 | order.lastOrderUpdateTimestamp = Utils.getEpoch()
87 | return order
88 | except Exception as e:
89 | logging.info('%s Order cancel failed: %s', self.broker, str(e))
90 | raise Exception(str(e))
91 |
92 | def fetchAndUpdateAllOrderDetails(self, orders):
93 | logging.info('%s Going to fetch order book', self.broker)
94 | kite = self.brokerHandle
95 | orderBook = None
96 | try:
97 | orderBook = kite.orders()
98 | except Exception as e:
99 | logging.error('%s Failed to fetch order book', self.broker)
100 | return
101 |
102 | logging.info('%s Order book length = %d', self.broker, len(orderBook))
103 | numOrdersUpdated = 0
104 | for bOrder in orderBook:
105 | foundOrder = None
106 | for order in orders:
107 | if order.orderId == bOrder['order_id']:
108 | foundOrder = order
109 | break
110 |
111 | if foundOrder != None:
112 | logging.info('Found order for orderId %s', foundOrder.orderId)
113 | foundOrder.qty = bOrder['quantity']
114 | foundOrder.filledQty = bOrder['filled_quantity']
115 | foundOrder.pendingQty = bOrder['pending_quantity']
116 | foundOrder.orderStatus = bOrder['status']
117 | if foundOrder.orderStatus == OrderStatus.CANCELLED and foundOrder.filledQty > 0:
118 | # Consider this case as completed in our system as we cancel the order with pending qty when strategy stop timestamp reaches
119 | foundOrder.orderStatus = OrderStatus.COMPLETED
120 | foundOrder.price = bOrder['price']
121 | foundOrder.triggerPrice = bOrder['trigger_price']
122 | foundOrder.averagePrice = bOrder['average_price']
123 | logging.info('%s Updated order %s', self.broker, foundOrder)
124 | numOrdersUpdated += 1
125 |
126 | logging.info('%s: %d orders updated with broker order details', self.broker, numOrdersUpdated)
127 |
128 | def convertToBrokerProductType(self, productType):
129 | kite = self.brokerHandle
130 | if productType == ProductType.MIS:
131 | return kite.PRODUCT_MIS
132 | elif productType == ProductType.NRML:
133 | return kite.PRODUCT_NRML
134 | elif productType == ProductType.CNC:
135 | return kite.PRODUCT_CNC
136 | return None
137 |
138 | def convertToBrokerOrderType(self, orderType):
139 | kite = self.brokerHandle
140 | if orderType == OrderType.LIMIT:
141 | return kite.ORDER_TYPE_LIMIT
142 | elif orderType == OrderType.MARKET:
143 | return kite.ORDER_TYPE_MARKET
144 | elif orderType == OrderType.SL_MARKET:
145 | return kite.ORDER_TYPE_SLM
146 | elif orderType == OrderType.SL_LIMIT:
147 | return kite.ORDER_TYPE_SL
148 | return None
149 |
150 | def convertToBrokerDirection(self, direction):
151 | kite = self.brokerHandle
152 | if direction == Direction.LONG:
153 | return kite.TRANSACTION_TYPE_BUY
154 | elif direction == Direction.SHORT:
155 | return kite.TRANSACTION_TYPE_SELL
156 | return None
157 |
--------------------------------------------------------------------------------
/src/utils/Utils.py:
--------------------------------------------------------------------------------
1 | import math
2 | import uuid
3 | import time
4 | import logging
5 | import calendar
6 | from datetime import datetime, timedelta
7 |
8 | from config.Config import getHolidays
9 | from models.Direction import Direction
10 | from trademgmt.TradeState import TradeState
11 |
12 | class Utils:
13 | dateFormat = "%Y-%m-%d"
14 | timeFormat = "%H:%M:%S"
15 | dateTimeFormat = "%Y-%m-%d %H:%M:%S"
16 |
17 | @staticmethod
18 | def roundOff(price): # Round off to 2 decimal places
19 | return round(price, 2)
20 |
21 | @staticmethod
22 | def roundToNSEPrice(price):
23 | x = round(price, 2) * 20
24 | y = math.ceil(x)
25 | return y / 20
26 |
27 | @staticmethod
28 | def isMarketOpen():
29 | if Utils.isTodayHoliday():
30 | return False
31 | now = datetime.now()
32 | marketStartTime = Utils.getMarketStartTime()
33 | marketEndTime = Utils.getMarketEndTime()
34 | return now >= marketStartTime and now <= marketEndTime
35 |
36 | @staticmethod
37 | def isMarketClosedForTheDay():
38 | # This method returns true if the current time is > marketEndTime
39 | # Please note this will not return true if current time is < marketStartTime on a trading day
40 | if Utils.isTodayHoliday():
41 | return True
42 | now = datetime.now()
43 | marketEndTime = Utils.getMarketEndTime()
44 | return now > marketEndTime
45 |
46 | @staticmethod
47 | def waitTillMarketOpens(context):
48 | nowEpoch = Utils.getEpoch(datetime.now())
49 | marketStartTimeEpoch = Utils.getEpoch(Utils.getMarketStartTime())
50 | waitSeconds = marketStartTimeEpoch - nowEpoch
51 | if waitSeconds > 0:
52 | logging.info("%s: Waiting for %d seconds till market opens...", context, waitSeconds)
53 | time.sleep(waitSeconds)
54 |
55 | @staticmethod
56 | def getEpoch(datetimeObj = None):
57 | # This method converts given datetimeObj to epoch seconds
58 | if datetimeObj == None:
59 | datetimeObj = datetime.now()
60 | epochSeconds = datetime.timestamp(datetimeObj)
61 | return int(epochSeconds) # converting double to long
62 |
63 | @staticmethod
64 | def getMarketStartTime(dateTimeObj = None):
65 | return Utils.getTimeOfDay(9, 15, 0, dateTimeObj)
66 |
67 | @staticmethod
68 | def getMarketEndTime(dateTimeObj = None):
69 | return Utils.getTimeOfDay(15, 30, 0, dateTimeObj)
70 |
71 | @staticmethod
72 | def getTimeOfDay(hours, minutes, seconds, dateTimeObj = None):
73 | if dateTimeObj == None:
74 | dateTimeObj = datetime.now()
75 | dateTimeObj = dateTimeObj.replace(hour=hours, minute=minutes, second=seconds, microsecond=0)
76 | return dateTimeObj
77 |
78 | @staticmethod
79 | def getTimeOfToDay(hours, minutes, seconds):
80 | return Utils.getTimeOfDay(hours, minutes, seconds, datetime.now())
81 |
82 | @staticmethod
83 | def getTodayDateStr():
84 | return Utils.convertToDateStr(datetime.now())
85 |
86 | @staticmethod
87 | def convertToDateStr(datetimeObj):
88 | return datetimeObj.strftime(Utils.dateFormat)
89 |
90 | @staticmethod
91 | def isHoliday(datetimeObj):
92 | dayOfWeek = calendar.day_name[datetimeObj.weekday()]
93 | if dayOfWeek == 'Saturday' or dayOfWeek == 'Sunday':
94 | return True
95 |
96 | dateStr = Utils.convertToDateStr(datetimeObj)
97 | holidays = getHolidays()
98 | if (dateStr in holidays):
99 | return True
100 | else:
101 | return False
102 |
103 | @staticmethod
104 | def isTodayHoliday():
105 | return Utils.isHoliday(datetime.now())
106 |
107 | @staticmethod
108 | def generateTradeID():
109 | return str(uuid.uuid4())
110 |
111 | @staticmethod
112 | def calculateTradePnl(trade):
113 | if trade.tradeState == TradeState.ACTIVE:
114 | if trade.cmp > 0:
115 | if trade.direction == Direction.LONG:
116 | trade.pnl = Utils.roundOff(trade.filledQty * (trade.cmp - trade.entry))
117 | else:
118 | trade.pnl = Utils.roundOff(trade.filledQty * (trade.entry - trade.cmp))
119 | else:
120 | if trade.exit > 0:
121 | if trade.direction == Direction.LONG:
122 | trade.pnl = Utils.roundOff(trade.filledQty * (trade.exit - trade.entry))
123 | else:
124 | trade.pnl = Utils.roundOff(trade.filledQty * (trade.entry - trade.exit))
125 | tradeValue = trade.entry * trade.filledQty
126 | if tradeValue > 0:
127 | trade.pnlPercentage = Utils.roundOff(trade.pnl * 100 / tradeValue)
128 | return trade
129 |
130 | @staticmethod
131 | def prepareMonthlyExpiryFuturesSymbol(inputSymbol):
132 | expiryDateTime = Utils.getMonthlyExpiryDayDate()
133 | expiryDateMarketEndTime = Utils.getMarketEndTime(expiryDateTime)
134 | now = datetime.now()
135 | if now > expiryDateMarketEndTime:
136 | # increasing today date by 20 days to get some day in next month passing to getMonthlyExpiryDayDate()
137 | expiryDateTime = Utils.getMonthlyExpiryDayDate(now + timedelta(days=20))
138 | year2Digits = str(expiryDateTime.year)[2:]
139 | monthShort = calendar.month_name[expiryDateTime.month].upper()[0:3]
140 | futureSymbol = inputSymbol + year2Digits + monthShort + 'FUT'
141 | logging.info('prepareMonthlyExpiryFuturesSymbol[%s] = %s', inputSymbol, futureSymbol)
142 | return futureSymbol
143 |
144 | @staticmethod
145 | def prepareWeeklyOptionsSymbol(inputSymbol, strike, optionType, numWeeksPlus = 0):
146 | expiryDateTime = Utils.getWeeklyExpiryDayDate()
147 | todayMarketStartTime = Utils.getMarketStartTime()
148 | expiryDayMarketEndTime = Utils.getMarketEndTime(expiryDateTime)
149 | if numWeeksPlus > 0:
150 | expiryDateTime = expiryDateTime + timedelta(days=numWeeksPlus * 7)
151 | expiryDateTime = Utils.getWeeklyExpiryDayDate(expiryDateTime)
152 | if todayMarketStartTime > expiryDayMarketEndTime:
153 | expiryDateTime = expiryDateTime + timedelta(days=6)
154 | expiryDateTime = Utils.getWeeklyExpiryDayDate(expiryDateTime)
155 | # Check if monthly and weekly expiry same
156 | expiryDateTimeMonthly = Utils.getMonthlyExpiryDayDate()
157 | weekAndMonthExpriySame = False
158 | if expiryDateTime == expiryDateTimeMonthly:
159 | weekAndMonthExpriySame = True
160 | logging.info('Weekly and Monthly expiry is same for %s', expiryDateTime)
161 | year2Digits = str(expiryDateTime.year)[2:]
162 | optionSymbol = None
163 | if weekAndMonthExpriySame == True:
164 | monthShort = calendar.month_name[expiryDateTime.month].upper()[0:3]
165 | optionSymbol = inputSymbol + str(year2Digits) + monthShort + str(strike) + optionType.upper()
166 | else:
167 | m = expiryDateTime.month
168 | d = expiryDateTime.day
169 | mStr = str(m)
170 | if m == 10:
171 | mStr = "O"
172 | elif m == 11:
173 | mStr = "N"
174 | elif m == 12:
175 | mStr = "D"
176 | dStr = ("0" + str(d)) if d < 10 else str(d)
177 | optionSymbol = inputSymbol + str(year2Digits) + mStr + dStr + str(strike) + optionType.upper()
178 | logging.info('prepareWeeklyOptionsSymbol[%s, %d, %s, %d] = %s', inputSymbol, strike, optionType, numWeeksPlus, optionSymbol)
179 | return optionSymbol
180 |
181 | @staticmethod
182 | def getMonthlyExpiryDayDate(datetimeObj = None):
183 | if datetimeObj == None:
184 | datetimeObj = datetime.now()
185 | year = datetimeObj.year
186 | month = datetimeObj.month
187 | lastDay = calendar.monthrange(year, month)[1] # 2nd entry is the last day of the month
188 | datetimeExpiryDay = datetime(year, month, lastDay)
189 | while calendar.day_name[datetimeExpiryDay.weekday()] != 'Thursday':
190 | datetimeExpiryDay = datetimeExpiryDay - timedelta(days=1)
191 | while Utils.isHoliday(datetimeExpiryDay) == True:
192 | datetimeExpiryDay = datetimeExpiryDay - timedelta(days=1)
193 |
194 | datetimeExpiryDay = Utils.getTimeOfDay(0, 0, 0, datetimeExpiryDay)
195 | return datetimeExpiryDay
196 |
197 | @staticmethod
198 | def getWeeklyExpiryDayDate(dateTimeObj = None):
199 | if dateTimeObj == None:
200 | dateTimeObj = datetime.now()
201 | daysToAdd = 0
202 | if dateTimeObj.weekday() >= 3:
203 | daysToAdd = -1 * (dateTimeObj.weekday() - 3)
204 | else:
205 | daysToAdd = 3 - dateTimeObj.weekday()
206 | datetimeExpiryDay = dateTimeObj + timedelta(days=daysToAdd)
207 | while Utils.isHoliday(datetimeExpiryDay) == True:
208 | datetimeExpiryDay = datetimeExpiryDay - timedelta(days=1)
209 |
210 | datetimeExpiryDay = Utils.getTimeOfDay(0, 0, 0, datetimeExpiryDay)
211 | return datetimeExpiryDay
212 |
213 | @staticmethod
214 | def isTodayWeeklyExpiryDay():
215 | expiryDate = Utils.getWeeklyExpiryDayDate()
216 | todayDate = Utils.getTimeOfToDay(0, 0, 0)
217 | if expiryDate == todayDate:
218 | return True
219 | return False
220 |
221 | @staticmethod
222 | def isTodayOneDayBeforeWeeklyExpiryDay():
223 | expiryDate = Utils.getWeeklyExpiryDayDate()
224 | todayDate = Utils.getTimeOfToDay(0, 0, 0)
225 | if expiryDate - timedelta(days=1) == todayDate:
226 | return True
227 | return False
228 |
229 | @staticmethod
230 | def getNearestStrikePrice(price, nearestMultiple = 50):
231 | inputPrice = int(price)
232 | remainder = int(inputPrice % nearestMultiple)
233 | if remainder < int(nearestMultiple / 2):
234 | return inputPrice - remainder
235 | else:
236 | return inputPrice + (nearestMultiple - remainder)
237 |
--------------------------------------------------------------------------------
/src/trademgmt/TradeManager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import time
4 | import json
5 | from datetime import datetime
6 |
7 | from config.Config import getServerConfig
8 | from core.Controller import Controller
9 | from ticker.ZerodhaTicker import ZerodhaTicker
10 | from trademgmt.Trade import Trade
11 | from trademgmt.TradeState import TradeState
12 | from trademgmt.TradeExitReason import TradeExitReason
13 | from trademgmt.TradeEncoder import TradeEncoder
14 | from ordermgmt.ZerodhaOrderManager import ZerodhaOrderManager
15 | from ordermgmt.OrderInputParams import OrderInputParams
16 | from ordermgmt.OrderModifyParams import OrderModifyParams
17 | from ordermgmt.Order import Order
18 | from models.OrderType import OrderType
19 | from models.OrderStatus import OrderStatus
20 | from models.Direction import Direction
21 |
22 | from utils.Utils import Utils
23 |
24 | class TradeManager:
25 | ticker = None
26 | trades = [] # to store all the trades
27 | strategyToInstanceMap = {}
28 | symbolToCMPMap = {}
29 | intradayTradesDir = None
30 | registeredSymbols = []
31 |
32 | @staticmethod
33 | def run():
34 | if Utils.isTodayHoliday():
35 | logging.info("Cannot start TradeManager as Today is Trading Holiday.")
36 | return
37 |
38 | if Utils.isMarketClosedForTheDay():
39 | logging.info("Cannot start TradeManager as Market is closed for the day.")
40 | return
41 |
42 | Utils.waitTillMarketOpens("TradeManager")
43 |
44 | # check and create trades directory for today`s date
45 | serverConfig = getServerConfig()
46 | tradesDir = os.path.join(serverConfig['deployDir'], 'trades')
47 | TradeManager.intradayTradesDir = os.path.join(tradesDir, Utils.getTodayDateStr())
48 | if os.path.exists(TradeManager.intradayTradesDir) == False:
49 | logging.info('TradeManager: Intraday Trades Directory %s does not exist. Hence going to create.', TradeManager.intradayTradesDir)
50 | os.makedirs(TradeManager.intradayTradesDir)
51 |
52 | # start ticker service
53 | brokerName = Controller.getBrokerName()
54 | if brokerName == "zerodha":
55 | TradeManager.ticker = ZerodhaTicker()
56 | #elif brokerName == "fyers" # not implemented
57 | # ticker = FyersTicker()
58 |
59 | TradeManager.ticker.startTicker()
60 | TradeManager.ticker.registerListener(TradeManager.tickerListener)
61 |
62 | # sleep for 2 seconds for ticker connection establishment
63 | time.sleep(2)
64 |
65 | # Load all trades from json files to app memory
66 | TradeManager.loadAllTradesFromFile()
67 |
68 | # track and update trades in a loop
69 | while True:
70 | if Utils.isMarketClosedForTheDay():
71 | logging.info('TradeManager: Stopping TradeManager as market closed.')
72 | break
73 |
74 | try:
75 | # Fetch all order details from broker and update orders in each trade
76 | TradeManager.fetchAndUpdateAllTradeOrders()
77 | # track each trade and take necessary action
78 | TradeManager.trackAndUpdateAllTrades()
79 | except Exception as e:
80 | logging.exception("Exception in TradeManager Main thread")
81 |
82 | # save updated data to json file
83 | TradeManager.saveAllTradesToFile()
84 |
85 | # sleep for 30 seconds and then continue
86 | time.sleep(30)
87 | logging.info('TradeManager: Main thread woke up..')
88 |
89 | @staticmethod
90 | def registerStrategy(strategyInstance):
91 | TradeManager.strategyToInstanceMap[strategyInstance.getName()] = strategyInstance
92 |
93 | @staticmethod
94 | def loadAllTradesFromFile():
95 | tradesFilepath = os.path.join(TradeManager.intradayTradesDir, 'trades.json')
96 | if os.path.exists(tradesFilepath) == False:
97 | logging.warn('TradeManager: loadAllTradesFromFile() Trades Filepath %s does not exist', tradesFilepath)
98 | return
99 | TradeManager.trades = []
100 | tFile = open(tradesFilepath, 'r')
101 | tradesData = json.loads(tFile.read())
102 | for tr in tradesData:
103 | trade = TradeManager.convertJSONToTrade(tr)
104 | logging.info('loadAllTradesFromFile trade => %s', trade)
105 | TradeManager.trades.append(trade)
106 | if trade.tradingSymbol not in TradeManager.registeredSymbols:
107 | # Algo register symbols with ticker
108 | TradeManager.ticker.registerSymbols([trade.tradingSymbol])
109 | TradeManager.registeredSymbols.append(trade.tradingSymbol)
110 | logging.info('TradeManager: Successfully loaded %d trades from json file %s', len(TradeManager.trades), tradesFilepath)
111 |
112 | @staticmethod
113 | def saveAllTradesToFile():
114 | tradesFilepath = os.path.join(TradeManager.intradayTradesDir, 'trades.json')
115 | with open(tradesFilepath, 'w') as tFile:
116 | json.dump(TradeManager.trades, tFile, indent=2, cls=TradeEncoder)
117 | logging.info('TradeManager: Saved %d trades to file %s', len(TradeManager.trades), tradesFilepath)
118 |
119 | @staticmethod
120 | def addNewTrade(trade):
121 | if trade == None:
122 | return
123 | logging.info('TradeManager: addNewTrade called for %s', trade)
124 | for tr in TradeManager.trades:
125 | if tr.equals(trade):
126 | logging.warn('TradeManager: Trade already exists so not adding again. %s', trade)
127 | return
128 | # Add the new trade to the list
129 | TradeManager.trades.append(trade)
130 | logging.info('TradeManager: trade %s added successfully to the list', trade.tradeID)
131 | # Register the symbol with ticker so that we will start getting ticks for this symbol
132 | if trade.tradingSymbol not in TradeManager.registeredSymbols:
133 | TradeManager.ticker.registerSymbols([trade.tradingSymbol])
134 | TradeManager.registeredSymbols.append(trade.tradingSymbol)
135 | # Also add the trade to strategy trades list
136 | strategyInstance = TradeManager.strategyToInstanceMap[trade.strategy]
137 | if strategyInstance != None:
138 | strategyInstance.addTradeToList(trade)
139 |
140 | @staticmethod
141 | def disableTrade(trade, reason):
142 | if trade != None:
143 | logging.info('TradeManager: Going to disable trade ID %s with the reason %s', trade.tradeID, reason)
144 | trade.tradeState = TradeState.DISABLED
145 |
146 | @staticmethod
147 | def tickerListener(tick):
148 | # logging.info('tickerLister: new tick received for %s = %f', tick.tradingSymbol, tick.lastTradedPrice);
149 | TradeManager.symbolToCMPMap[tick.tradingSymbol] = tick.lastTradedPrice # Store the latest tick in map
150 | # On each new tick, get a created trade and call its strategy whether to place trade or not
151 | for strategy in TradeManager.strategyToInstanceMap:
152 | longTrade = TradeManager.getUntriggeredTrade(tick.tradingSymbol, strategy, Direction.LONG)
153 | shortTrade = TradeManager.getUntriggeredTrade(tick.tradingSymbol, strategy, Direction.SHORT)
154 | if longTrade == None and shortTrade == None:
155 | continue
156 | strategyInstance = TradeManager.strategyToInstanceMap[strategy]
157 | if longTrade != None:
158 | if strategyInstance.shouldPlaceTrade(longTrade, tick):
159 | # place the longTrade
160 | isSuccess = TradeManager.executeTrade(longTrade)
161 | if isSuccess == True:
162 | # set longTrade state to ACTIVE
163 | longTrade.tradeState = TradeState.ACTIVE
164 | longTrade.startTimestamp = Utils.getEpoch()
165 | continue
166 |
167 | if shortTrade != None:
168 | if strategyInstance.shouldPlaceTrade(shortTrade, tick):
169 | # place the shortTrade
170 | isSuccess = TradeManager.executeTrade(shortTrade)
171 | if isSuccess == True:
172 | # set shortTrade state to ACTIVE
173 | shortTrade.tradeState = TradeState.ACTIVE
174 | shortTrade.startTimestamp = Utils.getEpoch()
175 |
176 | @staticmethod
177 | def getUntriggeredTrade(tradingSymbol, strategy, direction):
178 | trade = None
179 | for tr in TradeManager.trades:
180 | if tr.tradeState == TradeState.DISABLED:
181 | continue
182 | if tr.tradeState != TradeState.CREATED:
183 | continue
184 | if tr.tradingSymbol != tradingSymbol:
185 | continue
186 | if tr.strategy != strategy:
187 | continue
188 | if tr.direction != direction:
189 | continue
190 | trade = tr
191 | break
192 | return trade
193 |
194 | @staticmethod
195 | def executeTrade(trade):
196 | logging.info('TradeManager: Execute trade called for %s', trade)
197 | trade.initialStopLoss = trade.stopLoss
198 | # Create order input params object and place order
199 | oip = OrderInputParams(trade.tradingSymbol)
200 | oip.direction = trade.direction
201 | oip.productType = trade.productType
202 | oip.orderType = OrderType.MARKET if trade.placeMarketOrder == True else OrderType.LIMIT
203 | oip.price = trade.requestedEntry
204 | oip.qty = trade.qty
205 | if trade.isFutures == True or trade.isOptions == True:
206 | oip.isFnO = True
207 | try:
208 | trade.entryOrder = TradeManager.getOrderManager().placeOrder(oip)
209 | except Exception as e:
210 | logging.error('TradeManager: Execute trade failed for tradeID %s: Error => %s', trade.tradeID, str(e))
211 | return False
212 |
213 | logging.info('TradeManager: Execute trade successful for %s and entryOrder %s', trade, trade.entryOrder)
214 | return True
215 |
216 | @staticmethod
217 | def fetchAndUpdateAllTradeOrders():
218 | allOrders = []
219 | for trade in TradeManager.trades:
220 | if trade.entryOrder != None:
221 | allOrders.append(trade.entryOrder)
222 | if trade.slOrder != None:
223 | allOrders.append(trade.slOrder)
224 | if trade.targetOrder != None:
225 | allOrders.append(trade.targetOrder)
226 |
227 | TradeManager.getOrderManager().fetchAndUpdateAllOrderDetails(allOrders)
228 |
229 | @staticmethod
230 | def trackAndUpdateAllTrades():
231 | for trade in TradeManager.trades:
232 | if trade.tradeState == TradeState.ACTIVE:
233 | TradeManager.trackEntryOrder(trade)
234 | TradeManager.trackSLOrder(trade)
235 | TradeManager.trackTargetOrder(trade)
236 | if trade.intradaySquareOffTimestamp != None:
237 | nowEpoch = Utils.getEpoch()
238 | if nowEpoch >= trade.intradaySquareOffTimestamp:
239 | TradeManager.squareOffTrade(trade, TradeExitReason.SQUARE_OFF)
240 |
241 | @staticmethod
242 | def trackEntryOrder(trade):
243 | if trade.tradeState != TradeState.ACTIVE:
244 | return
245 |
246 | if trade.entryOrder == None:
247 | return
248 |
249 | if trade.entryOrder.orderStatus == OrderStatus.CANCELLED or trade.entryOrder.orderStatus == OrderStatus.REJECTED:
250 | trade.tradeState = TradeState.CANCELLED
251 |
252 | trade.filledQty = trade.entryOrder.filledQty
253 | if trade.filledQty > 0:
254 | trade.entry = trade.entryOrder.averagePrice
255 | # Update the current market price and calculate pnl
256 | trade.cmp = TradeManager.symbolToCMPMap[trade.tradingSymbol]
257 | Utils.calculateTradePnl(trade)
258 |
259 | @staticmethod
260 | def trackSLOrder(trade):
261 | if trade.tradeState != TradeState.ACTIVE:
262 | return
263 | if trade.stopLoss == 0: # Do not place SL order if no stopLoss provided
264 | return
265 | if trade.slOrder == None:
266 | # Place SL order
267 | TradeManager.placeSLOrder(trade)
268 | else:
269 | if trade.slOrder.orderStatus == OrderStatus.COMPLETE:
270 | # SL Hit
271 | exit = trade.slOrder.averagePrice
272 | exitReason = TradeExitReason.SL_HIT if trade.initialStopLoss == trade.stopLoss else TradeExitReason.TRAIL_SL_HIT
273 | TradeManager.setTradeToCompleted(trade, exit, exitReason)
274 | # Make sure to cancel target order if exists
275 | TradeManager.cancelTargetOrder(trade)
276 |
277 | elif trade.slOrder.orderStatus == OrderStatus.CANCELLED:
278 | # SL order cancelled outside of algo (manually or by broker or by exchange)
279 | logging.error('SL order %s for tradeID %s cancelled outside of Algo. Setting the trade as completed with exit price as current market price.', trade.slOrder.orderId, trade.tradeID)
280 | exit = TradeManager.symbolToCMPMap[trade.tradingSymbol]
281 | TradeManager.setTradeToCompleted(trade, exit, TradeExitReason.SL_CANCELLED)
282 | # Cancel target order if exists
283 | TradeManager.cancelTargetOrder(trade)
284 |
285 | else:
286 | TradeManager.checkAndUpdateTrailSL(trade)
287 |
288 | @staticmethod
289 | def checkAndUpdateTrailSL(trade):
290 | # Trail the SL if applicable for the trade
291 | strategyInstance = TradeManager.strategyToInstanceMap[trade.strategy]
292 | if strategyInstance == None:
293 | return
294 |
295 | newTrailSL = strategyInstance.getTrailingSL(trade)
296 | updateSL = False
297 | if newTrailSL > 0:
298 | if trade.direction == Direction.LONG and newTrailSL > trade.stopLoss:
299 | updateSL = True
300 | elif trade.direction == Direction.SHORT and newTrailSL < trade.stopLoss:
301 | updateSL = True
302 | if updateSL == True:
303 | omp = OrderModifyParams()
304 | omp.newTriggerPrice = newTrailSL
305 | try:
306 | oldSL = trade.stopLoss
307 | TradeManager.getOrderManager().modifyOrder(trade.slOrder, omp)
308 | logging.info('TradeManager: Trail SL: Successfully modified stopLoss from %f to %f for tradeID %s', oldSL, newTrailSL, trade.tradeID)
309 | trade.stopLoss = newTrailSL # IMPORTANT: Dont forget to update this on successful modification
310 | except Exception as e:
311 | logging.error('TradeManager: Failed to modify SL order for tradeID %s orderId %s: Error => %s', trade.tradeID, trade.slOrder.orderId, str(e))
312 |
313 | @staticmethod
314 | def trackTargetOrder(trade):
315 | if trade.tradeState != TradeState.ACTIVE:
316 | return
317 | if trade.target == 0: # Do not place Target order if no target provided
318 | return
319 | if trade.targetOrder == None:
320 | # Place Target order
321 | TradeManager.placeTargetOrder(trade)
322 | else:
323 | if trade.targetOrder.orderStatus == OrderStatus.COMPLETE:
324 | # Target Hit
325 | exit = trade.targetOrder.averagePrice
326 | TradeManager.setTradeToCompleted(trade, exit, TradeExitReason.TARGET_HIT)
327 | # Make sure to cancel sl order
328 | TradeManager.cancelSLOrder(trade)
329 |
330 | elif trade.targetOrder.orderStatus == OrderStatus.CANCELLED:
331 | # Target order cancelled outside of algo (manually or by broker or by exchange)
332 | logging.error('Target order %s for tradeID %s cancelled outside of Algo. Setting the trade as completed with exit price as current market price.', trade.targetOrder.orderId, trade.tradeID)
333 | exit = TradeManager.symbolToCMPMap[trade.tradingSymbol]
334 | TradeManager.setTradeToCompleted(trade, exit, TradeExitReason.TARGET_CANCELLED)
335 | # Cancel SL order
336 | TradeManager.cancelSLOrder(trade)
337 |
338 | @staticmethod
339 | def placeSLOrder(trade):
340 | oip = OrderInputParams(trade.tradingSymbol)
341 | oip.direction = Direction.SHORT if trade.direction == Direction.LONG else Direction.LONG
342 | oip.productType = trade.productType
343 | oip.orderType = OrderType.SL_MARKET
344 | oip.triggerPrice = trade.stopLoss
345 | oip.qty = trade.qty
346 | if trade.isFutures == True or trade.isOptions == True:
347 | oip.isFnO = True
348 | try:
349 | trade.slOrder = TradeManager.getOrderManager().placeOrder(oip)
350 | except Exception as e:
351 | logging.error('TradeManager: Failed to place SL order for tradeID %s: Error => %s', trade.tradeID, str(e))
352 | return False
353 | logging.info('TradeManager: Successfully placed SL order %s for tradeID %s', trade.slOrder.orderId, trade.tradeID)
354 | return True
355 |
356 | @staticmethod
357 | def placeTargetOrder(trade, isMarketOrder = False):
358 | oip = OrderInputParams(trade.tradingSymbol)
359 | oip.direction = Direction.SHORT if trade.direction == Direction.LONG else Direction.LONG
360 | oip.productType = trade.productType
361 | oip.orderType = OrderType.MARKET if isMarketOrder == True else OrderType.LIMIT
362 | oip.price = 0 if isMarketOrder == True else trade.target
363 | oip.qty = trade.qty
364 | if trade.isFutures == True or trade.isOptions == True:
365 | oip.isFnO = True
366 | try:
367 | trade.targetOrder = TradeManager.getOrderManager().placeOrder(oip)
368 | except Exception as e:
369 | logging.error('TradeManager: Failed to place Target order for tradeID %s: Error => %s', trade.tradeID, str(e))
370 | return False
371 | logging.info('TradeManager: Successfully placed Target order %s for tradeID %s', trade.targetOrder.orderId, trade.tradeID)
372 | return True
373 |
374 | @staticmethod
375 | def cancelEntryOrder(trade):
376 | if trade.entryOrder == None:
377 | return
378 | if trade.entryOrder.orderStatus == OrderStatus.CANCELLED:
379 | return
380 | try:
381 | TradeManager.getOrderManager().cancelOrder(trade.entryOrder)
382 | except Exception as e:
383 | logging.error('TradeManager: Failed to cancel Entry order %s for tradeID %s: Error => %s', trade.entryOrder.orderId, trade.tradeID, str(e))
384 | logging.info('TradeManager: Successfully cancelled Entry order %s for tradeID %s', trade.entryOrder.orderId, trade.tradeID)
385 |
386 | @staticmethod
387 | def cancelSLOrder(trade):
388 | if trade.slOrder == None:
389 | return
390 | if trade.slOrder.orderStatus == OrderStatus.CANCELLED:
391 | return
392 | try:
393 | TradeManager.getOrderManager().cancelOrder(trade.slOrder)
394 | except Exception as e:
395 | logging.error('TradeManager: Failed to cancel SL order %s for tradeID %s: Error => %s', trade.slOrder.orderId, trade.tradeID, str(e))
396 | logging.info('TradeManager: Successfully cancelled SL order %s for tradeID %s', trade.slOrder.orderId, trade.tradeID)
397 |
398 | @staticmethod
399 | def cancelTargetOrder(trade):
400 | if trade.targetOrder == None:
401 | return
402 | if trade.targetOrder.orderStatus == OrderStatus.CANCELLED:
403 | return
404 | try:
405 | TradeManager.getOrderManager().cancelOrder(trade.targetOrder)
406 | except Exception as e:
407 | logging.error('TradeManager: Failed to cancel Target order %s for tradeID %s: Error => %s', trade.targetOrder.orderId, trade.tradeID, str(e))
408 | logging.info('TradeManager: Successfully cancelled Target order %s for tradeID %s', trade.targetOrder.orderId, trade.tradeID)
409 |
410 | @staticmethod
411 | def setTradeToCompleted(trade, exit, exitReason = None):
412 | trade.tradeState = TradeState.COMPLETED
413 | trade.exit = exit
414 | trade.exitReason = exitReason if trade.exitReason == None else trade.exitReason
415 | trade.endTimestamp = Utils.getEpoch()
416 | trade = Utils.calculateTradePnl(trade)
417 | logging.info('TradeManager: setTradeToCompleted strategy = %s, symbol = %s, qty = %d, entry = %f, exit = %f, pnl = %f, exit reason = %s', trade.strategy, trade.tradingSymbol, trade.filledQty, trade.entry, trade.exit, trade.pnl, trade.exitReason)
418 |
419 | @staticmethod
420 | def squareOffTrade(trade, reason = TradeExitReason.SQUARE_OFF):
421 | logging.info('TradeManager: squareOffTrade called for tradeID %s with reason %s', trade.tradeID, reason)
422 | if trade == None or trade.tradeState != TradeState.ACTIVE:
423 | return
424 |
425 | trade.exitReason = reason
426 | if trade.entryOrder != None:
427 | if trade.entryOrder.orderStatus == OrderStatus.OPEN:
428 | # Cancel entry order if it is still open (not filled or partially filled case)
429 | TradeManager.cancelEntryOrder(trade)
430 |
431 | if trade.slOrder != None:
432 | TradeManager.cancelSLOrder(trade)
433 |
434 | if trade.targetOrder != None:
435 | # Change target order type to MARKET to exit position immediately
436 | logging.info('TradeManager: changing target order %s to MARKET to exit position for tradeID %s', trade.targetOrder.orderId, trade.tradeID)
437 | TradeManager.getOrderManager().modifyOrderToMarket(trade.targetOrder)
438 | else:
439 | # Place new target order to exit position
440 | logging.info('TradeManager: placing new target order to exit position for tradeID %s', trade.tradeID)
441 | TradeManager.placeTargetOrder(trade, True)
442 |
443 | @staticmethod
444 | def getOrderManager():
445 | orderManager = None
446 | brokerName = Controller.getBrokerName()
447 | if brokerName == "zerodha":
448 | orderManager = ZerodhaOrderManager()
449 | #elif brokerName == "fyers": # Not implemented
450 | return orderManager
451 |
452 | @staticmethod
453 | def getNumberOfTradesPlacedByStrategy(strategy):
454 | count = 0
455 | for trade in TradeManager.trades:
456 | if trade.strategy != strategy:
457 | continue
458 | if trade.tradeState == TradeState.CREATED or trade.tradeState == TradeState.DISABLED:
459 | continue
460 | # consider active/completed/cancelled trades as trades placed
461 | count += 1
462 | return count
463 |
464 | @staticmethod
465 | def getAllTradesByStrategy(strategy):
466 | tradesByStrategy = []
467 | for trade in TradeManager.trades:
468 | if trade.strategy == strategy:
469 | tradesByStrategy.append(trade)
470 | return tradesByStrategy
471 |
472 | @staticmethod
473 | def convertJSONToTrade(jsonData):
474 | trade = Trade(jsonData['tradingSymbol'])
475 | trade.tradeID = jsonData['tradeID']
476 | trade.strategy = jsonData['strategy']
477 | trade.direction = jsonData['direction']
478 | trade.productType = jsonData['productType']
479 | trade.isFutures = jsonData['isFutures']
480 | trade.isOptions = jsonData['isOptions']
481 | trade.optionType = jsonData['optionType']
482 | trade.placeMarketOrder = jsonData['placeMarketOrder']
483 | trade.intradaySquareOffTimestamp = jsonData['intradaySquareOffTimestamp']
484 | trade.requestedEntry = jsonData['requestedEntry']
485 | trade.entry = jsonData['entry']
486 | trade.qty = jsonData['qty']
487 | trade.filledQty = jsonData['filledQty']
488 | trade.initialStopLoss = jsonData['initialStopLoss']
489 | trade.stopLoss = jsonData['stopLoss']
490 | trade.target = jsonData['target']
491 | trade.cmp = jsonData['cmp']
492 | trade.tradeState = jsonData['tradeState']
493 | trade.timestamp = jsonData['timestamp']
494 | trade.createTimestamp = jsonData['createTimestamp']
495 | trade.startTimestamp = jsonData['startTimestamp']
496 | trade.endTimestamp = jsonData['endTimestamp']
497 | trade.pnl = jsonData['pnl']
498 | trade.pnlPercentage = jsonData['pnlPercentage']
499 | trade.exit = jsonData['exit']
500 | trade.exitReason = jsonData['exitReason']
501 | trade.exchange = jsonData['exchange']
502 | trade.entryOrder = TradeManager.convertJSONToOrder(jsonData['entryOrder'])
503 | trade.slOrder = TradeManager.convertJSONToOrder(jsonData['slOrder'])
504 | trade.targetOrder = TradeManager.convertJSONToOrder(jsonData['targetOrder'])
505 | return trade
506 |
507 | @staticmethod
508 | def convertJSONToOrder(jsonData):
509 | if jsonData == None:
510 | return None
511 | order = Order()
512 | order.tradingSymbol = jsonData['tradingSymbol']
513 | order.exchange = jsonData['exchange']
514 | order.productType = jsonData['productType']
515 | order.orderType = jsonData['orderType']
516 | order.price = jsonData['price']
517 | order.triggerPrice = jsonData['triggerPrice']
518 | order.qty = jsonData['qty']
519 | order.orderId = jsonData['orderId']
520 | order.orderStatus = jsonData['orderStatus']
521 | order.averagePrice = jsonData['averagePrice']
522 | order.filledQty = jsonData['filledQty']
523 | order.pendingQty = jsonData['pendingQty']
524 | order.orderPlaceTimestamp = jsonData['orderPlaceTimestamp']
525 | order.lastOrderUpdateTimestamp = jsonData['lastOrderUpdateTimestamp']
526 | order.message = jsonData['message']
527 | return order
528 |
529 | @staticmethod
530 | def getLastTradedPrice(tradingSymbol):
531 | return TradeManager.symbolToCMPMap[tradingSymbol]
532 |
--------------------------------------------------------------------------------