Example #1
0
 def __init__(self, config, ib: IB):
     self.config = config
     self.ib = ib
     self._nope_value = 0
     self._underlying_price = 0
     self.qt = QuestradeClient(token_yaml=self.QT_ACCESS_TOKEN)
     self.run_qt_tasks()
Example #2
0
class NopeStrategy:
    QT_ACCESS_TOKEN = "qt/access_token.yml"
    SYMBOL = "SPY"

    def __init__(self, config, ib: IB):
        self.config = config
        self.ib = ib
        self._nope_value = 0
        self._underlying_price = 0
        self.ib_tasks_dict = dict()
        self.qt = QuestradeClient(token_yaml=self.QT_ACCESS_TOKEN)
        self.run_qt_tasks()

    def console_log(self, s):
        if self.config["debug"]["verbose"]:
            _, curr_dt = get_datetime_for_logging()
            print(
                s,
                f"| {self._nope_value} | {self._underlying_price} | {curr_dt}")

    def get_tasks_dict(self):
        return self.ib_tasks_dict

    def req_market_data(self):
        # https://interactivebrokers.github.io/tws-api/market_data_type.html
        self.ib.reqMarketDataType(1)
        self.ib.reqAllOpenOrders()
        self.ib.reqPositions()

    def set_nope_value(self):
        self._nope_value, self._underlying_price = self.qt.get_nope()

    def get_portfolio(self):
        portfolio = self.ib.portfolio()
        # Filter out non-SPY contracts
        portfolio = [
            item for item in portfolio if item.contract.symbol == self.SYMBOL
        ]
        return portfolio

    def get_trades(self):
        trades = self.ib.openTrades()
        trades = [
            t for t in trades
            if t.isActive() and t.contract.symbol == self.SYMBOL
        ]
        return trades

    # From thetagang
    # https://github.com/brndnmtthws/thetagang
    def find_eligible_contracts(self, symbol, right):
        is_auto_select = self.config["nope"]["contract_auto_select"]
        EXCHANGE = "SMART"
        MAX_STRIKE_OFFSET = 6 if is_auto_select else 11

        stock = Stock(symbol, EXCHANGE, currency="USD")
        self.ib.qualifyContracts(stock)
        [ticker] = self.ib.reqTickers(stock)
        ticker_value = ticker.marketPrice()
        chains = self.ib.reqSecDefOptParams(stock.symbol, "", stock.secType,
                                            stock.conId)
        chain = next(c for c in chains if c.exchange == EXCHANGE)

        def valid_strike(strike):
            if strike % 1 == 0:
                if right == "C":
                    max_ntm_call_strike = ticker_value + MAX_STRIKE_OFFSET
                    min_itm_call_strike = (ticker_value - MAX_STRIKE_OFFSET
                                           if is_auto_select else ticker_value)
                    return min_itm_call_strike <= strike <= max_ntm_call_strike
                elif right == "P":
                    min_ntm_put_strike = ticker_value - MAX_STRIKE_OFFSET
                    max_itm_put_strike = (ticker_value + MAX_STRIKE_OFFSET
                                          if is_auto_select else ticker_value)
                    return min_ntm_put_strike <= strike <= max_itm_put_strike
            return False

        strikes = [strike for strike in chain.strikes if valid_strike(strike)]

        if is_auto_select:
            min_dte = self.config["nope"]["auto_min_dte"]
            expirations = sorted(
                exp for exp in chain.expirations)[min_dte:min_dte + 5]
        else:
            exp_offset = self.config["nope"]["expiry_offset"]
            expirations = sorted(
                exp for exp in chain.expirations)[exp_offset:exp_offset + 1]

        contracts = [
            Option(
                self.SYMBOL,
                expiration,
                strike,
                right,
                EXCHANGE,
                tradingClass=self.SYMBOL,
            ) for expiration in expirations for strike in strikes
        ]

        return contracts

    def get_num_open_buy_orders(self, right):
        trades = self.get_trades()
        return sum(
            map(
                lambda t: t.order.totalQuantity,
                filter(
                    lambda t: t.contract.right == right and t.order.action ==
                    "BUY",
                    trades,
                ),
            ))

    def get_total_position(self, right):
        held_contracts = self.get_held_contracts_info(right)
        return sum(map(lambda c: c["position"], held_contracts))

    def cancel_order_type(self, action, order_type="LMT"):
        trades = self.get_trades()
        filtered = list(
            filter(
                lambda t: t.order.action == action and t.order.orderType ==
                order_type,
                trades,
            ))
        for trade in filtered:
            self.ib.cancelOrder(trade.order)

    def get_open_stop_orders(self):
        trades = self.get_trades()
        return set(
            map(
                lambda t: t.contract.conId,
                filter(lambda t: t.order.orderType == "STP", trades),
            ))

    def cancel_stop_loss_task(self):
        if "set_stop_loss" in self.ib_tasks_dict:
            stop_loss_task = self.ib_tasks_dict.pop("set_stop_loss")
            stop_loss_task.cancel()

    def set_stop_loss(self, right):
        self.console_log("Check stop loss conditions")
        total_position = self.get_total_position(right)
        buy_limit = (self.config["nope"]["call_limit"]
                     if right == "C" else self.config["nope"]["put_limit"])
        if total_position >= buy_limit:
            existing_stop_orders = self.get_open_stop_orders()
            held_contracts_info_no_stop_order = filter(
                lambda c: c["contract"].conId not in existing_stop_orders,
                self.get_held_contracts_info(right),
            )
            for contract_info in held_contracts_info_no_stop_order:
                position = contract_info["position"]
                avg_price = contract_info["avg"] / 100
                contract = contract_info["contract"]
                qualified_contracts = self.ib.qualifyContracts(contract)
                order_price = stop_order_price(
                    avg_price, self.config["nope"]["stop_loss_percentage"])

                if len(qualified_contracts) > 0:
                    stop_loss_order = StopOrder(
                        "SELL",
                        position,
                        order_price,
                        tif="DAY",
                    )
                    qualified_contract = qualified_contracts[0]
                    trade = self.ib.placeOrder(qualified_contract,
                                               stop_loss_order)
                    trade.filledEvent += log_fill
                    self.log_order(qualified_contract, position, order_price,
                                   "STOP")
                    self.cancel_stop_loss_task()

    def schedule_stop_order_task(self, right):
        async def stop_loss_periodic():
            async def schedule_stop_loss():
                try:
                    self.set_stop_loss(right)
                except Exception as e:
                    log_exception(e, "schedule_stop_order_task")

            while True:
                await asyncio.gather(asyncio.sleep(120), schedule_stop_loss())

        if "set_stop_loss" not in self.ib_tasks_dict:
            loop = asyncio.get_event_loop()
            self.ib_tasks_dict["set_stop_loss"] = loop.create_task(
                stop_loss_periodic())

    def on_buy_fill(self, trade):
        try:
            fill = trade.fills[0]
        except Exception as e:
            log_exception(e, "on_buy_fill")
            return

        self.schedule_stop_order_task(fill.contract.right)

    def check_acc_balance(self, price, quantity):
        ib_account = self.config["ib"]["account"]
        if not ib_account:
            return True
        acc_values = self.ib.accountValues(account=ib_account)
        buy_power = list(
            filter(lambda a: a.tag == "BuyingPower" and a.currency == "USD",
                   acc_values))
        try:
            balance = buy_power[0]
        except Exception as e:
            log_exception(e, "check_acc_balance")
            return False

        return float(balance.value) > price * 100 * quantity

    def select_contract(self, contracts, right):
        if self.config["nope"]["contract_auto_select"]:
            target = self.config["nope"]["auto_target_delta"] / 100
            target_delta = target if right == "C" else -target

            def reducer(ticker_prev, ticker):
                greeks_prev = ticker_prev.modelGreeks
                greeks = ticker.modelGreeks
                if greeks_prev is None and greeks is None:
                    return ticker_prev
                elif greeks_prev is None:
                    return ticker
                elif greeks is None:
                    return ticker_prev

                ticker_next = (
                    ticker if abs(ticker.modelGreeks.delta - target_delta) <
                    abs(ticker_prev.modelGreeks.delta - target_delta) else
                    ticker_prev)

                return ticker_next

            qualified_contracts = self.ib.qualifyContracts(*contracts)
            tickers = self.ib.reqTickers(*qualified_contracts)
            if len(tickers) > 0:
                closest = reduce(reducer, tickers)
                return closest
        else:
            offset = (self.config["nope"]["call_strike_offset"] if right == "C"
                      else -self.config["nope"]["put_strike_offset"] - 1)
            contract_to_buy = contracts[offset]
            qualified_contracts = self.ib.qualifyContracts(contract_to_buy)
            tickers = self.ib.reqTickers(*qualified_contracts)
            if len(tickers) > 0:
                return tickers[0]
        return None

    def buy_contracts(self, right):
        action = "BUY"
        contracts = self.find_eligible_contracts(self.SYMBOL, right)
        ticker = self.select_contract(contracts, right)
        if ticker is not None:
            price = midpoint_or_market_price(ticker)
            quantity = (self.config["nope"]["call_quantity"] if right == "C"
                        else self.config["nope"]["put_quantity"])
            if not util.isNan(price) and self.check_acc_balance(
                    price, quantity):
                contract = ticker.contract
                order = LimitOrder(
                    action,
                    quantity,
                    price,
                    algoStrategy="Adaptive",
                    algoParams=[
                        TagValue(tag="adaptivePriority", value="Normal")
                    ],
                    tif="DAY",
                )
                self.cancel_order_type("SELL", "STP")
                trade = self.ib.placeOrder(contract, order)
                trade.filledEvent += log_fill
                trade.filledEvent += self.on_buy_fill
                self.log_order(contract, quantity, price, action)
            else:
                with open("logs/errors.txt", "a") as f:
                    f.write(
                        f"Error buying {right} at {self._nope_value} | {self._underlying_price}\n"
                    )

    def get_total_buys(self, right):
        held_puts = self.get_total_position(right)
        existing_order_quantity = self.get_num_open_buy_orders(right)
        return held_puts + existing_order_quantity

    def enter_positions(self):
        self.console_log("Check enter thresholds")
        if (self.config["nope"]["long_enter"] > self._nope_value >
                self.config["nope"]["long_enter_limit"]):
            total_buys = self.get_total_buys("C")
            if total_buys < self.config["nope"]["call_limit"]:
                self.buy_contracts("C")
        elif (self.config["nope"]["short_enter"] < self._nope_value <
              self.config["nope"]["short_enter_limit"]):
            total_buys = self.get_total_buys("P")
            if total_buys < self.config["nope"]["put_limit"]:
                self.buy_contracts("P")

    def get_held_contracts_info(self, right):
        portfolio = self.get_portfolio()
        return [
            c for c in map(
                lambda p: {
                    "contract": p.contract,
                    "position": p.position,
                    "avg": p.averageCost,
                },
                portfolio,
            ) if c["contract"].right == right and c["position"] > 0
        ]

    def get_existing_order_ids(self, right, action):
        trades = self.get_trades()
        return set(
            map(
                lambda t: t.contract.conId,
                filter(
                    lambda t: t.contract.right == right and t.order.action ==
                    action and t.order.orderType != "STP",
                    trades,
                ),
            ))

    def log_order(self, contract, quantity, price, action, avg=0):
        curr_date, curr_dt = get_datetime_for_logging()
        log_str = f"Placed {action} order {quantity} {contract.strike}{contract.right}{contract.lastTradeDateOrContractMonth}"
        if action == "SELL":
            log_str += f" ({round(avg, 2)} average)"
        log_str += f" for {round(price * 100, 2)} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n"
        with open(f"logs/{curr_date}-trade.txt", "a") as f:
            f.write(log_str)

    def on_sell_fill(self, trade):
        self.cancel_order_type("SELL", "STP")
        self.cancel_stop_loss_task()

    def sell_held_contracts(self, right):
        action = "SELL"
        held_contracts_info = self.get_held_contracts_info(right)
        existing_contract_order_ids = self.get_existing_order_ids(
            right, action)
        remaining_contracts_info = list(
            filter(
                lambda c: c["contract"].conId not in
                existing_contract_order_ids,
                held_contracts_info,
            ))

        if len(remaining_contracts_info) > 0:
            remaining_contracts_info.sort(key=lambda c: c["contract"].conId)
            remaining_contracts = [
                c["contract"] for c in remaining_contracts_info
            ]
            qualified_contracts = self.ib.qualifyContracts(
                *remaining_contracts)
            tickers = self.ib.reqTickers(*qualified_contracts)
            tickers.sort(key=lambda t: t.contract.conId)
            for idx, ticker in enumerate(tickers):
                price = midpoint_or_market_price(ticker)
                avg = remaining_contracts_info[idx]["avg"]
                if not util.isNan(price) and price > (avg / 100):
                    quantity = remaining_contracts_info[idx]["position"]
                    order = LimitOrder(
                        action,
                        quantity,
                        price,
                        algoStrategy="Adaptive",
                        algoParams=[
                            TagValue(tag="adaptivePriority", value="Normal")
                        ],
                        tif="DAY",
                    )
                    contract = ticker.contract
                    trade = self.ib.placeOrder(contract, order)
                    trade.filledEvent += log_fill
                    trade.filledEvent += self.on_sell_fill
                    self.log_order(contract, quantity, price, action, avg)
                else:
                    with open("logs/errors.txt", "a") as f:
                        f.write(
                            f"Error selling {right} at {self._nope_value} | {self._underlying_price}\n"
                        )

    def exit_positions(self):
        self.console_log("Check exit thresholds")
        if self._nope_value > self.config["nope"]["long_exit"]:
            self.sell_held_contracts("C")
        if self._nope_value < self.config["nope"]["short_exit"]:
            self.sell_held_contracts("P")

    def run_ib(self):
        async def ib_periodic():
            async def enter_pos():
                try:
                    self.enter_positions()
                except Exception as e:
                    log_exception(e, "enter_positions")

            async def exit_pos():
                try:
                    self.exit_positions()
                except Exception as e:
                    log_exception(e, "exit_positions")

            while True:
                await asyncio.gather(asyncio.sleep(60), enter_pos(),
                                     exit_pos())

        async def check_orders():
            cancel_after = self.config["nope"]["minutes_cancel_unfilled"]

            async def cancel_unfilled_orders():
                cancellable_statuses = ["PreSubmitted", "Submitted"]
                trades = self.get_trades()
                filtered = list(
                    filter(
                        lambda t: t.order.orderType != "STP",
                        trades,
                    ))
                for trade in filtered:
                    submit_logs = list(
                        filter(lambda l: l.status in cancellable_statuses,
                               trade.log))
                    try:
                        submit_log = submit_logs[0]
                    except Exception as e:
                        log_exception(e, "cancel_unfilled_orders")
                        return

                    diff = get_datetime_diff_from_now(submit_log.time)
                    if diff > cancel_after:
                        self.ib.cancelOrder(trade.order)
                        self.console_log("Cancelled unfilled order")

            while True:
                await asyncio.gather(asyncio.sleep(cancel_after * 60),
                                     cancel_unfilled_orders())

        loop = asyncio.get_event_loop()
        self.ib_tasks_dict["run_ib"] = loop.create_task(ib_periodic())
        self.ib_tasks_dict["check_orders"] = loop.create_task(check_orders())

    def run_qt_tasks(self):
        async def nope_periodic():
            async def fetch_and_report():
                try:
                    self.set_nope_value()
                except Exception as e:
                    log_exception(e, "set_nope_value")

                self.console_log("Updated NOPE and stock price")
                curr_date, curr_dt = get_datetime_for_logging()
                with open(f"logs/{curr_date}.txt", "a") as f:
                    f.write(
                        f"NOPE @ {self._nope_value} | Stock Price @ {self._underlying_price} | {curr_dt}\n"
                    )

            while True:
                await asyncio.gather(asyncio.sleep(60), fetch_and_report())

        async def token_refresh_periodic():
            async def refresh_token():
                try:
                    self.qt.refresh_access_token()
                except Exception as e:
                    log_exception(e, "refresh_token")

            while True:
                await asyncio.sleep(120)
                await refresh_token()

        def run_thread():
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.create_task(nope_periodic())
            loop.create_task(token_refresh_periodic())
            loop.run_forever()

        thread = threading.Thread(target=run_thread)
        thread.start()

    def execute(self):
        self.req_market_data()
        self.run_ib()
Example #3
0
class NopeStrategy:
    QT_ACCESS_TOKEN = "qt/access_token.yml"
    SYMBOL = "SPY"

    def __init__(self, config, ib: IB):
        self.config = config
        self.ib = ib
        self._nope_value = 0
        self._underlying_price = 0
        self.qt = QuestradeClient(token_yaml=self.QT_ACCESS_TOKEN)
        self.run_qt_tasks()

    def req_market_data(self):
        # https://interactivebrokers.github.io/tws-api/market_data_type.html
        self.ib.reqMarketDataType(1)
        self.ib.reqAllOpenOrders()
        self.ib.reqPositions()

    def set_nope_value(self):
        self._nope_value, self._underlying_price = self.qt.get_nope()

    def get_portfolio(self):
        portfolio = self.ib.portfolio()
        # Filter out non-SPY contracts
        portfolio = [
            item for item in portfolio if item.contract.symbol == self.SYMBOL
        ]
        return portfolio

    def get_trades(self):
        trades = self.ib.openTrades()
        trades = [
            t for t in trades
            if t.isActive() and t.contract.symbol == self.SYMBOL
        ]
        return trades

    # From thetagang
    # https://github.com/brndnmtthws/thetagang
    def find_eligible_contracts(self, symbol, right):
        EXCHANGE = "SMART"
        MAX_STRIKE_OFFSET = 5

        stock = Stock(symbol, EXCHANGE, currency="USD")
        self.ib.qualifyContracts(stock)
        [ticker] = self.ib.reqTickers(stock)
        ticker_value = ticker.marketPrice()
        chains = self.ib.reqSecDefOptParams(stock.symbol, "", stock.secType,
                                            stock.conId)
        chain = next(c for c in chains if c.exchange == EXCHANGE)

        def valid_strike(strike):
            if strike % 1 == 0:
                if right == "C":
                    max_ntm_call_strike = ticker_value + MAX_STRIKE_OFFSET
                    return ticker_value <= strike <= max_ntm_call_strike
                elif right == "P":
                    min_ntm_put_strike = ticker_value - MAX_STRIKE_OFFSET
                    return min_ntm_put_strike <= strike <= ticker_value
            return False

        strikes = [strike for strike in chain.strikes if valid_strike(strike)]

        # TODO: Remove slicing once contract selection algorithm implemented
        exp_offset = self.config["nope"]["expiry_offset"]
        expirations = sorted(
            exp for exp in chain.expirations)[exp_offset:exp_offset + 1]

        contracts = [
            Option(
                self.SYMBOL,
                expiration,
                strike,
                right,
                EXCHANGE,
                tradingClass=self.SYMBOL,
            ) for expiration in expirations for strike in strikes
        ]

        return contracts

    def get_num_open_buy_orders(self, trades, right):
        return sum(
            map(
                lambda t: t.order.totalQuantity,
                filter(
                    lambda t: t.contract.right == right and t.order.action ==
                    "BUY",
                    trades,
                ),
            ))

    def get_total_position(self, portfolio, right):
        held_contracts = self.get_held_contracts_info(portfolio, right)
        return sum(map(lambda c: c["position"], held_contracts))

    def buy_contracts(self, right):
        action = "BUY"
        contracts = self.find_eligible_contracts(self.SYMBOL, right)
        # TODO: Improve contract selection https://github.com/ajhpark/ib_nope/issues/21
        offset = (self.config["nope"]["call_strike_offset"] if right == "C"
                  else -self.config["nope"]["put_strike_offset"] - 1)
        contract_to_buy = contracts[offset]
        qualified_contracts = self.ib.qualifyContracts(contract_to_buy)
        tickers = self.ib.reqTickers(*qualified_contracts)
        if len(tickers) > 0:
            price = midpoint_or_market_price(tickers[0])
            if not util.isNan(price):
                contract = qualified_contracts[0]
                quantity = (self.config["nope"]["call_quantity"] if right
                            == "C" else self.config["nope"]["put_quantity"])
                order = LimitOrder(
                    action,
                    quantity,
                    price,
                    algoStrategy="Adaptive",
                    algoParams=[
                        TagValue(tag="adaptivePriority", value="Normal")
                    ],
                    tif="DAY",
                )
                trade = self.ib.placeOrder(contract, order)
                trade.filledEvent += log_fill
                self.log_order(contract, quantity, price, action)
            else:
                with open("logs/errors.txt", "a") as f:
                    f.write(
                        f"Error buying {right} at {self._nope_value} | {self._underlying_price}\n"
                    )

    def get_total_buys(self, right):
        portfolio = self.get_portfolio()
        trades = self.get_trades()
        held_puts = self.get_total_position(portfolio, right)
        existing_order_quantity = self.get_num_open_buy_orders(trades, right)
        return held_puts + existing_order_quantity

    def enter_positions(self):
        if self._nope_value < self.config["nope"]["long_enter"]:
            total_buys = self.get_total_buys("C")
            if total_buys < self.config["nope"]["call_limit"]:
                self.buy_contracts("C")
        elif self._nope_value > self.config["nope"]["short_enter"]:
            total_buys = self.get_total_buys("P")
            if total_buys < self.config["nope"]["put_limit"]:
                self.buy_contracts("P")

    def get_held_contracts_info(self, portfolio, right):
        return [
            c for c in map(
                lambda p: {
                    "contract": p.contract,
                    "position": p.position,
                    "avg": p.averageCost,
                },
                portfolio,
            ) if c["contract"].right == right and c["position"] > 0
        ]

    def get_existing_order_ids(self, trades, right, buy_or_sell):
        return set(
            map(
                lambda t: t.contract.conId,
                filter(
                    lambda t: t.contract.right == right and t.order.action ==
                    buy_or_sell,
                    trades,
                ),
            ))

    def log_order(self, contract, quantity, price, action, avg=0):
        curr_date, curr_dt = get_datetime_for_logging()
        log_str = f"Placed {action} order {quantity} {contract.strike}{contract.right}{contract.lastTradeDateOrContractMonth}"
        if action == "SELL":
            log_str += f" ({round(avg, 2)} average)"
        log_str += f" for {round(price * 100, 2)} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n"
        with open(f"logs/{curr_date}-trade.txt", "a") as f:
            f.write(log_str)

    def sell_held_contracts(self, right):
        portfolio = self.get_portfolio()
        trades = self.get_trades()
        action = "SELL"

        held_contracts_info = self.get_held_contracts_info(portfolio, right)
        existing_contract_order_ids = self.get_existing_order_ids(
            trades, right, action)
        remaining_contracts_info = list(
            filter(
                lambda c: c["contract"].conId not in
                existing_contract_order_ids,
                held_contracts_info,
            ))

        if len(remaining_contracts_info) > 0:
            remaining_contracts_info.sort(key=lambda c: c["contract"].conId)
            remaining_contracts = [
                c["contract"] for c in remaining_contracts_info
            ]
            qualified_contracts = self.ib.qualifyContracts(
                *remaining_contracts)
            tickers = self.ib.reqTickers(*qualified_contracts)
            tickers.sort(key=lambda t: t.contract.conId)
            for idx, ticker in enumerate(tickers):
                price = midpoint_or_market_price(ticker)
                avg = remaining_contracts_info[idx]["avg"]
                if not util.isNan(price):
                    quantity = remaining_contracts_info[idx]["position"]
                    order = LimitOrder(
                        action,
                        quantity,
                        price,
                        algoStrategy="Adaptive",
                        algoParams=[
                            TagValue(tag="adaptivePriority", value="Normal")
                        ],
                        tif="DAY",
                    )
                    contract = ticker.contract
                    trade = self.ib.placeOrder(contract, order)
                    trade.filledEvent += log_fill
                    self.log_order(contract, quantity, price, action, avg)
                else:
                    with open("logs/errors.txt", "a") as f:
                        f.write(
                            f"Error selling {right} at {self._nope_value} | {self._underlying_price}\n"
                        )

    def exit_positions(self):
        if self._nope_value > self.config["nope"]["long_exit"]:
            self.sell_held_contracts("C")
        if self._nope_value < self.config["nope"]["short_exit"]:
            self.sell_held_contracts("P")

    def run_ib(self):
        async def ib_periodic():
            async def enter_pos():
                try:
                    self.enter_positions()
                except Exception as e:
                    log_exception(e, "enter_positions")

            async def exit_pos():
                try:
                    self.exit_positions()
                except Exception as e:
                    log_exception(e, "exit_positions")

            while True:
                await asyncio.gather(asyncio.sleep(60), enter_pos(),
                                     exit_pos())

        loop = asyncio.get_event_loop()
        return loop.create_task(ib_periodic())

    def run_qt_tasks(self):
        async def nope_periodic():
            async def fetch_and_report():
                try:
                    self.set_nope_value()
                except Exception as e:
                    log_exception(e, "set_nope_value")

                curr_date, curr_dt = get_datetime_for_logging()
                with open(f"logs/{curr_date}.txt", "a") as f:
                    f.write(
                        f"NOPE @ {self._nope_value} | Stock Price @ {self._underlying_price} | {curr_dt}\n"
                    )

            while True:
                await asyncio.gather(asyncio.sleep(60), fetch_and_report())

        async def token_refresh_periodic():
            async def refresh_token():
                try:
                    self.qt.refresh_access_token()
                except Exception as e:
                    log_exception(e, "refresh_token")

            while True:
                await asyncio.sleep(600)
                await refresh_token()

        loop = asyncio.get_event_loop()
        loop.create_task(nope_periodic())
        loop.create_task(token_refresh_periodic())

    def execute(self):
        self.req_market_data()
        return self.run_ib()
Example #4
0
class NopeStrategy:
    QT_ACCESS_TOKEN = 'qt/access_token.yml'
    SYMBOL = "SPY"

    def __init__(self, config, ib: IB):
        self.config = config
        self.ib = ib
        self._nope_value = 0
        self._underlying_price = 0
        self.qt = QuestradeClient(token_yaml=self.QT_ACCESS_TOKEN)
        self.run_qt_tasks()

    def req_market_data(self):
        # https://interactivebrokers.github.io/tws-api/market_data_type.html
        self.ib.reqMarketDataType(1)
        self.ib.reqAllOpenOrders()
        self.ib.reqPositions()

    def set_nope_value(self):
        self._nope_value, self._underlying_price = self.qt.get_nope()

    def get_portfolio(self):
        portfolio = self.ib.portfolio()
        # Filter out non-SPY contracts
        portfolio = [item for item in portfolio
                     if item.contract.symbol == self.SYMBOL]
        return portfolio

    def get_trades(self):
        trades = self.ib.openTrades()
        trades = [t for t in trades
                  if t.isActive() and t.contract.symbol == self.SYMBOL]
        return trades

    # From thetagang
    # https://github.com/brndnmtthws/thetagang
    def wait_for_trade_submitted(self, trade):
        while_n_times(
            lambda: trade.orderStatus.status
            not in [
                "Submitted",
                "Filled",
                "ApiCancelled",
                "Cancelled",
                "PreSubmitted"
            ],
            lambda: self.ib.waitOnUpdate(timeout=5),
            25
        )
        return trade

    # From thetagang
    # https://github.com/brndnmtthws/thetagang
    def find_eligible_contracts(self, symbol, right):
        EXCHANGE = 'SMART'
        MAX_STRIKE_OFFSET = 5

        stock = Stock(symbol, EXCHANGE, currency="USD")
        self.ib.qualifyContracts(stock)
        [ticker] = self.ib.reqTickers(stock)
        ticker_value = ticker.marketPrice()
        chains = self.ib.reqSecDefOptParams(stock.symbol, "", stock.secType, stock.conId)
        chain = next(c for c in chains if c.exchange == EXCHANGE)

        def valid_strike(strike):
            if strike % 1 == 0:
                if right == 'C':
                    max_ntm_call_strike = ticker_value + MAX_STRIKE_OFFSET
                    return ticker_value <= strike <= max_ntm_call_strike
                elif right == 'P':
                    min_ntm_put_strike = ticker_value - MAX_STRIKE_OFFSET
                    return min_ntm_put_strike <= strike <= ticker_value
            return False

        strikes = [strike for strike in chain.strikes if valid_strike(strike)]

        # TODO: Remove slicing once contract selection algorithm implemented
        exp_offset = self.config["nope"]["expiry_offset"]
        expirations = sorted(exp for exp in chain.expirations)[exp_offset:exp_offset + 1]

        contracts = [Option(self.SYMBOL, expiration, strike, right, EXCHANGE, tradingClass=self.SYMBOL)
                     for expiration in expirations
                     for strike in strikes]

        return contracts

    def get_total_buys(self, trades, right):
        return sum(map(lambda t: t.order.totalQuantity,
                   filter(lambda t: t.contract.right == right and t.order.action == 'BUY', trades)))

    def get_total_position(self, portfolio, right):
        held_contracts = self.get_held_contracts(portfolio, right)
        return sum(map(lambda c: c['position'], held_contracts))

    def enter_positions(self):
        portfolio = self.get_portfolio()
        trades = self.get_trades()
        curr_date, curr_dt = get_datetime_for_logging()
        if self._nope_value < self.config["nope"]["long_enter"]:
            held_calls = self.get_total_position(portfolio, 'C')
            existing_order_quantity = self.get_total_buys(trades, 'C')
            total_buys = held_calls + existing_order_quantity
            if total_buys < self.config["nope"]["call_limit"]:
                contracts = self.find_eligible_contracts(self.SYMBOL, 'C')
                # TODO: Implement contract selection from eligible candidiates
                contract_to_buy = contracts[self.config["nope"]["call_strike_offset"]]
                qualified_contracts = self.ib.qualifyContracts(contract_to_buy)
                tickers = self.ib.reqTickers(*qualified_contracts)
                if len(tickers) > 0:
                    price = midpoint_or_market_price(tickers[0])
                    call_contract = qualified_contracts[0]
                    if not util.isNan(price):
                        quantity = self.config["nope"]["call_quantity"]
                        order = LimitOrder('BUY', quantity, price,
                                           algoStrategy="Adaptive",
                                           algoParams=[TagValue(tag='adaptivePriority', value='Normal')],
                                           tif="DAY")
                        self.wait_for_trade_submitted(self.ib.placeOrder(call_contract, order))
                        with open(f"logs/{curr_date}-trade.txt", "a") as f:
                            f.write(f'Bought {quantity} {call_contract.strike}C{call_contract.lastTradeDateOrContractMonth} for {price * 100} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
                    else:
                        with open("logs/errors.txt", "a") as f:
                            f.write(f'Error buying call at {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
        elif self._nope_value > self.config["nope"]["short_enter"]:
            held_puts = self.get_total_position(portfolio, 'P')
            existing_order_quantity = self.get_total_buys(trades, 'P')
            total_buys = held_puts + existing_order_quantity
            if total_buys < self.config["nope"]["put_limit"]:
                contracts = self.find_eligible_contracts(self.SYMBOL, 'P')
                # TODO: Implement contract selection from eligible candidates
                contract_to_buy = contracts[-self.config["nope"]["put_strike_offset"] - 1]
                qualified_contracts = self.ib.qualifyContracts(contract_to_buy)
                tickers = self.ib.reqTickers(*qualified_contracts)
                if len(tickers) > 0:
                    price = midpoint_or_market_price(tickers[0])
                    put_contract = qualified_contracts[0]
                    if not util.isNan(price):
                        quantity = self.config["nope"]["put_quantity"]
                        order = LimitOrder('BUY', quantity, price,
                                           algoStrategy="Adaptive",
                                           algoParams=[TagValue(tag='adaptivePriority', value='Normal')],
                                           tif="DAY")
                        self.wait_for_trade_submitted(self.ib.placeOrder(put_contract, order))
                        with open(f"logs/{curr_date}-trade.txt", "a") as f:
                            f.write(f'Bought {quantity} {put_contract.strike}P{put_contract.lastTradeDateOrContractMonth} for {price * 100} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
                    else:
                        with open("logs/errors.txt", "a") as f:
                            f.write(f'Error buying put at {self._nope_value} | {self._underlying_price} | {curr_dt}\n')

    def get_held_contracts(self, portfolio, right):
        return [c for c in map(lambda p: {'contract': p.contract, 'position': p.position, 'avg': p.averageCost}, portfolio)
                if c['contract'].right == right
                and c['position'] > 0]

    def get_existing_order_ids(self, trades, right, buy_or_sell):
        return set(map(lambda t: t.contract.conId,
                       filter(lambda t: t.contract.right == right and t.order.action == buy_or_sell, trades)))

    def exit_positions(self):
        portfolio = self.get_portfolio()
        trades = self.get_trades()
        curr_date, curr_dt = get_datetime_for_logging()
        if self._nope_value > self.config["nope"]["long_exit"]:
            held_calls = self.get_held_contracts(portfolio, 'C')
            existing_call_order_ids = self.get_existing_order_ids(trades, 'C', 'SELL')
            remaining_calls = list(filter(lambda c: c['contract'].conId not in existing_call_order_ids, held_calls))

            if len(remaining_calls) > 0:
                remaining_calls.sort(key=lambda c: c['contract'].conId)
                remaining_call_contracts = [c['contract'] for c in remaining_calls]
                qualified_contracts = self.ib.qualifyContracts(*remaining_call_contracts)
                tickers = self.ib.reqTickers(*qualified_contracts)
                tickers.sort(key=lambda t: t.contract.conId)
                for idx, ticker in enumerate(tickers):
                    price = midpoint_or_market_price(ticker)
                    if not util.isNan(price):
                        quantity = remaining_calls[idx]['position']
                        order = LimitOrder("SELL", quantity, price,
                                           algoStrategy="Adaptive",
                                           algoParams=[TagValue(tag='adaptivePriority', value='Normal')],
                                           tif="DAY")
                        call_contract = ticker.contract
                        self.wait_for_trade_submitted(self.ib.placeOrder(call_contract, order))
                        with open(f"logs/{curr_date}-trade.txt", "a") as f:
                            f.write(f'Sold {quantity} {call_contract.strike}C{call_contract.lastTradeDateOrContractMonth} ({remaining_calls[idx]["avg"]} average) for {price * 100} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
                    else:
                        with open("logs/errors.txt", "a") as f:
                            f.write(f'Error selling call at {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
        if self._nope_value < self.config["nope"]["short_exit"]:
            held_puts = self.get_held_contracts(portfolio, 'P')
            existing_put_order_ids = self.get_existing_order_ids(trades, 'P', 'SELL')
            remaining_puts = list(filter(lambda c: c['contract'].conId not in existing_put_order_ids, held_puts))

            if len(remaining_puts) > 0:
                remaining_puts.sort(key=lambda c: c['contract'].conId)
                remaining_put_contracts = [c['contract'] for c in remaining_puts]
                qualified_contracts = self.ib.qualifyContracts(*remaining_put_contracts)
                tickers = self.ib.reqTickers(*qualified_contracts)
                tickers.sort(key=lambda t: t.contract.conId)
                for idx, ticker in enumerate(tickers):
                    price = midpoint_or_market_price(ticker)
                    if not util.isNan(price):
                        quantity = remaining_puts[idx]['position']
                        order = LimitOrder("SELL", quantity, price,
                                           algoStrategy="Adaptive",
                                           algoParams=[TagValue(tag='adaptivePriority', value='Normal')],
                                           tif="DAY")
                        put_contract = ticker.contract
                        self.wait_for_trade_submitted(self.ib.placeOrder(put_contract, order))
                        with open(f"logs/{curr_date}-trade.txt", "a") as f:
                            f.write(f'Sold {quantity} {put_contract.strike}P{put_contract.lastTradeDateOrContractMonth} ({remaining_puts[idx]["avg"]} average) for {price * 100} each, {self._nope_value} | {self._underlying_price} | {curr_dt}\n')
                    else:
                        with open("logs/errors.txt", "a") as f:
                            f.write(f'Error selling put at {self._nope_value} | {self._underlying_price} | {curr_dt}\n')

    def run_ib(self):
        async def ib_periodic():
            async def enter_pos():
                try:
                    self.enter_positions()
                except Exception as e:
                    log_exception(e, "enter_positions")

            async def exit_pos():
                try:
                    self.exit_positions()
                except Exception as e:
                    log_exception(e, "exit_positions")

            while True:
                await asyncio.gather(asyncio.sleep(60), enter_pos(), exit_pos())

        loop = asyncio.get_event_loop()
        return loop.create_task(ib_periodic())

    def run_qt_tasks(self):
        async def nope_periodic():
            async def fetch_and_report():
                try:
                    self.set_nope_value()
                except Exception as e:
                    log_exception(e, "set_nope_value")

                curr_date, curr_dt = get_datetime_for_logging()
                with open(f"logs/{curr_date}.txt", "a") as f:
                    f.write(f'NOPE @ {self._nope_value} | Stock Price @ {self._underlying_price} | {curr_dt}\n')
            while True:
                await asyncio.gather(asyncio.sleep(60), fetch_and_report())

        async def token_refresh_periodic():
            async def refresh_token():
                try:
                    self.qt.refresh_access_token()
                except Exception as e:
                    log_exception(e, "refresh_token")

            while True:
                await asyncio.sleep(600)
                await refresh_token()

        loop = asyncio.get_event_loop()
        loop.create_task(nope_periodic())
        loop.create_task(token_refresh_periodic())

    def execute(self):
        self.req_market_data()
        return self.run_ib()