├── .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 | --------------------------------------------------------------------------------