yatws 0.1.7

Yet Another TWS (Interactive Brokers TWS API) Implementation
Documentation
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import datetime
import logging
import sys
import time
import threading

from ibapi import wrapper
from ibapi.client import EClient
from ibapi.contract import Contract
from ibapi.order import Order
from ibapi.utils import iswrapper

# --- Constants ---
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 4002
DEFAULT_CLIENT_ID = 103  # Use a different ID than other test scripts

# --- Logging Setup ---
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)  # Keep DEBUG level
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)


# --- Test Application Class ---
class TestApp(wrapper.EWrapper, EClient):
    def __init__(self):
        wrapper.EWrapper.__init__(self)
        EClient.__init__(self, wrapper=self)
        self.nKeybInt = 0
        self.started = False
        self.nextValidOrderId = -1
        self.permId2ord = {}
        self.reqId2nErr = {}
        self.globalCancelOnly = False
        self._my_errors = {}

        # For what-if order results
        self.what_if_order_id = -1
        self.what_if_results = {}
        self.what_if_finished = threading.Event()

    @iswrapper
    def connectAck(self):
        log.info("Connection acknowledged.")

    @iswrapper
    def nextValidId(self, orderId: int):
        super().nextValidId(orderId)
        self.nextValidOrderId = orderId
        log.info("nextValidId: %d", orderId)
        self.start()  # Start the main logic after getting nextValidId

    def start(self):
        if self.started:
            return
        self.started = True
        log.info("Executing what-if order request")
        self.request_vwap_what_if_order_test()
        log.info("Request finished")

    def keyboardInterrupt(self):
        self.nKeybInt += 1
        if self.nKeybInt == 1:
            log.info("Keyboard interrupt detected. Disconnecting...")
            self.done = True
            self.disconnect()
        else:
            log.info("Forcing exit...")
            sys.exit(0)

    @iswrapper
    def error(self, reqId: int, errorCode: int, errorString: str, advancedOrderReject=""):
        super().error(reqId, errorCode, errorString, advancedOrderReject)
        if advancedOrderReject:
            log.error("Error. Id: %d, Code: %d, Msg: %s, AdvancedOrderReject: %s",
                     reqId, errorCode, errorString, advancedOrderReject)
        else:
            log.error("Error. Id: %d, Code: %d, Msg: %s", reqId, errorCode, errorString)

        # Errors related to what-if order
        if reqId == self.what_if_order_id:
            log.error(f"What-if order request {reqId} failed with error {errorCode}: {errorString}")
            self.what_if_finished.set()  # Signal completion on error

        # Store errors keyed by reqId
        if reqId > 0:
            self.reqId2nErr[reqId] = self.reqId2nErr.get(reqId, 0) + 1
            self._my_errors[reqId] = (errorCode, errorString)

    # --- Order Status Callbacks ---
    @iswrapper
    def orderStatus(self, orderId: int, status: str, filled: float, remaining: float,
                   avgFillPrice: float, permId: int, parentId: int, lastFillPrice: float,
                   clientId: int, whyHeld: str, mktCapPrice: float):
        log.info("CALLBACK orderStatus: OrderId: %d, Status: %s, Filled: %g, Remaining: %g, "
                "AvgFillPrice: %g, PermId: %d, ParentId: %d, LastFillPrice: %g, "
                "ClientId: %d, WhyHeld: %s, MktCapPrice: %g",
                orderId, status, filled, remaining, avgFillPrice, permId, parentId,
                lastFillPrice, clientId, whyHeld, mktCapPrice)

    @iswrapper
    def openOrder(self, orderId: int, contract: Contract, order: Order, orderState):
        log.info("CALLBACK openOrder: OrderId: %d, Symbol: %s, SecType: %s, Status: %s",
                orderId, contract.symbol, contract.secType, orderState.status)

        if orderId == self.what_if_order_id and order.whatIf:
            log.info("Received what-if order results for order %d", orderId)

            # Extract the margin and commission information
            self.what_if_results = {
                'orderId': orderId,
                'status': orderState.status,
                'initMarginBefore': orderState.initMarginBefore,
                'maintMarginBefore': orderState.maintMarginBefore,
                'equityWithLoanBefore': orderState.equityWithLoanBefore,
                'initMarginAfter': orderState.initMarginAfter,
                'maintMarginAfter': orderState.maintMarginAfter,
                'equityWithLoanAfter': orderState.equityWithLoanAfter,
                'initMarginChange': orderState.initMarginChange,
                'maintMarginChange': orderState.maintMarginChange,
                'equityWithLoanChange': orderState.equityWithLoanChange,
                'commission': orderState.commission,
                'commissionCurrency': orderState.commissionCurrency,
                'warningText': orderState.warningText
            }

            log.info("What-if results:")
            log.info("  Initial Margin After: %s", orderState.initMarginAfter)
            log.info("  Maintenance Margin After: %s", orderState.maintMarginAfter)
            log.info("  Commission: %s %s", orderState.commission, orderState.commissionCurrency)
            if orderState.warningText:
                log.info("  Warning: %s", orderState.warningText)

            self.what_if_finished.set()  # Signal completion

    @iswrapper
    def openOrderEnd(self):
        log.info("CALLBACK openOrderEnd")

    # --- Test Logic ---
    # 3·1·0·AAPL·STK··0.0···SMART··USD·····BUY·5000·LMT·150.0······0··1·0·0·0·0·0·0·0··0·······0··-1·0···0···0·0··0······0·····0···········0···0·0·Vwap·6·maxPctVol·0.10·startTime·20250525-18:44:23·endTime·20250526-00:44:23·allowPastEndTime·0·noTakeLiq·0·speedUp·0··1··0·0·0·0··1.7976931348623157e+308·1.7976931348623157e+308·1.7976931348623157e+308·1.7976931348623157e+308·1.7976931348623157e+308·0····1.7976931348623157e+308·····0·0·0··2147483647·2147483647·0····0··2147483647·
    def request_vwap_what_if_order_test(self):
        self.what_if_order_id = self.nextValidOrderId
        self.nextValidOrderId += 1
        log.info(f"Requesting VWAP what-if order with orderId: {self.what_if_order_id}")

        # Create contract for AAPL stock
        contract = Contract()
        contract.symbol = "AAPL"
        contract.secType = "STK"
        contract.exchange = "SMART"
        contract.currency = "USD"

        # Create VWAP algorithm order
        order = Order()
        order.action = "BUY"
        order.totalQuantity = 5000
        order.orderType = "LMT"
        order.lmtPrice = 150.0
        order.whatIf = True  # This makes it a what-if order
        order.transmit = True

        # Set up VWAP algorithm parameters
        order.algoStrategy = "Vwap"
        order.algoParams = []

        # Calculate start and end times (1 hour from now, ending 6 hours later)
        now = datetime.datetime.utcnow()
        start_time = now + datetime.timedelta(hours=1)
        end_time = start_time + datetime.timedelta(hours=6)

        # Format times as TWS expects: "YYYYMMDD-HH:MM:SS" (UTC notation with dash)
        start_time_str = start_time.strftime("%Y%m%d-%H:%M:%S")
        end_time_str = end_time.strftime("%Y%m%d-%H:%M:%S")

        # Add VWAP algorithm parameters
        from ibapi.tag_value import TagValue
        order.algoParams.append(TagValue("maxPctVol", "0.10"))  # 10% of volume
        order.algoParams.append(TagValue("startTime", start_time_str))
        order.algoParams.append(TagValue("endTime", end_time_str))
        order.algoParams.append(TagValue("allowPastEndTime", "0"))  # false
        order.algoParams.append(TagValue("noTakeLiq", "0"))  # false
        order.algoParams.append(TagValue("speedUp", "0"))  # false

        log.info(f"VWAP Algorithm Parameters:")
        log.info(f"  Max % of Volume: 10%")
        log.info(f"  Start Time: {start_time_str}")
        log.info(f"  End Time: {end_time_str}")
        log.info(f"  Allow Past End Time: False")
        log.info(f"  No Take Liquidity: False")
        log.info(f"  Speed Up: False")

        log.info(f"Placing what-if order for {order.totalQuantity} shares of {contract.symbol} "
                f"at ${order.lmtPrice} using VWAP algorithm")

        # Place the what-if order
        self.placeOrder(self.what_if_order_id, contract, order)

        log.info(f"What-if order request sent for ID: {self.what_if_order_id}")


# --- Main Execution ---
def main():
    parser = argparse.ArgumentParser(description="IB API VWAP What-If Order Test")
    parser.add_argument("--host", default=DEFAULT_HOST, help="Host address")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Port number")
    parser.add_argument("--clientId", type=int, default=DEFAULT_CLIENT_ID, help="Client ID")
    args = parser.parse_args()

    log.info("Starting VWAP What-If Order Test")
    log.info(f"Connecting to {args.host}:{args.port} with clientId {args.clientId}")

    try:
        app = TestApp()
        log.info(f"Logger level set to: {logging.getLevelName(log.level)}")

        app.connect(args.host, args.port, args.clientId)
        log.info("Connection initiated. Server version: %s", app.serverVersion())

        # Start the EClient message loop in a separate thread
        log.info("Starting EClient.run() message loop in background thread...")
        thread = threading.Thread(target=app.run, daemon=True)
        thread.start()
        log.info("EClient.run() thread started.")

        # --- Wait for the what-if order results ---
        wait_timeout_secs = 60  # Timeout for the what-if order
        log.info(f"Main thread waiting up to {wait_timeout_secs}s for what-if order results...")
        wait_successful = app.what_if_finished.wait(timeout=wait_timeout_secs)

        if wait_successful:
            log.info(f"Main thread: What-if order {app.what_if_order_id} completed.")

            if app.what_if_results:
                results = app.what_if_results
                log.info("=== WHAT-IF ORDER RESULTS ===")
                log.info(f"Order ID: {results['orderId']}")
                log.info(f"Status: {results['status']}")

                if results['initMarginAfter']:
                    log.info(f"Initial Margin After: ${results['initMarginAfter']}")
                if results['maintMarginAfter']:
                    log.info(f"Maintenance Margin After: ${results['maintMarginAfter']}")
                if results['commission']:
                    log.info(f"Estimated Commission: {results['commission']} {results['commissionCurrency']}")

                if results['initMarginChange']:
                    log.info(f"Initial Margin Change: ${results['initMarginChange']}")
                if results['maintMarginChange']:
                    log.info(f"Maintenance Margin Change: ${results['maintMarginChange']}")
                if results['equityWithLoanChange']:
                    log.info(f"Equity with Loan Change: ${results['equityWithLoanChange']}")

                if results['warningText']:
                    log.warning(f"Warning: {results['warningText']}")

                log.info("=============================")
            else:
                log.warning("What-if order completed but no results received")
        else:
            log.warning(f"Main thread: What-if order {app.what_if_order_id} timed out!")

        # --- Disconnect and wait for thread exit ---
        log.info("Main thread: Disconnecting...")
        if app.isConnected():
            app.disconnect()
        else:
            log.info("Main thread: Already disconnected.")

        # Wait for the EClient thread to finish after disconnect
        join_timeout_secs = 10
        log.info(f"Main thread waiting for EClient thread to join (timeout {join_timeout_secs}s)...")
        thread.join(timeout=join_timeout_secs)
        log.info("Main thread finished waiting for EClient thread.")

        if thread.is_alive():
            log.warning("EClient thread did not exit cleanly after disconnect and join timeout.")
            if app.isConnected():
                log.warning("Attempting disconnect again...")
                app.disconnect()
        else:
            log.info("EClient thread exited cleanly.")

        log.info("VWAP What-If Order Test finished.")

    except Exception as e:
        log.exception("Unhandled exception in main:")
    finally:
        log.info("Exiting.")


if __name__ == "__main__":
    main()