import asyncio
from optrabot import config, crud, schemas
from optrabot.config import Config
from contextlib import suppress
import datetime as dt
from datetime import datetime
from ib_async import *
import json
from loguru import logger
import httpx
from typing import Any, List
from fastapi import status
from sqlalchemy.orm import Session
import optrabot.config as optrabotcfg
from optrabot.database import get_db_engine
from optrabot.signaldata import SignalData
from optrabot.tradehelper import TradeHelper
from optrabot.optionhelper import OptionHelper
from optrabot.stoplossadjuster import StopLossAdjuster
from optrabot.tradetemplate.processor.templateprocessor import TemplateProcessor
#from optrabot.tradetemplate.template import Template
from optrabot.tradetemplate.templatefactory import Template
from optrabot.tradetemplate.templatetrigger import TriggerType
from optrabot.util.singletonmeta import SingletonMeta


class TradinghubClient(metaclass=SingletonMeta):
	def __init__(self, optraBot):
		logger.debug("TradinghubClient Init")
		self._lastAnswerReceivedAt = None
		self._config = optrabotcfg.appConfig
		self.optraBot = optraBot
		if self.optraBot:
			self._config : Config = self.optraBot['config']
		else:
			self._config = Config()
		self._instanceId = ''
		self.hub_host = ''
		self._contracts: int = 0
		try:
			self._contracts = int(self._config.get('tws.contracts'))
		except KeyError as keyErr:
			self._contracts = 1

		try:
			self.hub_host = self._config.get('general.hub')
		except KeyError as keyError:
			self.hub_host = config.defaultHubHost
			logger.warning("No Hub URL is configured in config.yaml. Using the default.")

		try:
			self._apiKey = self._config.get('general.apikey')
		except KeyError as keyError:
			logger.error("No API Key is configured in config.yaml. Stopping here!")
			self._apiKey = None

		try:
			self._instanceId = self._config.getInstanceId()
			logger.info("Running with Instance ID '{}'.", self._instanceId)
		except KeyError as keyError:
			self._instanceId = None

		self._entryTrade = None
		self._entryTradeContract = None
		self._currentTemplate = None
		self._currentTrade = None
		self._slShortTrade = None
		self._tpTrade = None
		self._ironFlyAskPrice = 0.0
		self._ironFlyComboContract = None
		self._ironFlyContracts = None
		self._ironFlyLongLegContracts = None
		self._ironFlyShortComboContract = None
		self._longLegFillsPrice = 0.0
		self._longLegFillsReceived = 0
		self._previousIronFlyPrice = 0.0
		self._unchangedIronFlyPriceCounter = 0
		self._shuttingdown = False
		#self._positionMonitorTask = None
		self._position = False
		self._closingLongLegs = False
		self._fillTransactionMap = {}
		self._entryOrderFilled = 0   # Number of fills for the entry order
		
	async def shutdown(self):
		logger.info('Shutting down Trading Hub Client.')
		self._shuttingdown = True
		#self._stopPositionMonitoring()

	async def reportAction(self, action: str, additional_data: str = '') -> bool:
		"""
		Reports a livecycle action and relevant details to the OptraBot Hub and check if it is allowed.
		- Instance with the same id must not be running already
		- A subscription must be active
		- The configured account numbers must not be used by another registered user already

		Returns true if the action was reported successfully, otherwise false.
		"""
		data = {
			'action': action,
			'instance_id': self._instanceId,
			'additional_data': additional_data
		}
		headers = {'X-API-Key': self._apiKey, 'X-Version': self.optraBot.Version, "Content-Type": "application/json"}
		try:
			response = httpx.post(self.hub_host + '/report_action', json=data, headers=headers)
		except Exception as excp:
			raise excp
		
		if response.status_code != status.HTTP_200_OK:
			jsonData = response.json()
			if jsonData['detail']:
				errorMessage = jsonData['detail']
			else:
				errorMessage = response.reason_phrase
			raise Exception(errorMessage)

	async def _poll(self):
		try:
			fetch_url = self.hub_host + '/fetch_signal'
			url_params = {'instanceid': self._instanceId}
			headers = {'X-API-Key': self._apiKey, 'X-Version': self.optraBot.Version}
			if self._shuttingdown:
				logger.debug("Client Session closed. Stop polling.")
				return
			
			logger.debug('Checking for Signal from Hub.')
			response = httpx.get(fetch_url, params=url_params, follow_redirects=True, headers=headers)
			logger.debug('Answer received ({}).', response.status_code)
			self._pollTimeouts = 0
			if response.status_code != status.HTTP_200_OK:
				if response.status_code == status.HTTP_401_UNAUTHORIZED:
					logger.error("Error requesting signal from OptraBot Hub: {}", response.json()['detail'])
				else:
					logger.error("Error on HTTP request: {}", response.reason_phrase)
				return
			
			self._lastAnswerReceivedAt = datetime.now()
			if response.text != '\"\"' and response.text != '':
				logger.debug("Response {}", response.content )

				try:
					response_data = json.loads(response.content)
				except json.JSONDecodeError as jsonExcp:
					logger.error("Didn't receive JSON data!")
					return
				
				# Check if the signal is valid and configured
				signalStrategy = response_data['strategy']
				logger.debug('Received Signal: {}', response_data['strategy'])
				templateProcessor = TemplateProcessor()
				triggeredTemplate = templateProcessor.determineTemplate(signalStrategy)
				#for template in self._config.getTemplates():
				#	trigger = template.getTrigger()
				#	if trigger.type == TriggerType.External and trigger.value == response_data['strategy']:
				#		triggeredTemplate = template
				#		break
				if triggeredTemplate == None:
					logger.warning('No template for external trigger value {} configured!', response_data['strategy'])
					return
				
				signalData = SignalData(timestamp=self._parseTimestamp(response_data['time']), close=response_data['close'], strike=0 )
				
				# Use the VWAP target as strike price for the Iron Fly and the Lunch Iron Fly
				if signalStrategy == '0DTEIronFly' or signalStrategy == '0DTELunchIF':
					signalData.strike = response_data['vwaptarget']
				else:
					signalData.strike = response_data['shortstrike']

				#templateProcessor.processTemplate(triggeredTemplate, signalData)
			
				#signalTime = self._parseTimestamp(response_data['time'])
				#if signalTime == None or self._signalIsOutdated(signalTime):
				#	logger.warning('Signal is outdated already or Signal timestamp is invalid!')
				#	return
				
				# Check if there is no positions open already
				#if self._position != False:
				#	logger.warning('Skipping signal, because there is a position open already!')
				#	return
				
				# Obtain the Template Executor and schedule a job to process the signal
				#await self.optraBot.executeTradeTemplate(triggeredTemplate)
				self.optraBot._backgroundScheduler.add_job(templateProcessor.processTemplate, args=[triggeredTemplate, signalData], id='processtemplate')
				return
		except httpx.TimeoutException as timeoutException:
			logger.debug("Timeout while fetching Signal from Hub: {}", timeoutException)
			self._pollTimeouts += 1
			if self._pollTimeouts > 3:
				logger.error("Too many timeouts while fetching Signal from Hub. Please check the connection!")
				return
		except Exception as anyEcxp:
			logger.error("Exception occured during poll: {}", anyEcxp)

	async def excuteTradeTemplate(self, template: Template, response_data: dict):
		""" This method is called via Job Scheduler for executing the given Trade Template in the background.
		"""
		logger.debug(f"Excuting Trade Template {template.name}")
		try:
			ib: IB = self.optraBot['ib']
			if not ib.isConnected():
				logger.error("Interactive Brokers is not connected. Unable to process received signal!")
				return
			
			if not self.optraBot.isTradingEnabled():
				logger.error("Trading is not enabled. Looks like your Market Data Subscription is wrong. Skippimg this Trade!")
				return
			
			spx = Index('SPX', 'CBOE')
			qualifiedContracts = await ib.qualifyContractsAsync(spx) 
			[ticker] = await ib.reqTickersAsync(spx)
			spxValue = ticker.marketPrice()
			#self.app['SPXPrice'] = spxValue
			logger.debug("SPX Market Price {}", spxValue)

			chains = await ib.reqSecDefOptParamsAsync(spx.symbol, '', spx.secType, spx.conId)
			chain = next(c for c in chains if c.tradingClass == 'SPXW' and c.exchange == 'SMART')
			if chain == None:
				logger.error("No Option Chain for SPXW and CBOE found! Doing no trade!")
				return
			
			
			# Options Kontrakte ermitteln
			nan = float('nan')
			self._currentTemplate = template
			self._currentTemplate.resetStopLossAdjuster()
			if not self._currentTemplate.wing:
				wingSize = 70
			else:
				wingSize = self._currentTemplate.wing
			amount = self._currentTemplate.amount
			accountNo = self._currentTemplate.account
			current_date = dt.date.today()
			expiration = current_date.strftime('%Y%m%d')
			shortLegStrike = float(response_data['vwaptarget'])
			longPutStrike = shortLegStrike - wingSize
			longCallStrike = shortLegStrike + wingSize
			logger.info("Building Iron Fly combo with Short strike {}, Long Put strike {} and Long Call strike {}", shortLegStrike, longPutStrike, longCallStrike)
			
			# Überprüfen das auf den geplanten Legs keine anderen Orders liegen
			logger.debug('Checking for open Trades if Combo Legs are colliding')
			contractsWithOpenOrders = await self._getAllOpenOrderContracts(accountNo, spx.symbol)

			shortPutContract = Option(spx.symbol, expiration, shortLegStrike, 'P', 'SMART', tradingClass = 'SPXW')
			await ib.qualifyContractsAsync(shortPutContract)
			if not OptionHelper.checkContractIsQualified(shortPutContract):
				return
			if shortPutContract.conId in contractsWithOpenOrders:
				logger.error('Existing open order at stike price {}. Trade cannot be placed!', shortLegStrike)
				return

			shortCallContract = Option(spx.symbol, expiration, shortLegStrike, 'C', 'SMART', tradingClass = 'SPXW')				
			await ib.qualifyContractsAsync(shortCallContract)
			if not OptionHelper.checkContractIsQualified(shortCallContract):
				return
			if shortCallContract.conId in contractsWithOpenOrders:
				logger.error('Existing open order at strike price {}. Trade cannot be placed!', shortLegStrike)
				return
			
			longPutContract = await self._DetermineValidLongOption(spx.symbol, expiration, longPutStrike, 'P', contractsWithOpenOrders)
			if longPutContract == None or not OptionHelper.checkContractIsQualified(longPutContract):
				logger.error('Unable to determine a valid Long Put option for the trade!')
				return
			if longPutContract.strike != longPutStrike:
				logger.warning('Using different Long Put strike {}, because of existing open orders on desired strike {}.', longPutContract.strike, longPutStrike)

			longCallContract = await self._DetermineValidLongOption(spx.symbol, expiration, longCallStrike, 'C', contractsWithOpenOrders)
			if longCallContract == None or not OptionHelper.checkContractIsQualified(longCallContract):
				logger.error('Unable to determine a valid Long Call option for the trade!')
				return
			if longCallContract.strike != longCallStrike:
				logger.warning('Using different Long Call strike {}, because of existing open orders on desired strike {}.', longCallContract.strike, longCallStrike)

			self._ironFlyLongLegContracts = [longCallContract, longPutContract]
			self._ironFlyContracts = [shortPutContract, shortCallContract, longPutContract, longCallContract]
			self._ironFlyComboContract = Contract(symbol=spx.symbol, secType='BAG', exchange='SMART', currency='USD', comboLegs=[])
			self._ironFlyShortComboContract = Contract(symbol=spx.symbol, secType='BAG', exchange='SMART', currency='USD', comboLegs=[])
			ironFlyMidPrice = None
			for i in range(5):
				tickers = await ib.reqTickersAsync(*self._ironFlyContracts)
				logger.debug("Tickers {}", tickers)
				ironFlyMidPrice = self._calculateIronFlyMidPrice(tickers, ironFlyComboContract=self._ironFlyComboContract, 
											   ironFlyShortComboContract=self._ironFlyShortComboContract, 
											   shortPutContract=shortPutContract,
											   shortCallContract=shortCallContract,
											   longPutContract=longPutContract,
											   longCallContract=longCallContract)
				if ironFlyMidPrice != None:
					break
				await asyncio.sleep(1)
			
			#if util.isNan(ironFlyMidPrice):
			if ironFlyMidPrice == None:
				logger.error("No Mid Price for combo could be calculated!")
				return

			limitPrice = ironFlyMidPrice

			logger.info("IronFly Combo Mid Price: {} Ask Price: {}", ironFlyMidPrice, self._ironFlyAskPrice)

			if not self._meetsMinimumPremium(limitPrice):
				logger.info('Premium below configured minimum premium of ${}. Trade is not executed!', self._currentTemplate.minPremium)
				return
			
			order = LimitOrder('BUY', amount, limitPrice)
			with Session(get_db_engine()) as session:
				newTrade = schemas.TradeCreate(account=accountNo, symbol='SPX', strategy=self._currentTemplate.strategy)
				self._currentTrade = crud.create_trade(session, newTrade)
				order.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + template.name + ' - Open'
			order.account = accountNo
			order.outsideRth = True
			self._fillTransactionMap.clear()
			self._entryTrade = ib.placeOrder(self._ironFlyComboContract, order)
			self._entryTradeContract = self._ironFlyComboContract
			self._ironFlyAskPrice = 0.0
			self._longLegFillsPrice = 0.0
			self._previousIronFlyPrice = 0.0
			self._unchangedIronFlyPriceCounter = 0
			self._longLegFillsReceived = 0
			self._slShortTrade = None
			self._tpTrade = None
			self._entryOrderFilled = 0
			self._closingLongLegs = False
			logger.debug("Account: {} Trade placed: {} Number of contracts: {}", order.account, self._entryTrade, amount)
			self.optraBot._backgroundScheduler.add_job(self._trackEntryOrder, 'interval', seconds=5, id='TrackEntryOrder')

		except Exception as excp:
						logger.error("Exception: {}", excp)

	# async def _poll_old(self):
	# 	try:
	# 		fetch_url = self.hub_host + '/fetch_signal'
	# 		url_params = {'agentid': self._instanceId}
	# 		headers = {'X-API-Key': self._apiKey, 'X-Version': self.optraBot.Version}
	# 		if self._shuttingdown:
	# 			logger.debug("Client Session closed. Stop polling.")
	# 			return
			
	# 		logger.debug('Checking for Signal from Hub.')
	# 		response = httpx.get(fetch_url, params=url_params, follow_redirects=True, headers=headers)
	# 		logger.debug('Answer received ({}).', response.status_code)
	# 		if response.status_code != status.HTTP_200_OK:
	# 			if response.status_code == status.HTTP_401_UNAUTHORIZED:
	# 				logger.error("Error requesting signal from OptraBot Hub: {}", response.json()['detail'])
	# 			else:
	# 				logger.error("Error on HTTP request: {}", response.reason_phrase)
	# 			await self._scheduleNextPoll()
	# 			return

	# 		self._lastAnswerReceivedAt = datetime.now()
	# 		if response.text != '\"\"' and response.text != '':
	# 			logger.debug("Response {}", response.content )

	# 			try:
	# 				response_data = json.loads(response.content)
	# 			except json.JSONDecodeError as jsonExcp:
	# 				logger.error("Didn't receive JSON data!")
	# 				await self._scheduleNextPoll()
	# 				return

	# 			# Check if the signal is valid and configured
	# 			logger.debug('Received Signal: {}', response_data['strategy'])
	# 			triggeredTemplate = None
	# 			for template in self._config.getTemplates():
	# 				trigger = template.getTrigger()
	# 				if trigger.type == TriggerType.External and trigger.value == response_data['strategy']:
	# 					triggeredTemplate = template
	# 					break
	# 			if triggeredTemplate == None:
	# 				logger.warning('No template for external trigger value {} configured!', response_data['strategy'])
	# 				await self._scheduleNextPoll()
	# 				return

	# 			signalTime = self._parseTimestamp(response_data['time'])
	# 			if signalTime == None or self._signalIsOutdated(signalTime):
	# 				logger.warning('Signal is outdated already or Signal timestamp is invalid!')
	# 				await self._scheduleNextPoll()
	# 				return
				
	# 			# Check if there is no positions open already
	# 			if self._position != False:
	# 				logger.warning('Skipping signal, because there is a position open already!')
	# 				await self._scheduleNextPoll()
	# 				return

	# 			try:
	# 				ib: IB = self.optraBot['ib']
	# 				if not ib.isConnected():
	# 					logger.error("Interactive Brokers is not connected. Unable to process received signal!")
	# 					await self._scheduleNextPoll()
	# 					return
					
	# 				if not self.optraBot.isTradingEnabled():
	# 					logger.error("Trading is not enabled. Looks like your Market Data Subscription is wrong. Skippimg this Trade!")
	# 					await self._scheduleNextPoll()
	# 					return

	# 				spx = Index('SPX', 'CBOE')
	# 				qualifiedContracts = await ib.qualifyContractsAsync(spx) 
	# 				[ticker] = await ib.reqTickersAsync(spx)
	# 				spxValue = ticker.marketPrice()
	# 				#self.app['SPXPrice'] = spxValue
	# 				logger.debug("SPX Market Price {}", spxValue)

	# 				chains = await ib.reqSecDefOptParamsAsync(spx.symbol, '', spx.secType, spx.conId)
	# 				chain = next(c for c in chains if c.tradingClass == 'SPXW' and c.exchange == 'SMART')
	# 				if chain == None:
	# 					logger.error("No Option Chain for SPXW and CBOE found! Doing no trade!")
	# 					await self._scheduleNextPoll()
	# 					return
					
	# 				# Options Kontrakte ermitteln
	# 				nan = float('nan')
	# 				self._currentTemplate = triggeredTemplate
	# 				self._currentTemplate.resetStopLossAdjuster()
	# 				if not self._currentTemplate.wing:
	# 					wingSize = 70
	# 				else:
	# 					wingSize = self._currentTemplate.wing
	# 				amount = self._currentTemplate.amount
	# 				accountNo = self._currentTemplate.account
	# 				current_date = dt.date.today()
	# 				expiration = current_date.strftime('%Y%m%d')
	# 				shortLegStrike = float(response_data['vwaptarget'])
	# 				longPutStrike = shortLegStrike - wingSize
	# 				longCallStrike = shortLegStrike + wingSize
	# 				logger.info("Building Iron Fly combo with Short strike {}, Long Put strike {} and Long Call strike {}", shortLegStrike, longPutStrike, longCallStrike)
					
	# 				# Überprüfen das auf den geplanten Legs keine anderen Orders liegen
	# 				logger.debug('Checking for open Trades if Combo Legs are colliding')
	# 				contractsWithOpenOrders = await self._getAllOpenOrderContracts(accountNo, spx.symbol)

	# 				shortPutContract = Option(spx.symbol, expiration, shortLegStrike, 'P', 'SMART', tradingClass = 'SPXW')
	# 				await ib.qualifyContractsAsync(shortPutContract)
	# 				if not OptionHelper.checkContractIsQualified(shortPutContract):
	# 					await self._scheduleNextPoll()
	# 					return
	# 				if shortPutContract.conId in contractsWithOpenOrders:
	# 					logger.error('Existing open order at stike price {}. Trade cannot be placed!', shortLegStrike)
	# 					await self._scheduleNextPoll()
	# 					return

	# 				shortCallContract = Option(spx.symbol, expiration, shortLegStrike, 'C', 'SMART', tradingClass = 'SPXW')				
	# 				await ib.qualifyContractsAsync(shortCallContract)
	# 				if not OptionHelper.checkContractIsQualified(shortCallContract):
	# 					await self._scheduleNextPoll()
	# 					return
	# 				if shortCallContract.conId in contractsWithOpenOrders:
	# 					logger.error('Existing open order at strike price {}. Trade cannot be placed!', shortLegStrike)
	# 					await self._scheduleNextPoll()
	# 					return
					
	# 				longPutContract = await self._DetermineValidLongOption(spx.symbol, expiration, longPutStrike, 'P', contractsWithOpenOrders)
	# 				if longPutContract == None or not OptionHelper.checkContractIsQualified(longPutContract):
	# 					logger.error('Unable to determine a valid Long Put option for the trade!')
	# 					await self._scheduleNextPoll()
	# 					return
	# 				if longPutContract.strike != longPutStrike:
	# 					logger.warning('Using different Long Put strike {}, because of existing open orders on desired strike {}.', longPutContract.strike, longPutStrike)

	# 				longCallContract = await self._DetermineValidLongOption(spx.symbol, expiration, longCallStrike, 'C', contractsWithOpenOrders)
	# 				if longCallContract == None or not OptionHelper.checkContractIsQualified(longCallContract):
	# 					logger.error('Unable to determine a valid Long Call option for the trade!')
	# 					await self._scheduleNextPoll()
	# 					return
	# 				if longCallContract.strike != longCallStrike:
	# 					logger.warning('Using different Long Call strike {}, because of existing open orders on desired strike {}.', longCallContract.strike, longCallStrike)

	# 				self._ironFlyLongLegContracts = [longCallContract, longPutContract]
	# 				self._ironFlyContracts = [shortPutContract, shortCallContract, longPutContract, longCallContract]
	# 				self._ironFlyComboContract = Contract(symbol=spx.symbol, secType='BAG', exchange='SMART', currency='USD', comboLegs=[])
	# 				self._ironFlyShortComboContract = Contract(symbol=spx.symbol, secType='BAG', exchange='SMART', currency='USD', comboLegs=[])

	# 				ironFlyMidPrice = None
	# 				for i in range(5):
	# 					tickers = await ib.reqTickersAsync(*self._ironFlyContracts)
	# 					logger.debug("Tickers {}", tickers)
	# 					ironFlyMidPrice = self._calculateIronFlyMidPrice(tickers, ironFlyComboContract=self._ironFlyComboContract, 
	# 												   ironFlyShortComboContract=self._ironFlyShortComboContract, 
	# 												   shortPutContract=shortPutContract,
	# 												   shortCallContract=shortCallContract,
	# 												   longPutContract=longPutContract,
	# 												   longCallContract=longCallContract)
	# 					if ironFlyMidPrice != None:
	# 						break
	# 					await asyncio.sleep(1)
					
	# 				#if util.isNan(ironFlyMidPrice):
	# 				if ironFlyMidPrice == None:
	# 					logger.error("No Mid Price for combo could be calculated!")
	# 					await self._scheduleNextPoll()
	# 					return

	# 				limitPrice = ironFlyMidPrice

	# 				logger.info("IronFly Combo Mid Price: {} Ask Price: {}", ironFlyMidPrice, self._ironFlyAskPrice)

	# 				if not self._meetsMinimumPremium(limitPrice):
	# 					logger.info('Premium below configured minimum premium of ${}. Trade is not executed!', self._currentTemplate.minPremium)
	# 				else:
	# 					order = LimitOrder('BUY', amount, limitPrice)
	# 					with Session(get_db_engine()) as session:
	# 						newTrade = schemas.TradeCreate(account=accountNo, symbol='SPX', strategy=self._currentTemplate.strategy)
	# 						self._currentTrade = crud.create_trade(session, newTrade)
	# 						order.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + triggeredTemplate.name + ' - Open'
	# 					order.account = accountNo
	# 					order.outsideRth = True
	# 					self._fillTransactionMap.clear()
	# 					self._entryTrade = ib.placeOrder(self._ironFlyComboContract, order)
	# 					self._entryTradeContract = self._ironFlyComboContract
	# 					self._ironFlyAskPrice = 0.0
	# 					self._longLegFillsPrice = 0.0
	# 					self._longLegFillsReceived = 0
	# 					self._slShortTrade = None
	# 					self._tpTrade = None
	# 					self._entryOrderFilled = 0
	# 					self._closingLongLegs = False
	# 					logger.debug("Account: {} Trade placed: {} Number of contracts: {}", order.account, self._entryTrade, amount)

	# 			except Exception as excp:
	# 					logger.error("Exception: {}", excp)

	# 	except Exception as anyEcxp:
	# 		logger.error("Exception occured during poll: {}", anyEcxp)

	# 	await self._scheduleNextPoll()

	def _calculateIronFlyMidPrice(self, tickers: List[Ticker], ironFlyComboContract: Contract, ironFlyShortComboContract: Contract, shortPutContract: Option, shortCallContract: Option, longPutContract: Option, longCallContract: Option) -> float:
		"""
		Calculate the Iron Fly Mid Price based on the given tickers. It returns
		None if one of the legs got no valid price.
		"""
		ironFlyMidPrice = 0
		self._ironFlyAskPrice = 0
		ironFlyComboContract.comboLegs.clear()
		ironFlyShortComboContract.comboLegs.clear()
		for ticker in tickers:
			tickerContract = ticker.contract
			if tickerContract.conId == shortPutContract.conId:
				midPrice = (ticker.ask + ticker.bid) / 2
				if util.isNan(midPrice) or (ticker.ask == -1.00 and ticker.bid == -1.00):
					return None
				ironFlyMidPrice -= midPrice
				if not util.isNan(ticker.bid):
					self._ironFlyAskPrice -= ticker.bid
					ironFlyComboContract.comboLegs.append(ComboLeg(conId=shortPutContract.conId, ratio=1, action='SELL', exchange='SMART'))
					ironFlyShortComboContract.comboLegs.append(ComboLeg(conId=shortPutContract.conId, ratio=1, action='BUY', exchange='SMART'))
			if tickerContract.conId == shortCallContract.conId:
				midPrice = (ticker.ask + ticker.bid) / 2
				if util.isNan(midPrice) or (ticker.ask == -1.00 and ticker.bid == -1.00):
					return None
				ironFlyMidPrice -= midPrice
				if not util.isNan(ticker.bid):
					self._ironFlyAskPrice -= ticker.bid
					ironFlyComboContract.comboLegs.append(ComboLeg(conId=shortCallContract.conId, ratio=1, action='SELL', exchange='SMART'))
					ironFlyShortComboContract.comboLegs.append(ComboLeg(conId=shortCallContract.conId, ratio=1, action='BUY', exchange='SMART'))
			if tickerContract.conId == longPutContract.conId:
				midPrice = (ticker.ask + ticker.bid) / 2
				if util.isNan(midPrice) or (ticker.ask == -1.00 and ticker.bid == -1.00):
					midPrice = 0.05
				ironFlyMidPrice += midPrice
				if not util.isNan(ticker.ask):
					self._ironFlyAskPrice += ticker.ask
					ironFlyComboContract.comboLegs.append(ComboLeg(conId=longPutContract.conId, ratio=1, action='BUY', exchange='SMART'))
			if tickerContract.conId == longCallContract.conId:
				midPrice = (ticker.ask + ticker.bid) / 2
				if util.isNan(midPrice) or (ticker.ask == -1.00 and ticker.bid == -1.00):
					midPrice = 0.05
				ironFlyMidPrice += midPrice
				if not util.isNan(ticker.ask):
					self._ironFlyAskPrice += ticker.ask
					ironFlyComboContract.comboLegs.append(ComboLeg(conId=longCallContract.conId, ratio=1, action='BUY', exchange='SMART'))
		self._ironFlyAskPrice = OptionHelper.roundToTickSize(self._ironFlyAskPrice)
		return OptionHelper.roundToTickSize(ironFlyMidPrice)

	async def _trackEntryOrder(self):
		ib: IB = self.optraBot['ib']
		if self._entryTrade == None:
			logger.debug("Stopping Entry Order Tracking")
			self.optraBot._backgroundScheduler.remove_job('TrackEntryOrder')
			return
		
		if self._entryTrade.orderStatus.status == OrderStatus.Cancelled or self._entryTrade.orderStatus.status == OrderStatus.Inactive:
			self._onEntryOrderCanceled()
		elif self._entryTrade.orderStatus.status == OrderStatus.Filled:
			logger.info("Entry Order has been filled already. No adjustment required")
			logger.debug("Stopping Entry Order Tracking")
			self.optraBot._backgroundScheduler.remove_job('TrackEntryOrder')
		elif self._entryTrade.orderStatus.status == OrderStatus.PreSubmitted or self._entryTrade.orderStatus.status == OrderStatus.PendingSubmit:
			logger.info("Entry Order is still in PreSubmitted state. Waiting for it to be submitted...")
		elif self._entryTrade.orderStatus.status == OrderStatus.Submitted:
			currentLimitPrice = self._entryTrade.order.lmtPrice
			adjustedLimitPrice = currentLimitPrice + self._currentTemplate.adjustmentStep
			logger.info("Entry Order status ({}). Entry price will be adjusted. Current Limit Price: ${}", self._entryTrade.orderStatus.status, currentLimitPrice)
			if self._meetsMinimumPremium(adjustedLimitPrice) and adjustedLimitPrice <= self._ironFlyAskPrice:
				self._entryTrade.order.lmtPrice = adjustedLimitPrice
				try:
					ib.placeOrder(self._entryTradeContract, self._entryTrade.order)
				except Exception as excp:
					logger('Exception beim Anpassen der Order')
				return
			else:
				if adjustedLimitPrice > -15:
					logger.info("Entry order limit price reached minimum premium. No entry.")
				if adjustedLimitPrice > self._ironFlyAskPrice:
					logger.info("Entry order limit price exceeded initial ask price. No entry.")
				ib.cancelOrder(self._entryTrade.order)
		else:
			logger.error("Entry Order status is not considered: {}", self._entryTrade.orderStatus.status)

	async def onOrderStatusEvent(self, trade: Trade):
		if trade == self._entryTrade:
			logger.debug('Order Status Event has been raised. Status: {}', trade.orderStatus.status)
			if trade.orderStatus.status == OrderStatus.Cancelled:
				logEntry: TradeLogEntry = None
				for logEntry in trade.log:
					if logEntry.status != 'Cancelled':
						continue
				if logEntry == None:
					logger.error('No log entry found for Entry order cancellation!')
				if logEntry.errorCode == 103:
					# Error 103, reqId 10292: Doppelt vorhandene Order-ID
					logger.info('Adjustment of entry order has been rejected, because Duplicate Order-ID. Entry Order still active.')
				elif logEntry.errorCode == 0:
					self._onEntryOrderCanceled()
			elif trade.orderStatus.status == OrderStatus.Filled:
				if self._position == False:
					logger.info('Entry Order has been filled at ${} (Qty: {}) and trade is running now.', trade.orderStatus.avgFillPrice, trade.orderStatus.filled)
					self._position = True
					if self._currentTemplate.stopLossAdjuster:
						self._currentTemplate.stopLossAdjuster.setBasePrice(round(self._entryTrade.orderStatus.avgFillPrice * -1, 2))
					asyncio.create_task(self._placeTakeProfitAndStop(trade)).add_done_callback(self.optraBot.handleTaskDone)
					#Run 1. Position Monitoring with delay
					#asyncio.create_task(self._monitorPositionDelayed()).add_done_callback(self.optraBot.handleTaskDone)
					self.optraBot._backgroundScheduler.add_job(self._monitorPosition, 'interval', seconds=5, id='MonitorPosition')
				elif self._position == True:
					logger.debug('Additionally fill quantity (Qty: {}) for the entry order...depending orders need to be adjusted.', trade.orderStatus.filled)
					asyncio.create_task(self._adjustTakeProfitAndStop(trade)).add_done_callback(self.optraBot.handleTaskDone)

				if trade.orderStatus.remaining > 0:
					logger.warning('Partial fill for entry order. Remaining: {}', trade.orderStatus.remaining)
				executedAmount = trade.orderStatus.filled - self._entryOrderFilled
				logger.debug('Executed amount for entry trade: {}', executedAmount)
				if executedAmount > 0:
					asyncio.create_task(self._reportExecutedTrade(trade, executedAmount)).add_done_callback(self.optraBot.handleTaskDone)
				self._entryOrderFilled = self._entryTrade.order.totalQuantity - trade.orderStatus.remaining

		elif self._tpTrade and trade.order.orderId == self._tpTrade.order.orderId:
			logger.debug('TP Order Status has been raised. Status: {}', trade.orderStatus.status)
			if trade.orderStatus.status == OrderStatus.Cancelled:
				logger.info('TP Order has been cancelled!')

			elif trade.orderStatus.status == OrderStatus.Filled:
				logger.success('TP Order has been filled. Trade finished')
				self._tpTrade = None
				self._slShortTrade = None
				self._onPositionClose()

		elif self._slShortTrade and trade.order.orderId == self._slShortTrade.order.orderId:
			logger.debug('SL order for Short Legs status has been changed. Status: {}', trade.orderStatus.status)
			if trade.orderStatus.status == OrderStatus.Cancelled:
				logEntry: TradeLogEntry = None
				for logEntry in trade.log:
					if logEntry.status != 'Cancelled':
						continue
				if logEntry == None:
					logger.error('No log entry found for SL order cancellation!')
					return
				if logEntry.errorCode == 201:
					# 201: Order rejected - reason:Stop price revision is disallowed after order has triggered
					logger.info('Adjustment of SL order has been rejected, because Stop Order was triggered or cancelled already.')
				elif logEntry.errorCode == 0:
					logger.info('SL Order for Short Legs has been cancelled.')
				else:
					logger.error('SL order for Short legs has been cancelled with error code {} and message: {}', logEntry.errorCode, logEntry.message)					

			elif trade.orderStatus.status == OrderStatus.Filled:
				logger.info('SL order for Short Legs has been filled. Trade finished')
				logger.info('Now....Long Legs need to be closed if possible')
				if self._closingLongLegs == False:
					asyncio.create_task(self._close_long_legs(self._entryTrade.orderStatus.filled, self._currentTemplate.name, self._currentTemplate.account, tradeId=self._currentTrade.id )).add_done_callback(self.optraBot.handleTaskDone)
					self._onPositionClose()

	def _onEntryOrderCanceled(self):
		""" Performs final steps after a entry order was canceled
		"""
		logger.info('Entry Order has been cancelled!')
		self._entryTrade = None
		logger.debug('Deleting trade {} from database...', self._currentTrade.id )
		with Session(get_db_engine()) as session:
			crud.delete_trade(session, self._currentTrade)
		self._currentTrade = None

	# async def _getAllOpenOrderContracts(self, account, symbol) -> List:
	# 	"""
	# 	Determine the ContractIds of all open Orders for the given account and symbol
	# 	"""
	# 	ib: IB = self.optraBot['ib']
	# 	openTrades: List[Trade] = await ib.reqAllOpenOrdersAsync()
	# 	openOrderContracts = list()
	# 	for openTrade in openTrades:
	# 		if openTrade.contract.symbol != symbol or openTrade.order.account != account:
	# 			continue
	# 		if openTrade.contract.secType == 'BAG':
	# 			for leg in openTrade.contract.comboLegs:
	# 				openOrderContracts.append(leg.conId)
	# 		else:
	# 			openOrderContracts.append(openTrade.contract.conId)
	# 	return openOrderContracts

	async def _DetermineValidLongOption(self, symbol, expiration, desiredStrike, right, contractsWithOpenOrders):
		""" Determines the next possible Long Leg Option on the given Strike price and right ensuring that
			there are no open orders.
		"""
		ib: IB = self.optraBot['ib']
		strike = desiredStrike
		validOption: Option = None
		tryCount = 0
		while tryCount < 5:
			tryCount += 1
			logger.debug('Checking Long {} Strike {}', right, strike)
			option = Option(symbol, expiration, strike, right, 'SMART', tradingClass = 'SPXW')
			await ib.qualifyContractsAsync(option)
			if not OptionHelper.checkContractIsQualified(option):
				break
			if not option.conId in contractsWithOpenOrders:
				validOption = option
				break
			if right == 'P':
				strike -= 5
			else:
				strike += 5
		return validOption


	async def onExecDetailsEvent(self, trade: Trade, fill: Fill):
		""" This eventhandler is called on trade execution
		"""
		# First we need to check if Execution is related to an OptraBot Trade
		# If so, the transaction needs to be recorded in the database
		logger.debug('Exec Detail for trade {}', trade)
		logger.debug('Fill: {}', fill)
		tradeId = TradeHelper.getTradeIdFromOrderRef(fill.execution.orderRef)
		if tradeId == 0:
			logger.debug('Fill not from OptraBot trade...ignoring it.')
			return
		if fill.contract.secType == 'BAG':
			# Do not store information on combo executions
			return
		with Session(get_db_engine()) as session:
			max_transactionid = crud.getMaxTransactionId(session, tradeId)
			dbTrade = crud.getTrade(session,tradeId)
			if max_transactionid == 0:
				# Opening Transaction of the trade
				dbTrade.status = 'OPEN'
			max_transactionid += 1
			transactionType = ''
			if fill.execution.side == 'BOT':
				transactionType = 'BUY'
			else:
				transactionType = 'SELL'
			expirationDate = datetime.strptime(fill.contract.lastTradeDateOrContractMonth, '%Y%m%d')
			newTransaction = schemas.TransactionCreate(tradeid=tradeId, id=max_transactionid, type=transactionType, sectype=fill.contract.right, timestamp=fill.execution.time, expiration=expirationDate, strike=fill.contract.strike, contracts=fill.execution.shares, price=fill.execution.avgPrice, fee=0, commission=0, notes='')
			self._fillTransactionMap[fill.execution.execId] = newTransaction.id # Memorize Execution ID of the fill for later commission report
			crud.createTransaction(session, newTransaction)

			# Check if trade is closed with all these transactions
			TradeHelper.updateTrade(dbTrade)
			session.commit()
	
	async def onCommissionReportEvent(self, trade: Trade, fill: Fill, report: CommissionReport):
		"""
		Handles the Commission Report Event
		"""
		logger.debug('Commission Report order id {} and fill {}', trade.order.orderId, fill)
		logger.debug('Commission Report: {}', report)
		tradeId = TradeHelper.getTradeIdFromOrderRef(fill.execution.orderRef)
		if tradeId == 0:
			logger.debug('Fill not from OptraBot trade...ignoring it.')
			return
		
		# Get Transaction ID for the fills execution id
		try:
			transactionId = self._fillTransactionMap[fill.execution.execId]
		except KeyError as keyError:
			logger.error('No trade transaction found for fill execution id {}', fill.execution.execId)
			return
		
		with Session(get_db_engine()) as session:
			transaction = crud.getTransactionById(session, tradeId, transactionId)
			if transaction == None:
				logger.error('Transaction with id {} for trade {} not found in database!', transactionId, tradeId)
				return
			transaction.commission = report.commission
			session.commit()

	async def _reportExecutedTrade(self, trade: Trade, executedAmount: int):
		"""
		Report the entry trade execution to Optrabot Hub
		"""
		try:
			executedContracts = 0
			if trade.contract.secType == 'BAG':
				for leg in trade.contract.comboLegs:
					executedContracts += leg.ratio * executedAmount
			else:
				executedContracts = executedAmount
					
			additional_data = {
				'trade_id': self._currentTrade.id,
				'account': self._currentTemplate.account,
				'contracts': int(executedContracts)
			}
			await self.reportAction('CT', additional_data=json.dumps(additional_data))
		except Exception as excp:
			logger.error('Error reporting position open event: {}', excp)

	async def _placeTakeProfitAndStop(self, entryTrade: Trade):
		fillPrice = entryTrade.orderStatus.avgFillPrice
		fillAmount = entryTrade.orderStatus.filled
		logger.debug('Entry Order was filled at ${}', fillPrice)
		if entryTrade.orderStatus.remaining > 0:
			logger.warning('Entry Order was filled partially only.')
		ib: IB = self.optraBot['ib']

		# Calculate Take Profit price
		logger.debug('Using Profit Level {} and Stop Level {}', self._currentTemplate.takeProfit, self._currentTemplate.stopLoss)
		profit = fillPrice * (self._currentTemplate.takeProfit / 100)

		limitPrice = OptionHelper.roundToTickSize(fillPrice - profit)
		now = datetime.now()
		ocaGroup = self._currentTemplate.account + '_' + now.strftime('%H%M%S')
		logger.info('Take Profit Limit Price ({}%) : {}', self._currentTemplate.takeProfit, limitPrice)
		order = LimitOrder('SELL', fillAmount, limitPrice)
		order.account = self._currentTemplate.account
		order.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + self._currentTemplate.name + ' - Take Profit'
		order.ocaGroup = ocaGroup
		order.outsideRth = True
		self._tpTrade = ib.placeOrder(self._ironFlyComboContract, order)
		logger.debug('Created TP order with id: {}', self._tpTrade.order.orderId)

		# Calculation of Stop Price
		stopFactor = ((self._currentTemplate.stopLoss / 100) + 1) * -1
		self._ironFlyStopPrice = OptionHelper.roundToTickSize(fillPrice * stopFactor)
		logger.info('Stop LossPrice ({}%): {}', self._currentTemplate.stopLoss, self._ironFlyStopPrice)

		stopShortsOrder = StopOrder('BUY', self._entryTrade.orderStatus.filled, self._ironFlyStopPrice)
		stopShortsOrder.account = self._currentTemplate.account
		stopShortsOrder.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + self._currentTemplate.name + ' - SL Short Legs'
		stopShortsOrder.ocaGroup = ocaGroup
		try:
			self._slShortTrade = ib.placeOrder(self._ironFlyShortComboContract, stopShortsOrder)
		except Exception as excp:
			logger('Exception beim Erstellen der Short Leg Stoploss order')

	async def _adjustTakeProfitAndStop(self, trade: Trade):
		""" Adjust Take Profit and Stop Order after another partial fill of the entry Order.
		"""
		ib: IB = self.optraBot['ib']
		newQuantity = trade.orderStatus.filled
		if newQuantity == self._tpTrade.order.totalQuantity:
			return
		logger.info('Adjusting quantity of TP and Stop Order to {}', newQuantity)
		self._tpTrade.order.totalQuantity = trade.orderStatus.filled
		ib.placeOrder(self._tpTrade.contract, self._tpTrade.order)
		logger.debug('Updated TP order with id: {}', self._tpTrade.order.orderId)
		self._slShortTrade.order.totalQuantity = trade.orderStatus.filled
		ib.placeOrder(self._slShortTrade.contract, self._slShortTrade.order)
		logger.debug('Updated SL order with id: {}', self._slShortTrade.order.orderId)

	async def _close_long_legs(self, positionSize: int, templatename: str, account: str, tradeId: int):
		logger.debug("Closing long legs of trade")
		self._closingLongLegs = True
		ib: IB = self.optraBot['ib']
		tickers = await ib.reqTickersAsync(*self._ironFlyLongLegContracts)
		for ticker in tickers:
			logger.debug("Long Leg {} {} Bid Price: {}", ticker.contract.right, ticker.contract.strike, ticker.bid)
			if ticker.bid >= 0.05:
				logger.debug('Creating limit sell order on bid price')
				order = LimitOrder('SELL', positionSize, ticker.bid)
				order.outsideRth = True
				order.account = account
				order.orderRef = 'OTB (' + str(tradeId) + '): ' + templatename + ' - Close Long Leg'
				closeTrade = ib.placeOrder(ticker.contract, order)
				logger.debug('Placed Long Leg closing order with id: {}', closeTrade.order.orderId)
			else:
				logger.info('Long leg {} {} is worthless and will be kept open till expiration.', ticker.contract.right, ticker.contract.strike)
				with Session(get_db_engine()) as session:
					max_transactionid = crud.getMaxTransactionId(session, tradeId)
					dbTrade = crud.getTrade(session,tradeId)
					assert(max_transactionid > 0)
					# Create expiration transaction for this leg
					max_transactionid += 1
					transactionType = 'EXP'
					expirationDate = datetime.strptime(ticker.contract.lastTradeDateOrContractMonth, '%Y%m%d')
					newTransaction = schemas.TransactionCreate(tradeid=tradeId, id=max_transactionid, type=transactionType, sectype=ticker.contract.right, timestamp=expirationDate, expiration=expirationDate, strike=ticker.contract.strike, contracts=positionSize, price=0.0, fee=0, commission=0.0, notes='')
					crud.createTransaction(session, newTransaction)
					TradeHelper.updateTrade(dbTrade)
					session.commit()

		logger.debug('exit _close_long_legs()')

	def _onPositionClose(self):
		logger.debug('_onPositionClose()')
		self._tpTrade = None
		self._slShortTrade = None
		self._entryTrade = None
		self._ironFlyComboContract = None
		self._ironFlyContracts = None
		self._position = False
		self._closingLongLegs = False
		self._currentTemplate = None
		self._currentTrade = None
		self._stopPositionMonitoring()

	async def _monitorPosition(self):
		logger.debug('Enter Monitor position()')
		if self._position == False:
			logger.debug('Position has been closed. Stopping Position-Monitoring now.')
			#self._positionMonitorTask = None
			self._stopPositionMonitoring()
			return

		#asyncio.create_task(self._monitorPositionDelayed()).add_done_callback(self.optraBot.handleTaskDone)

		ib: IB = self.optraBot['ib']
		if not ib.isConnected() or self.optraBot.isTradingEnabled() == False:
			logger.error('Interactive Brokers is not connected. Unable to monitor position!')
			return
		
		try:
			if self._position == False:
				logger.debug('Position has been closed. Stopping Position-Monitoring now.')
				return

			tickers = await ib.reqTickersAsync(*self._ironFlyContracts)
			longLegsValue = 0
			currentIronFlyAskPrice = 0
			currentIronFlyBidPrice = 0
			adjustedStopLossPrice = None
			for ticker in tickers:
				if ticker.contract in self._ironFlyLongLegContracts:
					if ticker.bid >= 0:
						longLegsValue += ticker.bid
						currentIronFlyBidPrice -= ticker.bid
						currentIronFlyAskPrice += ticker.ask
				else:
					# Für Short Legs müssen die Ask Preise addiert werden
					currentIronFlyBidPrice += ticker.ask
					currentIronFlyAskPrice -= ticker.bid
		
			if self._position == False:
				logger.debug('Position has been closed. Stopping Position-Monitoring now.')
				return

			currentIronFlyAskPrice = abs(currentIronFlyAskPrice)
			currentIronFlyPrice = OptionHelper.calculateMidPrice(currentIronFlyBidPrice, currentIronFlyAskPrice)
			if currentIronFlyPrice == self._previousIronFlyPrice:
				self._unchangedIronFlyPriceCounter += 1
			else:
				self._unchangedIronFlyPriceCounter = 0
			self._previousIronFlyPrice = currentIronFlyPrice
			if self._unchangedIronFlyPriceCounter > 10:
				logger.warning('Price of Iron Fly has not changed for 10 times. There is a problem with price data from TWS probably.')
				self._unchangedIronFlyPriceCounter = 0

			if self._currentTemplate.stopLossAdjuster:
				if not self._currentTemplate.stopLossAdjuster.isTriggered():
					adjustedStopLossPrice = self._currentTemplate.stopLossAdjuster.execute(currentIronFlyPrice)
					if adjustedStopLossPrice == None:
						logger.debug('No need to adjust stop loss now.')
					else:
						self._ironFlyStopPrice = adjustedStopLossPrice
						logger.info('Performing stop loss adjustment. New Stop Loss price: {}', self._ironFlyStopPrice)
				else:
					logger.debug('Stoploss adjustment has been performed already.')
			else:
				logger.debug('No StopLoss adjustment configured.')
			desiredStopPrice = OptionHelper.roundToTickSize(self._ironFlyStopPrice + longLegsValue)
			currentStopPrice = OptionHelper.roundToTickSize(self._slShortTrade.order.auxPrice)
			logger.debug('Long Legs value {} Current Short SL Price: {} Desired Short SL Pice: {}', round(longLegsValue,2), currentStopPrice, desiredStopPrice)
			openOrders = ib.openOrders()

			if self._position == False:
				logger.debug('Position has been closed. Stopping Position-Monitoring now.')
				return
			if any(order.orderId == self._slShortTrade.order.orderId for order in openOrders):
			#if self._slShortTrade.order in openOrders:
				if self._slShortTrade.isActive():
					if currentStopPrice != desiredStopPrice:
						self._slShortTrade.order.auxPrice = desiredStopPrice
						logger.info('Adjusting Stop Loss price to ${}', desiredStopPrice)
						ib.placeOrder(self._slShortTrade.contract, self._slShortTrade.order)
						logger.debug('Updated SL order with id: {}', self._slShortTrade.order.orderId)
					else:
						logger.debug('No adjustment of Stop Loss price required.')
				else:
					logger.debug('SL Order is not active anymore.')
			else:
				logger.warning('Caution: Stop Loss order for Short Strikes is missing. Trying to reestablish the Stop Loss order now!')
				await self._reestablishStopLossOrder(desiredStopPrice)
		except ConnectionError as connError:
			logger.error('Connection to TWS lost during Position Monitoring. Unable to monitor position!')

		logger.debug('Leave Monitor position()')

	async def _monitorPositionDelayed(self):
		logger.debug('Waiting 10 seconds for next position monitoring.')
		await asyncio.sleep(10)
		if self._position == True:
			asyncio.create_task(self._monitorPosition(), name='MonitorPosition').add_done_callback(self.optraBot.handleTaskDone)

	async def _reestablishStopLossOrder(self, desiredStopPrice: float):
		"""
		Tries to create a new Stop Loss order for the open position based on the given stop price.
		"""
		logger.debug('Reestablishing Stop Loss order for Short Legs')
		ib: IB = self.optraBot['ib']
		if not ib.isConnected():
			logger.error('Interactive Brokers is not connected. Unable to reestablish Stop Loss Order!')
			return
		
		now = datetime.now()
		ocaGroup = self._currentTemplate.account + '_' + now.strftime('%H%M%S')
		stopShortsOrder = StopOrder('BUY', self._entryTrade.orderStatus.filled, desiredStopPrice)
		stopShortsOrder.account = self._currentTemplate.account
		stopShortsOrder.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + self._currentTemplate.name + ' - SL Short Legs'
		stopShortsOrder.ocaGroup = ocaGroup
		try:
			self._slShortTrade = ib.placeOrder(self._ironFlyShortComboContract, stopShortsOrder)
		except Exception as excp:
			logger('Exception beim Erstellen der Short Leg Stoploss order')
		logger.info('Stop Loss order for Short Legs has been reestablished.')

		# Check if Take Profit Order is still in place and recreate it, if it's missing
		openTrades = await ib.reqOpenOrdersAsync()
		if self._tpTrade in openTrades:
			logger.info('Take Profit order is still in place.')
		else:
			logger.warning('Take Profit order is missing. Trying to reestablish the Take Profit order now!')
			order = LimitOrder('SELL', self._tpTrade.order.totalQuantity, self._tpTrade.order.lmtPrice)
			order.account = self._currentTemplate.account
			order.orderRef = 'OTB (' + str(self._currentTrade.id) + '): ' + self._currentTemplate.name + ' - Take Profit'
			order.ocaGroup = ocaGroup
			order.outsideRth = True
			self._tpTrade = ib.placeOrder(self._ironFlyComboContract, order)
			logger.debug('Created TP order with id: {}', self._tpTrade.order.orderId)

	def _meetsMinimumPremium(self, premium: float) -> bool:
		""" Checks if given premium meets minimum premium

		Parameters
		----------
		premium : float
			As premium is a typically a credit, a negative number is expected.
		
		Returns
		-------
		bool
			Returns True, if the received premium is more than the configured minimum premium
		"""
		if self._currentTemplate.minPremium == None:
			return True
		if premium > (self._currentTemplate.minPremium * -1):
			return False
		return True

	def _parseTimestamp(self, timestamp: str) -> datetime:
		""" Parses the given timestamp into a `datetime`object

		Parameters
		----------
		timestamp : str
    		Timestamp as string with timezone info e.g. 2023-11-07T14:10:00Z.
		"""
		try:
			parsedTime = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S%z')
		except Exception as excpt:
			logger.error("Timestamp {} got unexpected format.", timestamp)
			return None
		return parsedTime


	# def _signalIsOutdated(self, signalTimeStamp: datetime):
	# 	""" Checks if the time stamp of the signal is older than 10 minutes which means it's outdated.
		
	# 	Parameters
	# 	----------
	# 	signalTimeStamp : datetime
    # 		Timestamp of the signal.

	# 	Returns
	# 	-------
	# 	bool
	# 		Returns True, if the signal is outdated
	# 	"""
	# 	if datetime == None:
	# 		return True
	# 	currentTime = datetime.now().astimezone()
	# 	timediff = currentTime - signalTimeStamp
	# 	if (timediff.seconds / 60) > 10:
	# 		return True
	# 	return False

	async def _scheduleNextPoll(self):
		await asyncio.sleep(5)
		asyncio.create_task(self._poll()).add_done_callback(self.optraBot.handleTaskDone)
		
	def start_polling(self, scheduler):
		"""
		Tradinghub Polling runner
		"""
		logger.debug("start_polling")

		if self._apiKey == None:
			logger.debug("No API Key configured. Stop polling.")
			return

		if self._instanceId == None:
			logger.error("No Instance ID configured in config.yaml. Stop polling!")
			return

		scheduler.add_job(self._poll, 'interval', seconds=5)

		# try:
		# 	tasks: List[asyncio.Task[Any]] = [
		# 		asyncio.create_task(self._poll())
		# 	]
		# 	for task in tasks:
		# 		task.add_done_callback(self.optraBot.handleTaskDone)
		# 	#tasks.append(asyncio.create_task(self._stop_signal.wait()))
		# 	done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

		# 	for task in pending:
		# 		# (mostly) Graceful shutdown unfinished tasks
		# 		task.cancel()
		# 		with suppress(asyncio.CancelledError):
		# 			await task
		# 		# Wait finished tasks to propagate unhandled exceptions
		# 		await asyncio.gather(*done)
		# except Exception as excp:
		# 	logger.error("Exception {}", excp)

	def _stopPositionMonitoring(self):
		#tasks = asyncio.all_tasks()
		#for task in tasks:
		#	if task.get_name() == 'MonitorPosition':
		#		task.cancel()
		#if self._positionMonitorTask:
		#	self._positionMonitorTask.cancel()
		#	self._positionMonitorTask = None
		self.optraBot._backgroundScheduler.remove_job('MonitorPosition')

	def isHubConnectionOK(self) -> bool:
		""" Returns True if the last request to the OptraBot Hub was responed 
		30 seconds ago or less.
		"""
		if self._lastAnswerReceivedAt == None:
			return False
		timeDelta = datetime.now() - self._lastAnswerReceivedAt
		if timeDelta.total_seconds() > 30:
			return False
		else:
			return True