src/core/io/decoder.ts

Summary

Maintainability
F
1 mo
Test Coverage
import { Contract } from "../../api/contract/contract";
import { ContractDescription } from "../../api/contract/contractDescription";
import { ContractDetails } from "../../api/contract/contractDetails";
import { DeltaNeutralContract } from "../../api/contract/deltaNeutralContract";
import {
  FundAssetType,
  FundDistributionPolicyIndicator,
} from "../../api/contract/fund";
import DepthMktDataDescription from "../../api/data/container/depth-mkt-data-description";
import FamilyCode from "../../api/data/container/family-code";
import NewsProvider from "../../api/data/container/news-provider";
import SoftDollarTier from "../../api/data/container/soft-dollar-tier";
import TagValue from "../../api/data/container/tag-value";
import { EventName } from "../../api/data/enum/event-name";
import MIN_SERVER_VER from "../../api/data/enum/min-server-version";
import OptionType from "../../api/data/enum/option-type";
import SecType from "../../api/data/enum/sec-type";
import { HistoricalSession } from "../../api/historical/HistoricalSession";
import { HistogramEntry } from "../../api/historical/histogramEntry";
import { HistoricalTick } from "../../api/historical/historicalTick";
import { HistoricalTickBidAsk } from "../../api/historical/historicalTickBidAsk";
import { HistoricalTickLast } from "../../api/historical/historicalTickLast";
import { TickType } from "../../api/market/tickType";
import ExecutionCondition from "../../api/order/condition/execution-condition";
import MarginCondition from "../../api/order/condition/margin-condition";
import PercentChangeCondition from "../../api/order/condition/percent-change-condition";
import PriceCondition from "../../api/order/condition/price-condition";
import TimeCondition from "../../api/order/condition/time-condition";
import VolumeCondition from "../../api/order/condition/volume-condition";
import { ConjunctionConnection } from "../../api/order/enum/conjunction-connection";
import OrderAction from "../../api/order/enum/order-action";
import { OrderConditionType } from "../../api/order/enum/order-condition-type";
import { OrderStatus } from "../../api/order/enum/order-status";
import { OrderType, isPegBenchOrder } from "../../api/order/enum/orderType";
import { TimeInForce } from "../../api/order/enum/tif";
import { TriggerMethod } from "../../api/order/enum/trigger-method";
import { Execution } from "../../api/order/execution";
import { Order } from "../../api/order/order";
import { OrderState } from "../../api/order/orderState";
import { CommissionReport } from "../../api/report/commissionReport";
import { ErrorCode } from "../../common/errorCode";
import { IN_MSG_ID } from "./enum/in-msg-id";

/**
 * @internal
 *
 * Verify if the value is a valid OptionType.
 * Returns the value is valid, undefined otherwise.
 */
function validateOptionType(v: OptionType): OptionType | undefined {
  return Object.values(OptionType).indexOf(v) !== -1 ? v : undefined;
}

/**
 * @internal
 *
 * An underrun error on the input de-serialization.
 */
export class UnderrunError extends Error {
  constructor(public message: string = "An underrun error has occurred") {
    super();
  }

  readonly stack = new Error().stack;
  readonly name = "UnderrunError";
}

/**
 * @internal
 *
 * An item on the emit queue.
 */
interface EmitQueueItem {
  /** Event name. */
  name: EventName;

  /** Event arguments. */
  args: unknown[];
}

/**
 * @internal
 *
 * Callback interface of the [[Decoder]].
 */
export interface DecoderCallbacks {
  /** Get the IB API server version. */
  readonly serverVersion: number;

  /**
   * Emit an event to public API interface.
   *
   * @param eventName Event name.
   * @param args Event arguments.
   */
  emitEvent(eventName: EventName, ...args: unknown[]): void;

  /**
   * Emit an error event to public API interface.
   *
   * @param errMsg The error test message.
   * @param code The code identifying the error.
   * @param reqId The request identifier which generated the error.
   * @param advancedOrderReject An object providing more information in case of an order rejection
   */
  emitError(
    errMsg: string,
    code: number,
    reqId: number,
    advancedOrderReject?: unknown,
  ): void;

  /**
   * Emit an information message event to public API interface.
   *
   * @param message The message text.
   * @param code The message code.
   */
  emitInfo(message: string, code: number): void;
}

/**
 * @internal
 *
 * Class for decoding token data to messages and emitting events it to the
 * [[Controller]] event queue.
 */
export class Decoder {
  /**
   * Create an [[Incoming]] object.
   *
   * @param callback A [[DecoderCallbacks]] implementation.
   */
  constructor(private callback: DecoderCallbacks) {}

  /**
   * Input data queue.
   *
   * If the value is a string, this is a tokens as received from TWS / IB Gateway.
   * If the value is undefined, this signals the boundary (start or end) of a message (used with V100 protocol only).
   */
  private dataQueue: (string | undefined)[] = [];

  /** Data emit queue (data to be emitted to controller). */
  private emitQueue: EmitQueueItem[] = [];

  /**
   * Add a new message to queue.
   *
   * Used on V100 protocol.
   */
  enqueueMessage(tokens: string[]): void {
    this.dataQueue.push(undefined); // signal start boundary
    this.dataQueue = this.dataQueue.concat(tokens);
    this.dataQueue.push(undefined); // signal end boundary
  }

  /**
   * Add new tokens to queue.
   *
   * Used on pre-V100 protocol.
   */
  enqueueTokens(tokens: string[]): void {
    this.dataQueue = this.dataQueue.concat(tokens);
  }

  /**
   * Process a message on data queue.
   */
  processMsg(msgId: IN_MSG_ID): void {
    switch (msgId) {
      case IN_MSG_ID.TICK_PRICE:
        return this.decodeMsg_TICK_PRICE();
      case IN_MSG_ID.TICK_SIZE:
        return this.decodeMsg_TICK_SIZE();
      case IN_MSG_ID.ORDER_STATUS:
        return this.decodeMsg_ORDER_STATUS();
      case IN_MSG_ID.ERR_MSG:
        return this.decodeMsg_ERR_MSG();
      case IN_MSG_ID.OPEN_ORDER:
        return this.decodeMsg_OPEN_ORDER();
      case IN_MSG_ID.ACCT_VALUE:
        return this.decodeMsg_ACCT_VALUE();
      case IN_MSG_ID.PORTFOLIO_VALUE:
        return this.decodeMsg_PORTFOLIO_VALUE();
      case IN_MSG_ID.ACCT_UPDATE_TIME:
        return this.decodeMsg_ACCT_UPDATE_TIME();
      case IN_MSG_ID.NEXT_VALID_ID:
        return this.decodeMsg_NEXT_VALID_ID();
      case IN_MSG_ID.CONTRACT_DATA:
        return this.decodeMsg_CONTRACT_DATA();
      case IN_MSG_ID.EXECUTION_DATA:
        return this.decodeMsg_EXECUTION_DATA();
      case IN_MSG_ID.MARKET_DEPTH:
        return this.decodeMsg_MARKET_DEPTH();
      case IN_MSG_ID.MARKET_DEPTH_L2:
        return this.decodeMsg_MARKET_DEPTH_L2();
      case IN_MSG_ID.NEWS_BULLETINS:
        return this.decodeMsg_NEWS_BULLETINS();
      case IN_MSG_ID.MANAGED_ACCTS:
        return this.decodeMsg_MANAGED_ACCTS();
      case IN_MSG_ID.RECEIVE_FA:
        return this.decodeMsg_RECEIVE_FA();
      case IN_MSG_ID.HISTORICAL_DATA:
        return this.decodeMsg_HISTORICAL_DATA();
      case IN_MSG_ID.BOND_CONTRACT_DATA:
        return this.decodeMsg_BOND_CONTRACT_DATA();
      case IN_MSG_ID.SCANNER_PARAMETERS:
        return this.decodeMsg_SCANNER_PARAMETERS();
      case IN_MSG_ID.SCANNER_DATA:
        return this.decodeMsg_SCANNER_DATA();
      case IN_MSG_ID.TICK_OPTION_COMPUTATION:
        return this.decodeMsg_TICK_OPTION_COMPUTATION();
      case IN_MSG_ID.TICK_GENERIC:
        return this.decodeMsg_TICK_GENERIC();
      case IN_MSG_ID.TICK_STRING:
        return this.decodeMsg_TICK_STRING();
      case IN_MSG_ID.TICK_EFP:
        return this.decodeMsg_TICK_EFP();
      case IN_MSG_ID.CURRENT_TIME:
        return this.decodeMsg_CURRENT_TIME();
      case IN_MSG_ID.REAL_TIME_BARS:
        return this.decodeMsg_REAL_TIME_BARS();
      case IN_MSG_ID.FUNDAMENTAL_DATA:
        return this.decodeMsg_FUNDAMENTAL_DATA();
      case IN_MSG_ID.CONTRACT_DATA_END:
        return this.decodeMsg_CONTRACT_DATA_END();
      case IN_MSG_ID.OPEN_ORDER_END:
        return this.decodeMsg_OPEN_ORDER_END();
      case IN_MSG_ID.ACCT_DOWNLOAD_END:
        return this.decodeMsg_ACCT_DOWNLOAD_END();
      case IN_MSG_ID.EXECUTION_DATA_END:
        return this.decodeMsg_EXECUTION_DATA_END();
      case IN_MSG_ID.DELTA_NEUTRAL_VALIDATION:
        return this.decodeMsg_DELTA_NEUTRAL_VALIDATION();
      case IN_MSG_ID.TICK_SNAPSHOT_END:
        return this.decodeMsg_TICK_SNAPSHOT_END();
      case IN_MSG_ID.MARKET_DATA_TYPE:
        return this.decodeMsg_MARKET_DATA_TYPE();
      case IN_MSG_ID.COMMISSION_REPORT:
        return this.decodeMsg_COMMISSION_REPORT();
      case IN_MSG_ID.POSITION:
        return this.decodeMsg_POSITION();
      case IN_MSG_ID.POSITION_END:
        return this.decodeMsg_POSITION_END();
      case IN_MSG_ID.ACCOUNT_SUMMARY:
        return this.decodeMsg_ACCOUNT_SUMMARY();
      case IN_MSG_ID.ACCOUNT_SUMMARY_END:
        return this.decodeMsg_ACCOUNT_SUMMARY_END();
      /* For IB's internal purpose - not implement:
      case IN_MSG_ID.VERIFY_MESSAGE_API:
      case IN_MSG_ID.VERIFY_COMPLETED:
      */
      case IN_MSG_ID.DISPLAY_GROUP_LIST:
        return this.decodeMsg_DISPLAY_GROUP_LIST();
      case IN_MSG_ID.DISPLAY_GROUP_UPDATED:
        return this.decodeMsg_DISPLAY_GROUP_UPDATED();
      /* For IB's internal purpose - not implement:
      case IN_MSG_ID.VERIFY_AND_AUTH_MESSAGE_API:
      case IN_MSG_ID.VERIFY_AND_AUTH_COMPLETED
      */
      case IN_MSG_ID.POSITION_MULTI:
        return this.decodeMsg_POSITION_MULTI();
      case IN_MSG_ID.POSITION_MULTI_END:
        return this.decodeMsg_POSITION_MULTI_END();
      case IN_MSG_ID.ACCOUNT_UPDATE_MULTI:
        return this.decodeMsg_ACCOUNT_UPDATE_MULTI();
      case IN_MSG_ID.ACCOUNT_UPDATE_MULTI_END:
        return this.decodeMsg_ACCOUNT_UPDATE_MULTI_END();
      case IN_MSG_ID.SECURITY_DEFINITION_OPTION_PARAMETER:
        return this.decodeMsg_SECURITY_DEFINITION_OPTION_PARAMETER();
      case IN_MSG_ID.SECURITY_DEFINITION_OPTION_PARAMETER_END:
        return this.decodeMsg_SECURITY_DEFINITION_OPTION_PARAMETER_END();
      case IN_MSG_ID.SOFT_DOLLAR_TIERS:
        return this.decodeMsg_SOFT_DOLLAR_TIERS();
      case IN_MSG_ID.FAMILY_CODES:
        return this.decodeMsg_FAMILY_CODES();
      case IN_MSG_ID.SYMBOL_SAMPLES:
        return this.decodeMsg_SYMBOL_SAMPLES();
      case IN_MSG_ID.MKT_DEPTH_EXCHANGES:
        return this.decodeMsg_MKT_DEPTH_EXCHANGES();
      case IN_MSG_ID.TICK_REQ_PARAMS:
        return this.decodeMsg_TICK_REQ_PARAMS();
      case IN_MSG_ID.SMART_COMPONENTS:
        return this.decodeMsg_SMART_COMPONENTS();
      case IN_MSG_ID.NEWS_ARTICLE:
        return this.decodeMsg_NEWS_ARTICLE();
      case IN_MSG_ID.TICK_NEWS:
        return this.decodeMsg_TICK_NEWS();
      case IN_MSG_ID.NEWS_PROVIDERS:
        return this.decodeMsg_NEWS_PROVIDERS();
      case IN_MSG_ID.HISTORICAL_NEWS:
        return this.decodeMsg_HISTORICAL_NEWS();
      case IN_MSG_ID.HISTORICAL_NEWS_END:
        return this.decodeMsg_HISTORICAL_NEWS_END();
      case IN_MSG_ID.HEAD_TIMESTAMP:
        return this.decodeMsg_HEAD_TIMESTAMP();
      case IN_MSG_ID.HISTOGRAM_DATA:
        return this.decodeMsg_HISTOGRAM_DATA();
      case IN_MSG_ID.HISTORICAL_DATA_UPDATE:
        return this.decodeMsg_HISTORICAL_DATA_UPDATE();
      case IN_MSG_ID.REROUTE_MKT_DATA:
        return this.decodeMsg_REROUTE_MKT_DATA();
      case IN_MSG_ID.REROUTE_MKT_DEPTH:
        return this.decodeMsg_REROUTE_MKT_DEPTH();
      case IN_MSG_ID.MARKET_RULE:
        return this.decodeMsg_MARKET_RULE();
      case IN_MSG_ID.PNL:
        return this.decodeMsg_PNL();
      case IN_MSG_ID.PNL_SINGLE:
        return this.decodeMsg_PNL_SINGLE();
      case IN_MSG_ID.HISTORICAL_TICKS:
        return this.decodeMsg_HISTORICAL_TICKS();
      case IN_MSG_ID.HISTORICAL_TICKS_BID_ASK:
        return this.decodeMsg_HISTORICAL_TICKS_BID_ASK();
      case IN_MSG_ID.HISTORICAL_TICKS_LAST:
        return this.decodeMsg_HISTORICAL_TICKS_LAST();
      case IN_MSG_ID.TICK_BY_TICK:
        return this.decodeMsg_TICK_BY_TICK();
      case IN_MSG_ID.ORDER_BOUND:
        return this.decodeMsg_ORDER_BOUND();
      case IN_MSG_ID.COMPLETED_ORDER:
        return this.decodeMsg_COMPLETED_ORDER();
      case IN_MSG_ID.COMPLETED_ORDERS_END:
        return this.decodeMsg_COMPLETED_ORDERS_END();
      case IN_MSG_ID.REPLACE_FA_END:
        return this.decodeMsg_REPLACE_FA_END();
      case IN_MSG_ID.WSH_META_DATA:
        return this.decodeMsg_WSH_META_DATA();
      case IN_MSG_ID.WSH_EVENT_DATA:
        return this.decodeMsg_WSH_EVENT_DATA();
      case IN_MSG_ID.HISTORICAL_SCHEDULE:
        return this.decodeMsg_HISTORICAL_SCHEDULE();
      case IN_MSG_ID.USER_INFO:
        return this.decodeMsg_USER_INFO();

      default:
        this.callback.emitError(
          `No parser implementation found for token: ${IN_MSG_ID[msgId]} (${msgId}).`,
          ErrorCode.UNKNOWN_ID,
          -1,
        );
    }
  }

  /**
   * Process the data queue and emit events.
   */
  process(): void {
    while (true) {
      // verify there is data to process

      if (!this.dataQueue.length) {
        break;
      }

      // clear event queue

      this.emitQueue = [];

      // check if there is a message boundary marker

      let verifyMessageBoundary = false;
      if (this.dataQueue[0] === undefined) {
        verifyMessageBoundary = true;
        this.dataQueue.shift();
      }

      let msgId: IN_MSG_ID = IN_MSG_ID.UNDEFINED;

      try {
        // process message (invoke decoder function)

        msgId = this.readInt();
        this.processMsg(msgId);

        // check if all of the message data was processed and drain any remaining tokens

        if (verifyMessageBoundary) {
          if (this.dataQueue[0] !== undefined) {
            this.callback.emitError(
              `Decoding error on ${
                IN_MSG_ID[msgId]
              }: unprocessed data left on queue (${JSON.stringify(
                this.dataQueue,
              )}). Please report to https://github.com/stoqey/ib`,
              ErrorCode.UNKNOWN_ID,
              -1,
            );
          }

          this.drainQueue();
        }
      } catch (e) {
        if (e.name !== "UnderrunError") {
          throw e;
        }

        if (verifyMessageBoundary) {
          this.callback.emitError(
            `Underrun error on ${IN_MSG_ID[msgId]}: ${e.message} Please report to https://github.com/stoqey/ib`,
            ErrorCode.UNKNOWN_ID,
            -1,
          );
        }

        this.drainQueue();
      }

      // Emit events

      const toEmit = this.emitQueue;
      this.emitQueue = [];
      toEmit.forEach((item) =>
        this.callback.emitEvent(item.name, ...item.args),
      );
    }
  }

  /**
   * Get the API server version.
   */
  private get serverVersion(): number {
    return this.callback.serverVersion;
  }

  private decodeUnicodeEscapedString(str: string) {
    let v = str;

    try {
      while (true) {
        const escapeIndex: number = v.indexOf("\\u");

        if (escapeIndex == -1 || v.length - escapeIndex < 6) {
          break;
        }

        const escapeString: string = v.substring(escapeIndex, escapeIndex + 6);
        const hexVal: number = parseInt(escapeString.replace("\\u", ""), 16);

        v = v.replace(escapeString, String.fromCharCode(hexVal));
      }
    } catch (e) {}

    return v;
  }

  /**
   * Read a string token from queue.
   */
  readStr(): string {
    if (this.dataQueue.length === 0) {
      throw new UnderrunError();
    }
    const val = this.dataQueue.shift();
    if (val === undefined) {
      throw new UnderrunError("End of message reached.");
    }
    return val;
  }

  /**
   * Read a token from queue and return it as boolean value.
   */
  readBool(): boolean {
    return parseInt(this.readStr(), 10) != 0;
  }

  /**
   * Read a token from queue and return it as floating point value.
   *
   * Returns 0 if the token is empty.
   * Returns undefined is the token is Number.MAX_VALUE.
   */
  readDouble(): number | undefined {
    const token = this.readStr();
    if (!token || token === "") {
      return 0;
    }
    const val = parseFloat(token);
    return val === Number.MAX_VALUE ? undefined : val;
  }

  /**
   * Read a token from queue and return it as floating point value.
   *
   * Returns undefined if the token is empty or is Number.MAX_VALUE.
   */
  readDecimal(): number | undefined {
    const token = this.readStr();
    if (!token || token === "") {
      return undefined;
    }
    const val = parseFloat(token.replaceAll(",", ""));
    return val === Number.MAX_VALUE || val === Infinity ? undefined : val;
  }

  /**
   * Read a token from queue and return it as floating point value.
   *
   * Returns undefined if the token is empty or Number.MAX_VALUE.
   */
  readDoubleOrUndefined(): number | undefined {
    const token = this.readStr();
    if (!token) {
      return undefined;
    }
    const val = parseFloat(token);
    return val === Number.MAX_VALUE ? undefined : val;
  }

  /**
   * Read a token from queue and return it as integer value.
   *
   * Returns 0 if the token is empty.
   */
  readInt(): number {
    const token = this.readStr();
    if (!token || token === "") {
      return 0;
    }
    const val = parseInt(token, 10);
    return val;
  }

  /**
   * Read a token from queue and return it as integer value.
   *
   * Returns Number.MAX_VALUE if the token is empty.
   * @deprecated readIntOrUndefined is probably what you are looking for
   */
  readIntMax = this.readIntOrUndefined;

  /**
   * Read a token from queue and return it as integer value.
   *
   * Returns undefined if the token is empty or `2147483647`.
   */
  readIntOrUndefined(): number | undefined {
    const token = this.readStr();
    if (!token || token === "") {
      return undefined;
    }
    const val = parseInt(token, 10);
    return val === 2147483647 ? undefined : val;
  }

  /**
   * Drain all tokens on queue until the start marker of a new message or until queue is empty.
   */
  private drainQueue(): void {
    // drain data up to message end marker or until queue is empty

    while (this.dataQueue.length && this.dataQueue[0] !== undefined) {
      this.dataQueue.shift();
    }

    if (this.dataQueue.length) {
      // drain the end marker

      this.dataQueue.shift();
    }
  }

  /**
   * Add tokens to the emit queue.
   */
  private emit(eventName: EventName, ...args: unknown[]): void {
    this.emitQueue.push({ name: eventName, args: args });
  }

  /**
   * Decode a TICK_PRICE message from data queue and emit a tickPrice and tickSize event.
   */
  private decodeMsg_TICK_PRICE(): void {
    // read from input queue

    const version = this.readInt();
    const tickerId = this.readInt();
    const tickType = this.readInt();
    const price = this.readDouble();

    let size = undefined;
    if (version >= 2) {
      size = this.readDecimal();
    }

    let canAutoExecute = undefined;
    if (version >= 3) {
      canAutoExecute = this.readBool();
    }

    // emit events

    this.emit(EventName.tickPrice, tickerId, tickType, price, canAutoExecute);

    let sizeTickType = undefined;
    if (version >= 2) {
      switch (tickType) {
        case TickType.BID:
          sizeTickType = TickType.BID_SIZE;
          break;
        case TickType.ASK:
          sizeTickType = TickType.ASK_SIZE;
          break;
        case TickType.LAST:
          sizeTickType = TickType.LAST_SIZE;
          break;
        case TickType.DELAYED_BID:
          sizeTickType = TickType.DELAYED_BID_SIZE;
          break;
        case TickType.DELAYED_ASK:
          sizeTickType = TickType.DELAYED_ASK_SIZE;
          break;
        case TickType.DELAYED_LAST:
          sizeTickType = TickType.DELAYED_LAST_SIZE;
          break;
      }
    }

    if (sizeTickType) {
      this.emit(EventName.tickSize, tickerId, sizeTickType, size);
    }
  }

  /**
   * Decode a TICK_SIZE message from data queue and emit an tickSize event.
   */
  private decodeMsg_TICK_SIZE(): void {
    this.readInt(); // version
    const tickerId = this.readInt();
    const tickType = this.readInt();
    const size = this.readDecimal();

    this.emit(EventName.tickSize, tickerId, tickType, size);
  }

  /**
   * Decode a ORDER_STATUS message from data queue and emit an orderStatus event.
   */
  private decodeMsg_ORDER_STATUS(): void {
    const version =
      this.serverVersion >= MIN_SERVER_VER.MARKET_CAP_PRICE
        ? Number.MAX_SAFE_INTEGER
        : this.readInt();
    const id = this.readInt();
    const status = this.readStr();
    const filled = this.readDecimal();
    const remaining = this.readDecimal();
    const avgFillPrice = this.readDouble();

    let permId: number | undefined = undefined;
    if (version >= 2) {
      permId = this.readInt();
    }

    let parentId: number | undefined = undefined;
    if (version >= 3) {
      parentId = this.readInt();
    }

    let lastFillPrice: number | undefined = undefined;
    if (version >= 4) {
      lastFillPrice = this.readDouble();
    }

    let clientId: number | undefined = undefined;
    if (version >= 5) {
      clientId = this.readInt();
    }

    let whyHeld: string | undefined = undefined;
    if (version >= 6) {
      whyHeld = this.readStr();
    }

    let mktCapPrice: number | undefined = undefined;
    if (this.serverVersion >= MIN_SERVER_VER.MARKET_CAP_PRICE) {
      mktCapPrice = this.readDouble();
    }

    this.emit(
      EventName.orderStatus,
      id,
      status,
      filled,
      remaining,
      avgFillPrice,
      permId,
      parentId,
      lastFillPrice,
      clientId,
      whyHeld,
      mktCapPrice,
    );
  }

  /**
   * Decode a ERR_MSG message from data queue and emit and error event.
   */
  private decodeMsg_ERR_MSG(): void {
    const version = this.readInt();
    if (version < 2) {
      const errorMsg = this.readStr();
      this.callback.emitError(errorMsg, -1, -1);
    } else {
      const id = this.readInt();
      const code = this.readInt();
      let msg = this.readStr();
      if (this.serverVersion >= MIN_SERVER_VER.ENCODE_MSG_ASCII7) {
        msg = this.decodeUnicodeEscapedString(msg);
      }
      let advancedOrderReject: unknown;
      if (this.serverVersion >= MIN_SERVER_VER.ADVANCED_ORDER_REJECT) {
        const advancedOrderRejectJson: string = this.readStr();
        if (advancedOrderRejectJson?.length > 0) {
          advancedOrderReject = JSON.parse(
            this.decodeUnicodeEscapedString(advancedOrderRejectJson),
          );
        }
      }

      if (id === -1) {
        this.callback.emitInfo(msg, code);
      } else {
        this.callback.emitError(msg, code, id, advancedOrderReject);
      }
    }
  }

  /**
   * Decode a OPEN_ORDER message from data queue and emit a openOrder event.
   */
  private decodeMsg_OPEN_ORDER(): void {
    // read version
    const version =
      this.serverVersion < MIN_SERVER_VER.ORDER_CONTAINER
        ? this.readInt()
        : this.serverVersion;

    const contract: Contract = {};
    const order: Order = {};
    const orderState: OrderState = {};
    const orderDecoder = new OrderDecoder(
      this,
      contract,
      order,
      orderState,
      version,
      this.serverVersion,
    );

    // read order id
    orderDecoder.readOrderId();

    // read contract fields
    orderDecoder.readContractFields();

    // read order fields
    orderDecoder.readAction();
    orderDecoder.readTotalQuantity();
    orderDecoder.readOrderType();
    orderDecoder.readLmtPrice();
    orderDecoder.readAuxPrice();
    orderDecoder.readTIF();
    orderDecoder.readOcaGroup();
    orderDecoder.readAccount();
    orderDecoder.readOpenClose();
    orderDecoder.readOrigin();
    orderDecoder.readOrderRef();
    orderDecoder.readClientId();
    orderDecoder.readPermId();
    orderDecoder.readOutsideRth();
    orderDecoder.readHidden();
    orderDecoder.readDiscretionaryAmount();
    orderDecoder.readGoodAfterTime();
    orderDecoder.skipSharesAllocation();
    orderDecoder.readFAParams();
    orderDecoder.readModelCode();
    orderDecoder.readGoodTillDate();
    orderDecoder.readRule80A();
    orderDecoder.readPercentOffset();
    orderDecoder.readSettlingFirm();
    orderDecoder.readShortSaleParams();
    orderDecoder.readAuctionStrategy();
    orderDecoder.readBoxOrderParams();
    orderDecoder.readPegToStkOrVolOrderParams();
    orderDecoder.readDisplaySize();
    orderDecoder.readOldStyleOutsideRth();
    orderDecoder.readBlockOrder();
    orderDecoder.readSweepToFill();
    orderDecoder.readAllOrNone();
    orderDecoder.readMinQty();
    orderDecoder.readOcaType();
    orderDecoder.readETradeOnly();
    orderDecoder.readFirmQuoteOnly();
    orderDecoder.readNbboPriceCap();
    orderDecoder.readParentId();
    orderDecoder.readTriggerMethod();
    orderDecoder.readVolOrderParams(true);
    orderDecoder.readTrailParams();
    orderDecoder.readBasisPoints();
    orderDecoder.readComboLegs();
    orderDecoder.readSmartComboRoutingParams();
    orderDecoder.readScaleOrderParams();
    orderDecoder.readHedgeParams();
    orderDecoder.readOptOutSmartRouting();
    orderDecoder.readClearingParams();
    orderDecoder.readNotHeld();
    orderDecoder.readDeltaNeutral();
    orderDecoder.readAlgoParams();
    orderDecoder.readSolicited();
    orderDecoder.readWhatIfInfoAndCommission();
    orderDecoder.readVolRandomizeFlags();
    orderDecoder.readPegToBenchParams();
    orderDecoder.readConditions();
    orderDecoder.readAdjustedOrderParams();
    orderDecoder.readSoftDollarTier();
    orderDecoder.readCashQty();
    orderDecoder.readDontUseAutoPriceForHedge();
    orderDecoder.readIsOmsContainer();
    orderDecoder.readDiscretionaryUpToLimitPrice();
    orderDecoder.readUsePriceMgmtAlgo();
    orderDecoder.readDuration();
    orderDecoder.readPostToAts();
    orderDecoder.readAutoCancelParent(MIN_SERVER_VER.AUTO_CANCEL_PARENT);
    orderDecoder.readPegBestPegMidOrderAttributes();
    orderDecoder.readCustomerAccount();
    orderDecoder.readProfessionalCustomer();

    this.emit(EventName.openOrder, order.orderId, contract, order, orderState);
  }

  /**
   * Decode a OPEN_ORDER message from data queue and emit a updateAccountValue event.
   */
  private decodeMsg_ACCT_VALUE(): void {
    this.readInt(); // version
    const key = this.readStr();
    const value = this.readStr();
    const currency = this.readStr();
    const accountName = this.readStr();

    this.emit(EventName.updateAccountValue, key, value, currency, accountName);
  }

  /**
   * Decode a PORTFOLIO_VALUE message from data queue and emit a updatePortfolio (PortfolioValue) event.
   */
  private decodeMsg_PORTFOLIO_VALUE(): void {
    const version = this.readInt();

    const contract: Contract = {};
    if (version >= 6) {
      contract.conId = this.readInt();
    }
    contract.symbol = this.readStr();
    contract.secType = this.readStr() as SecType;
    contract.lastTradeDateOrContractMonth = this.readStr();
    contract.strike = this.readDouble();
    contract.right = validateOptionType(this.readStr() as OptionType);

    if (version >= 7) {
      contract.multiplier = this.readInt();
      contract.primaryExch = this.readStr();
    }

    contract.currency = this.readStr();

    if (version >= 2) {
      contract.localSymbol = this.readStr();
    }

    if (version >= 8) {
      contract.tradingClass = this.readStr();
    }

    const position: number = this.readDecimal();

    const marketPrice = this.readDouble();
    const marketValue = this.readDouble();
    let averageCost: number | undefined = undefined;
    let unrealizedPNL: number | undefined = undefined;
    let realizedPNL: number | undefined = undefined;
    if (version >= 3) {
      averageCost = this.readDouble();
      unrealizedPNL = this.readDouble();
      realizedPNL = this.readDouble();
    }

    let accountName: string | undefined = undefined;
    if (version >= 4) {
      accountName = this.readStr();
    }

    if (version === 6 && this.serverVersion === 39) {
      contract.primaryExch = this.readStr();
    }

    this.emit(
      EventName.updatePortfolio,
      contract,
      position,
      marketPrice,
      marketValue,
      averageCost,
      unrealizedPNL,
      realizedPNL,
      accountName,
    );
  }

  /**
   * Decode a ACCT_UPDATE_TIME message from data queue and emit a updateAccountTime event.
   */
  private decodeMsg_ACCT_UPDATE_TIME(): void {
    this.readInt(); // version
    const timeStamp = this.readStr();

    this.emit(EventName.updateAccountTime, timeStamp);
  }

  /**
   * Decode a NEXT_VALID_ID message from data queue and emit a nextValidId event.
   */
  private decodeMsg_NEXT_VALID_ID(): void {
    this.readInt(); // version
    const orderId = this.readInt();

    this.emit(EventName.nextValidId, orderId);
  }

  /**
   * Decode a CONTRACT_DATA message from data queue and emit a contractDetails event.
   */
  private decodeMsg_CONTRACT_DATA(): void {
    let version = 8;
    if (this.serverVersion < MIN_SERVER_VER.SIZE_RULES) {
      version = this.readInt();
    }

    let reqId = -1;
    if (version >= 3) {
      reqId = this.readInt();
    }

    const contract: ContractDetails = {
      contract: {},
    };

    contract.contract.symbol = this.readStr();
    contract.contract.secType = this.readStr() as SecType;
    this.readLastTradeDate(contract, false);
    if (this.serverVersion >= MIN_SERVER_VER.LAST_TRADE_DATE) {
      contract.contract.lastTradeDate = this.readStr();
    }
    contract.contract.strike = this.readDouble();
    contract.contract.right = validateOptionType(this.readStr() as OptionType);
    contract.contract.exchange = this.readStr();
    contract.contract.currency = this.readStr();
    contract.contract.localSymbol = this.readStr();
    contract.marketName = this.readStr();
    contract.contract.tradingClass = this.readStr();
    contract.contract.conId = this.readInt();
    contract.minTick = this.readDouble();
    if (
      this.serverVersion >= MIN_SERVER_VER.MD_SIZE_MULTIPLIER &&
      this.serverVersion < MIN_SERVER_VER.SIZE_RULES
    ) {
      this.readInt(); // mdSizeMultiplier - not used anymore
    }
    contract.contract.multiplier = this.readInt();
    contract.orderTypes = this.readStr();
    contract.validExchanges = this.readStr();

    if (version >= 2) {
      contract.priceMagnifier = this.readInt();
    }

    if (version >= 4) {
      contract.underConId = this.readInt();
    }

    if (version >= 5) {
      contract.longName = this.readStr();
      contract.contract.primaryExch = this.readStr();

      if (this.serverVersion >= MIN_SERVER_VER.ENCODE_MSG_ASCII7) {
        contract.longName = this.decodeUnicodeEscapedString(contract.longName);
      }
    }

    if (version >= 6) {
      contract.contractMonth = this.readStr();
      contract.industry = this.readStr();
      contract.category = this.readStr();
      contract.subcategory = this.readStr();
      contract.timeZoneId = this.readStr();
      contract.tradingHours = this.readStr();
      contract.liquidHours = this.readStr();
    }

    if (version >= 8) {
      contract.evRule = this.readStr();
      contract.evMultiplier = this.readDouble();
    }

    if (version >= 7) {
      const secIdListCount = this.readInt();
      if (secIdListCount > 0) {
        contract.secIdList = [];
        for (let i = 0; i < secIdListCount; ++i) {
          const tagValue: TagValue = {
            tag: this.readStr(),
            value: this.readStr(),
          };
          contract.secIdList.push(tagValue);
        }
      }
    }

    if (this.serverVersion >= MIN_SERVER_VER.AGG_GROUP) {
      contract.aggGroup = this.readInt();
    }

    if (this.serverVersion >= MIN_SERVER_VER.UNDERLYING_INFO) {
      contract.underSymbol = this.readStr();
      contract.underSecType = this.readStr() as SecType;
    }

    if (this.serverVersion >= MIN_SERVER_VER.MARKET_RULES) {
      contract.marketRuleIds = this.readStr();
    }

    if (this.serverVersion >= MIN_SERVER_VER.REAL_EXPIRATION_DATE) {
      contract.realExpirationDate = this.readStr();
    }

    if (this.serverVersion >= MIN_SERVER_VER.STOCK_TYPE) {
      contract.stockType = this.readStr();
    }

    if (
      this.serverVersion >= MIN_SERVER_VER.FRACTIONAL_SIZE_SUPPORT &&
      this.serverVersion < MIN_SERVER_VER.SIZE_RULES
    ) {
      this.readDecimal(); // sizeMinTick - not used anymore
    }

    if (this.serverVersion >= MIN_SERVER_VER.SIZE_RULES) {
      contract.minSize = this.readDecimal();
      contract.sizeIncrement = this.readDecimal();
      contract.suggestedSizeIncrement = this.readDecimal();
    }

    if (
      this.serverVersion >= MIN_SERVER_VER.FUND_DATA_FIELDS &&
      contract.contract.secType == SecType.FUND
    ) {
      contract.fundName = this.readStr();
      contract.fundFamily = this.readStr();
      contract.fundType = this.readStr();
      contract.fundFrontLoad = this.readStr();
      contract.fundBackLoad = this.readStr();
      contract.fundBackLoadTimeInterval = this.readStr();
      contract.fundManagementFee = this.readStr();
      contract.fundClosed = this.readBool();
      contract.fundClosedForNewInvestors = this.readBool();
      contract.fundClosedForNewMoney = this.readBool();
      contract.fundNotifyAmount = this.readStr();
      contract.fundMinimumInitialPurchase = this.readStr();
      contract.fundSubsequentMinimumPurchase = this.readStr();
      contract.fundBlueSkyStates = this.readStr();
      contract.fundBlueSkyTerritories = this.readStr();
      contract.fundDistributionPolicyIndicator =
        this.readStr() as FundDistributionPolicyIndicator;
      contract.fundAssetType = this.readStr() as FundAssetType;
    }

    this.emit(EventName.contractDetails, reqId, contract);
  }

  /**
   * Decode a EXECUTION_DATA message from data queue and emit a execDetails event.
   */
  private decodeMsg_EXECUTION_DATA(): void {
    let version = this.serverVersion;
    if (version < MIN_SERVER_VER.LAST_LIQUIDITY) {
      version = this.readInt();
    }

    let reqId = -1;
    if (version >= 7) {
      reqId = this.readInt();
    }

    const orderId = this.readInt();

    // read contract fields
    const contract: Contract = {};

    if (version >= 5) {
      contract.conId = this.readInt();
    }

    contract.symbol = this.readStr();
    contract.secType = this.readStr() as SecType;
    contract.lastTradeDateOrContractMonth = this.readStr();
    contract.strike = this.readDouble();
    contract.right = validateOptionType(this.readStr() as OptionType);

    if (version >= 9) {
      contract.multiplier = this.readInt();
    }

    contract.exchange = this.readStr();
    contract.currency = this.readStr();
    contract.localSymbol = this.readStr();

    if (version >= 10) {
      contract.tradingClass = this.readStr();
    }

    const exec: Execution = {};

    exec.orderId = orderId;
    exec.execId = this.readStr();
    exec.time = this.readStr();
    exec.acctNumber = this.readStr();
    exec.exchange = this.readStr();
    exec.side = this.readStr();
    exec.shares = this.readDecimal();
    exec.price = this.readDouble();

    if (version >= 2) {
      exec.permId = this.readInt();
    }

    if (version >= 3) {
      exec.clientId = this.readInt();
    }

    if (version >= 4) {
      exec.liquidation = this.readInt();
    }

    if (version >= 6) {
      exec.cumQty = this.readDecimal();
      exec.avgPrice = this.readDouble();
    }

    if (version >= 8) {
      exec.orderRef = this.readStr();
    }

    if (version >= 9) {
      exec.evRule = this.readStr();
      exec.evMultiplier = this.readDouble();
    }

    if (this.serverVersion >= MIN_SERVER_VER.MODELS_SUPPORT) {
      exec.modelCode = this.readStr();
    }

    if (this.serverVersion >= MIN_SERVER_VER.LAST_LIQUIDITY) {
      exec.lastLiquidity = { value: this.readInt() };
    }

    if (this.serverVersion >= MIN_SERVER_VER.PENDING_PRICE_REVISION) {
      exec.pendingPriceRevision = this.readBool();
    }

    this.emit(EventName.execDetails, reqId, contract, exec);
  }

  /**
   * Decode a MARKET_DEPTH message from data queue and emit a MarketDepth event.
   */
  private decodeMsg_MARKET_DEPTH(): void {
    this.readInt(); // version
    const id = this.readInt();
    const position = this.readInt();
    const operation = this.readInt();
    const side = this.readInt();
    const price = this.readDouble();
    const size = this.readDecimal();

    this.emit(
      EventName.updateMktDepth,
      id,
      position,
      operation,
      side,
      price,
      size,
    );
  }

  /**
   * Decode a MARKET_DEPTH_L2 message from data queue and emit a MarketDepthL2 event.
   */
  private decodeMsg_MARKET_DEPTH_L2(): void {
    this.readInt(); // version
    const id = this.readInt();
    const position = this.readInt();
    const marketMaker = this.readStr();
    const operation = this.readInt();
    const side = this.readInt();
    const price = this.readDouble();
    const size = this.readDecimal();

    let isSmartDepth = undefined;
    if (this.serverVersion >= MIN_SERVER_VER.SMART_DEPTH) {
      isSmartDepth = this.readBool();
    }

    this.emit(
      EventName.updateMktDepthL2,
      id,
      position,
      marketMaker,
      operation,
      side,
      price,
      size,
      isSmartDepth,
    );
  }

  /**
   * Decode a NEWS_BULLETINS message from data queue and emit a updateNewsBulletin event.
   */
  private decodeMsg_NEWS_BULLETINS(): void {
    this.readInt(); // version
    const newsMsgId = this.readInt();
    const newsMsgType = this.readInt();
    const newsMessage = this.readStr();
    const originatingExch = this.readStr();

    this.emit(
      EventName.updateNewsBulletin,
      newsMsgId,
      newsMsgType,
      newsMessage,
      originatingExch,
    );
  }

  /**
   * Decode a MANAGED_ACCTS message from data queue and emit a managedAccounts event.
   */
  private decodeMsg_MANAGED_ACCTS(): void {
    this.readInt(); // version
    const accountsList = this.readStr();

    this.emit(EventName.managedAccounts, accountsList);
  }

  /**
   * Decode a RECEIVE_FA message from data queue and emit a receiveFA event.
   */
  private decodeMsg_RECEIVE_FA(): void {
    this.readInt(); // version
    const faDataType = this.readInt();
    const xml = this.readStr();

    this.emit(EventName.receiveFA, faDataType, xml);
  }

  /**
   * Decode a HISTORICAL_DATA message from data queue and emit historicalData events.
   */
  private decodeMsg_HISTORICAL_DATA(): void {
    let version = Number.MAX_SAFE_INTEGER;
    if (this.serverVersion < MIN_SERVER_VER.SYNT_REALTIME_BARS) {
      version = this.readInt();
    }

    const reqId = this.readInt();

    let completedIndicator = "finished";
    let startDateStr = "";
    let endDateStr = "";
    if (version >= 2) {
      startDateStr = this.readStr();
      endDateStr = this.readStr();
      completedIndicator += "-" + startDateStr + "-" + endDateStr;
    }

    let itemCount = this.readInt();

    while (itemCount--) {
      const date = this.readStr();
      const open = this.readDouble();
      const high = this.readDouble();
      const low = this.readDouble();
      const close = this.readDouble();
      const volume = this.readDecimal();
      const WAP = this.readDecimal();
      // TODO: hasGap is deprecated, we should readStr and ignore result
      let hasGaps: boolean | undefined = undefined;
      if (this.serverVersion < MIN_SERVER_VER.SYNT_REALTIME_BARS) {
        hasGaps = this.readBool();
      }

      let barCount: number | undefined = undefined;
      if (version >= 3) {
        barCount = this.readInt();
      }

      this.emit(
        EventName.historicalData,
        reqId,
        date,
        open,
        high,
        low,
        close,
        volume,
        barCount,
        WAP,
        hasGaps,
      );
    }

    // send end of dataset marker
    this.emit(
      EventName.historicalData,
      reqId,
      completedIndicator,
      -1,
      -1,
      -1,
      -1,
      -1,
      -1,
      -1,
      false,
    );
  }

  /**
   * Decode a HISTORICAL_DATA_UPDATE message from data queue and emit historicalDataUpdate events.
   */
  private decodeMsg_HISTORICAL_DATA_UPDATE(): void {
    const reqId = this.readInt();
    const barCount = this.readInt();
    const date = this.readStr();
    const open = this.readDouble();
    const close = this.readDouble();
    const high = this.readDouble();
    const low = this.readDouble();
    const WAP = this.readDecimal();
    const volume = this.readDecimal();
    this.emit(
      EventName.historicalDataUpdate,
      reqId,
      date,
      open,
      high,
      low,
      close,
      volume,
      barCount,
      WAP,
    );
  }

  /**
   * Decode a REROUTE_MKT_DATA message from data queue and emit a rerouteMktDataReq event.
   */
  private decodeMsg_REROUTE_MKT_DATA(): void {
    const reqId = this.readInt();
    const conId = this.readInt();
    const exchange = this.readStr();
    this.emit(EventName.rerouteMktDataReq, reqId, conId, exchange);
  }

  /**
   * Decode a REROUTE_MKT_DEPTH message from data queue and emit a rerouteMktDepthReq event.
   */
  private decodeMsg_REROUTE_MKT_DEPTH(): void {
    const reqId = this.readInt();
    const conId = this.readInt();
    const exchange = this.readStr();
    this.emit(EventName.rerouteMktDepthReq, reqId, conId, exchange);
  }

  /**
   * Decode a MARKET_RULE message from data queue and emit a marketRule event.
   */
  private decodeMsg_MARKET_RULE(): void {
    const marketRuleId = this.readInt();
    const nPriceIncrements = this.readInt();
    const priceIncrements = new Array(nPriceIncrements);

    for (let i = 0; i < nPriceIncrements; i++) {
      priceIncrements[i] = {
        lowEdge: this.readDouble(),
        increment: this.readDouble(),
      };
    }

    this.emit(EventName.marketRule, marketRuleId, priceIncrements);
  }

  /**
   * Decode a BOND_CONTRACT_DATA message from data queue and emit a BondContractData event.
   */
  private decodeMsg_BOND_CONTRACT_DATA(): void {
    let version = 6;
    if (this.serverVersion < MIN_SERVER_VER.SIZE_RULES) {
      version = this.readInt();
    }

    let reqId = -1;
    if (version >= 3) {
      reqId = this.readInt();
    }

    const contract: ContractDetails = {
      contract: {},
    };

    contract.contract.symbol = this.readStr();
    contract.contract.secType = this.readStr() as SecType;
    contract.cusip = this.readStr();
    contract.coupon = this.readDouble();
    this.readLastTradeDate(contract, true);
    contract.issueDate = this.readStr();
    contract.ratings = this.readStr();
    contract.bondType = this.readStr();
    contract.couponType = this.readStr();
    contract.convertible = this.readBool();
    contract.callable = this.readBool();
    contract.putable = this.readBool();
    contract.descAppend = this.readStr();
    contract.contract.exchange = this.readStr();
    contract.contract.currency = this.readStr();
    contract.marketName = this.readStr();
    contract.contract.tradingClass = this.readStr();
    contract.contract.conId = this.readInt();
    contract.minTick = this.readDouble();
    if (
      this.serverVersion >= MIN_SERVER_VER.MD_SIZE_MULTIPLIER &&
      this.serverVersion < MIN_SERVER_VER.SIZE_RULES
    ) {
      this.readInt(); // mdSizeMultiplier - not used anymore
    }
    contract.orderTypes = this.readStr();
    contract.validExchanges = this.readStr();

    if (version >= 2) {
      contract.nextOptionDate = this.readStr();
      contract.nextOptionType = this.readStr();
      contract.nextOptionPartial = this.readBool();
      contract.notes = this.readStr();
    }

    if (version >= 4) {
      contract.longName = this.readStr();
    }

    if (version >= 6) {
      contract.evRule = this.readStr();
      contract.evMultiplier = this.readDouble();
    }

    if (version >= 5) {
      let secIdListCount = this.readInt();
      if (secIdListCount > 0) {
        contract.secIdList = [];
        while (secIdListCount--) {
          const tagValue: TagValue = {
            tag: this.readStr(),
            value: this.readStr(),
          };
          contract.secIdList.push(tagValue);
        }
      }
    }

    if (this.serverVersion >= MIN_SERVER_VER.AGG_GROUP) {
      contract.aggGroup = this.readInt();
    }

    if (this.serverVersion >= MIN_SERVER_VER.MARKET_RULES) {
      contract.marketRuleIds = this.readStr();
    }
    if (this.serverVersion >= MIN_SERVER_VER.SIZE_RULES) {
      contract.minSize = this.readDecimal();
      contract.sizeIncrement = this.readDecimal();
      contract.suggestedSizeIncrement = this.readDecimal();
    }

    this.emit(EventName.bondContractDetails, reqId, contract);
  }

  /**
   * Decode a SCANNER_PARAMETERS message from data queue and emit a scannerParameters event.
   */
  private decodeMsg_SCANNER_PARAMETERS(): void {
    this.readInt(); // version
    const xml = this.readStr();

    this.emit(EventName.scannerParameters, xml);
  }

  /**
   * Decode a SCANNER_DATA message from data queue and emit a scannerData and scannerDataEnd event.
   */
  private decodeMsg_SCANNER_DATA(): void {
    const version = this.readInt();
    const reqId = this.readInt();
    let numberOfElements = this.readInt();

    while (numberOfElements--) {
      const contract: ContractDetails = {
        contract: {},
      };

      const rank = this.readInt();
      if (version >= 3) {
        contract.contract.conId = this.readInt();
      }

      contract.contract.symbol = this.readStr();
      contract.contract.secType = this.readStr() as SecType;
      this.readLastTradeDate(contract, false);
      contract.contract.strike = this.readDouble();
      contract.contract.right = validateOptionType(
        this.readStr() as OptionType,
      );
      contract.contract.exchange = this.readStr();
      contract.contract.currency = this.readStr();
      contract.contract.localSymbol = this.readStr();
      contract.marketName = this.readStr();
      contract.contract.tradingClass = this.readStr();

      const distance = this.readStr();
      const benchmark = this.readStr();
      const projection = this.readStr();

      let legsStr: string | undefined = undefined;
      if (version >= 2) {
        legsStr = this.readStr();
      }

      this.emit(
        EventName.scannerData,
        reqId,
        rank,
        contract,
        distance,
        benchmark,
        projection,
        legsStr,
      );
    }

    this.emit(EventName.scannerDataEnd, reqId);
  }

  /**
   * Decode a TICK_OPTION_COMPUTATION message from data queue and emit a tickOptionComputation event.
   */
  private decodeMsg_TICK_OPTION_COMPUTATION(): void {
    let version;
    if (this.serverVersion >= MIN_SERVER_VER.PRICE_BASED_VOLATILITY)
      version = Number.MAX_VALUE;
    else version = this.readInt();

    const tickerId = this.readInt();
    const tickType = this.readInt();

    let tickAttrib = Number.MAX_VALUE;
    if (this.serverVersion >= MIN_SERVER_VER.PRICE_BASED_VOLATILITY) {
      tickAttrib = this.readInt();
    }

    let impliedVol = this.readDouble();
    if (impliedVol == -1) {
      // -1 is the "not yet computed" indicator
      impliedVol = undefined;
    }

    let delta = this.readDouble();
    if (delta == -2) {
      // -2 is the "not yet computed" indicator
      delta = undefined;
    }

    let optPrice: number | undefined = undefined;
    let pvDividend: number | undefined = undefined;
    let gamma: number | undefined = undefined;
    let vega: number | undefined = undefined;
    let theta: number | undefined = undefined;
    let undPrice: number | undefined = undefined;

    if (
      version >= 6 ||
      tickType === TickType.MODEL_OPTION ||
      tickType === TickType.DELAYED_MODEL_OPTION
    ) {
      optPrice = this.readDouble();
      if (optPrice == -1) {
        // -1 is the "not yet computed" indicator
        optPrice = undefined;
      }

      pvDividend = this.readDouble();
      if (pvDividend == -1) {
        // -1 is the "not yet computed" indicator
        pvDividend = undefined;
      }
    }

    if (version >= 6) {
      gamma = this.readDouble();
      if (gamma == -2) {
        // -2 is the "not yet computed" indicator
        gamma = undefined;
      }

      vega = this.readDouble();
      if (vega == -2) {
        // -2 is the "not yet computed" indicator
        vega = undefined;
      }

      theta = this.readDouble();
      if (theta == -2) {
        // -2 is the "not yet computed" indicator
        theta = undefined;
      }

      undPrice = this.readDouble();
      if (undPrice == -1) {
        // -1 is the "not yet computed" indicator
        undPrice = undefined;
      }
    }

    this.emit(
      EventName.tickOptionComputation,
      tickerId,
      tickType,
      tickAttrib,
      impliedVol,
      delta,
      optPrice,
      pvDividend,
      gamma,
      vega,
      theta,
      undPrice,
    );
  }

  /**
   * Decode a TICK_GENERIC message from data queue and emit a tickGeneric event.
   */
  private decodeMsg_TICK_GENERIC(): void {
    this.readInt(); // version
    const tickerId = this.readInt();
    const tickType = this.readInt();
    const value = this.readDouble();

    this.emit(EventName.tickGeneric, tickerId, tickType, value);
  }

  /**
   * Decode a TICK_STRING message from data queue and emit a tickString event.
   */
  private decodeMsg_TICK_STRING(): void {
    this.readInt(); // version
    const tickerId = this.readInt();
    const tickType = this.readInt();
    const value = this.readStr();

    this.emit(EventName.tickString, tickerId, tickType, value);
  }

  /**
   * Decode a TICK_EFP message from data queue and emit a tickEFP event.
   */
  private decodeMsg_TICK_EFP(): void {
    this.readInt(); // version
    const tickerId = this.readInt();
    const tickType = this.readInt();
    const basisPoints = this.readDouble();
    const formattedBasisPoints = this.readStr();
    const impliedFuturesPrice = this.readDouble();
    const holdDays = this.readInt();
    const futureExpiry = this.readStr();
    const dividendImpact = this.readDouble();
    const dividendsToExpiry = this.readDouble();

    this.emit(
      EventName.tickEFP,
      tickerId,
      tickType,
      basisPoints,
      formattedBasisPoints,
      impliedFuturesPrice,
      holdDays,
      futureExpiry,
      dividendImpact,
      dividendsToExpiry,
    );
  }

  /**
   * Decode a CURRENT_TIME message from data queue and emit a currentTime event.
   */
  private decodeMsg_CURRENT_TIME(): void {
    this.readInt(); //  version
    const time = this.readInt();

    this.emit(EventName.currentTime, time);
  }

  /**
   * Decode a REAL_TIME_BARS message from data queue and emit a realtimeBars event.
   */
  private decodeMsg_REAL_TIME_BARS(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const time = this.readInt();
    const open = this.readDouble();
    const high = this.readDouble();
    const low = this.readDouble();
    const close = this.readDouble();
    const volume = this.readDecimal();
    const wap = this.readDecimal();
    const count = this.readInt();

    this.emit(
      EventName.realtimeBar,
      reqId,
      time,
      open,
      high,
      low,
      close,
      volume,
      wap,
      count,
    );
  }

  /**
   * Decode a REAL_TIME_BARS message from data queue and emit a fundamentalData event.
   */
  private decodeMsg_FUNDAMENTAL_DATA(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const data = this.readStr();

    this.emit(EventName.fundamentalData, reqId, data);
  }

  /**
   * Decode a CONTRACT_DATA_END message from data queue and emit a contractDetailsEnd event.
   */
  private decodeMsg_CONTRACT_DATA_END(): void {
    this.readInt(); // version
    const reqId = this.readInt();

    this.emit(EventName.contractDetailsEnd, reqId);
  }

  /**
   * Decode a OPEN_ORDER_END message from data queue and emit a openOrderEnd event.
   */
  private decodeMsg_OPEN_ORDER_END(): void {
    this.readInt(); // version

    this.emit(EventName.openOrderEnd);
  }

  /**
   * Decode a ACCT_DOWNLOAD_END  message from data queue and emit a accountDownloadEnd event.
   */
  private decodeMsg_ACCT_DOWNLOAD_END(): void {
    this.readInt(); // version
    const accountName = this.readStr();

    this.emit(EventName.accountDownloadEnd, accountName);
  }

  /**
   * Decode a EXECUTION_DATA_END  message from data queue and emit a execDetailsEnd event.
   */
  private decodeMsg_EXECUTION_DATA_END(): void {
    this.readInt(); // version
    const reqId = this.readInt();

    this.emit(EventName.execDetailsEnd, reqId);
  }

  /**
   * Decode a DELTA_NEUTRAL_VALIDATION message from data queue and emit a deltaNeutralValidation event.
   */
  private decodeMsg_DELTA_NEUTRAL_VALIDATION(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const underComp: DeltaNeutralContract = {
      conId: this.readInt(),
      delta: this.readDouble(),
      price: this.readDouble(),
    };

    this.emit(EventName.deltaNeutralValidation, reqId, underComp);
  }

  /**
   * Decode a TICK_SNAPSHOT_END message from data queue and emit a tickSnapshotEnd event.
   */
  private decodeMsg_TICK_SNAPSHOT_END(): void {
    this.readInt(); // version
    const reqId = this.readInt();

    this.emit(EventName.tickSnapshotEnd, reqId);
  }

  /**
   * Decode a MARKET_DATA_TYPE message from data queue and emit a marketDataType event.
   */
  private decodeMsg_MARKET_DATA_TYPE(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const marketDataType = this.readInt();

    this.emit(EventName.marketDataType, reqId, marketDataType);
  }

  /**
   * Decode a COMMISSION_REPORT message from data queue and emit a commissionReport event.
   */
  private decodeMsg_COMMISSION_REPORT(): void {
    this.readInt(); // version

    const commissionReport: CommissionReport = {};
    commissionReport.execId = this.readStr();
    commissionReport.commission = this.readDouble();
    commissionReport.currency = this.readStr();
    commissionReport.realizedPNL = this.readDouble();
    commissionReport.yield = this.readDouble();
    commissionReport.yieldRedemptionDate = this.readInt();

    this.emit(EventName.commissionReport, commissionReport);
  }

  /**
   * Decode a POSITION message from data queue and emit a position event.
   */
  private decodeMsg_POSITION(): void {
    const version = this.readInt();
    const account = this.readStr();
    const contract: Contract = {};

    contract.conId = this.readInt();
    contract.symbol = this.readStr();
    contract.secType = this.readStr() as SecType;
    contract.lastTradeDateOrContractMonth = this.readStr();
    contract.strike = this.readDouble();
    contract.right = validateOptionType(this.readStr() as OptionType);
    contract.multiplier = this.readInt();
    contract.exchange = this.readStr();
    contract.currency = this.readStr();
    contract.localSymbol = this.readStr();
    if (version >= 2) {
      contract.tradingClass = this.readStr();
    }

    const pos = this.readDecimal();

    let avgCost = 0;
    if (version >= 3) {
      avgCost = this.readDouble();
    }

    this.emit(EventName.position, account, contract, pos, avgCost);
  }

  /**
   * Decode a POSITION_END message from data queue and emit a positionEnd event.
   */
  private decodeMsg_POSITION_END(): void {
    this.readInt(); // version

    this.emit(EventName.positionEnd);
  }

  /**
   * Decode a ACCOUNT_SUMMARY message from data queue and emit a accountSummary event.
   */
  private decodeMsg_ACCOUNT_SUMMARY(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const account = this.readStr();
    const tag = this.readStr();
    const value = this.readStr();
    const currency = this.readStr();

    this.emit(EventName.accountSummary, reqId, account, tag, value, currency);
  }

  /**
   * Decode a ACCOUNT_SUMMARY message from data queue and emit a accountSummaryEnd event.
   */
  private decodeMsg_ACCOUNT_SUMMARY_END(): void {
    this.readInt(); // version
    const reqId = this.readInt();

    this.emit(EventName.accountSummaryEnd, reqId);
  }

  /**
   * Decode a DISPLAY_GROUP_LIST message from data queue and emit a displayGroupList event.
   */
  private decodeMsg_DISPLAY_GROUP_LIST(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const list = this.readStr();

    this.emit(EventName.displayGroupList, reqId, list);
  }

  /**
   * Decode a DISPLAY_GROUP_UPDATED message from data queue and emit a displayGroupUpdated event.
   */
  private decodeMsg_DISPLAY_GROUP_UPDATED(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const contractInfo = this.readStr();

    this.emit(EventName.displayGroupUpdated, reqId, contractInfo);
  }

  /**
   * Decode a POSITION_MULTI message from data queue and emit a PositionMulti event.
   */
  private decodeMsg_POSITION_MULTI(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const account = this.readStr();
    const contract: Contract = {};

    contract.conId = this.readInt();
    contract.symbol = this.readStr();
    contract.secType = this.readStr() as SecType;
    contract.lastTradeDateOrContractMonth = this.readStr();
    contract.strike = this.readDouble();
    contract.right = validateOptionType(this.readStr() as OptionType);
    contract.multiplier = this.readInt();
    contract.exchange = this.readStr();
    contract.currency = this.readStr();
    contract.localSymbol = this.readStr();
    contract.tradingClass = this.readStr();
    const pos = this.readDecimal();
    const avgCost = this.readDouble();
    const modelCode = this.readStr();

    this.emit(
      EventName.positionMulti,
      reqId,
      account,
      modelCode,
      contract,
      pos,
      avgCost,
    );
  }

  /**
   * Decode a POSITION_MULTI_END message from data queue and emit a positionMultiEnd event.
   */
  private decodeMsg_POSITION_MULTI_END(): void {
    this.readInt(); // version
    const reqId = this.readInt();

    this.emit(EventName.positionMultiEnd, reqId);
  }

  /**
   * Decode a ACCOUNT_UPDATE_MULTI message from data queue and emit a accountUpdateMulti event.
   */
  private decodeMsg_ACCOUNT_UPDATE_MULTI(): void {
    this.readInt(); // version
    const reqId = this.readInt();
    const account = this.readStr();
    const modelCode = this.readStr();
    const key = this.readStr();
    const value = this.readStr();
    const currency = this.readStr();

    this.emit(
      EventName.accountUpdateMulti,
      reqId,
      account,
      modelCode,
      key,
      value,
      currency,
    );
  }

  /**
   * Decode a ACCOUNT_UPDATE_MULTI_END message from data queue and emit a accountUpdateMultiEnd event.
   */
  private decodeMsg_ACCOUNT_UPDATE_MULTI_END(): void {
    this.readInt(); // version
    const reqId = this.readStr();

    this.emit(EventName.accountUpdateMultiEnd, reqId);
  }

  /**
   * Decode a SECURITY_DEFINITION_OPTION_PARAMETER message from data queue and emit a securityDefinitionOptionParameter event.
   */
  private decodeMsg_SECURITY_DEFINITION_OPTION_PARAMETER(): void {
    const reqId = this.readInt();
    const exchange = this.readStr();
    const underlyingConId = this.readInt();
    const tradingClass = this.readStr();
    const multiplier = this.readStr();
    const expCount = this.readInt();
    const expirations: unknown[] = [];

    for (let i = 0; i < expCount; i++) {
      expirations.push(this.readStr());
    }

    const strikeCount = this.readInt();
    const strikes: number[] = [];
    for (let j = 0; j < strikeCount; j++) {
      strikes.push(this.readDouble());
    }

    this.emit(
      EventName.securityDefinitionOptionParameter,
      reqId,
      exchange,
      underlyingConId,
      tradingClass,
      multiplier,
      expirations,
      strikes,
    );
  }

  /**
   * Decode a SECURITY_DEFINITION_OPTION_PARAMETER_END message from data queue and emit a securityDefinitionOptionParameterEnd event.
   */
  private decodeMsg_SECURITY_DEFINITION_OPTION_PARAMETER_END(): void {
    const reqId = this.readInt();

    this.emit(EventName.securityDefinitionOptionParameterEnd, reqId);
  }

  /**
   * Decode a SOFT_DOLLAR_TIERS message from data queue and emit a softDollarTiers event.
   */
  private decodeMsg_SOFT_DOLLAR_TIERS(): void {
    const reqId = this.readInt();
    const nTiers = this.readInt();

    const tiers: SoftDollarTier[] = new Array(nTiers);
    for (let i = 0; i < nTiers; i++) {
      tiers[i] = {
        name: this.readStr(),
        value: this.readStr(),
        displayName: this.readStr(),
      };
    }

    this.emit(EventName.softDollarTiers, reqId, tiers);
  }

  /**
   * Decode a FAMILY_CODES message from data queue and emit a familyCodes event.
   */
  private decodeMsg_FAMILY_CODES(): void {
    const nFamilyCodes = this.readInt();

    const familyCodes: FamilyCode[] = new Array(nFamilyCodes);
    for (let i = 0; i < nFamilyCodes; i++) {
      familyCodes[i] = {
        accountID: this.readStr(),
        familyCode: this.readStr(),
      };
    }

    this.emit(EventName.familyCodes, familyCodes);
  }

  /**
   * Decode a SYMBOL_SAMPLES message from data queue and emit a symbolSamples event.
   */
  private decodeMsg_SYMBOL_SAMPLES(): void {
    const reqId = this.readInt();

    const nContractDescriptions = this.readInt();
    const contractDescriptions: ContractDescription[] = new Array(
      nContractDescriptions,
    );
    for (let i = 0; i < nContractDescriptions; i++) {
      const contract: Contract = {
        conId: this.readInt(),
        symbol: this.readStr(),
        secType: this.readStr() as SecType,
        primaryExch: this.readStr(),
        currency: this.readStr(),
      };

      const nDerivativeSecTypes = this.readInt();
      const derivativeSecTypes: SecType[] = new Array(nDerivativeSecTypes);
      for (let j = 0; j < nDerivativeSecTypes; j++) {
        derivativeSecTypes[j] = this.readStr() as SecType;
      }

      if (this.serverVersion >= MIN_SERVER_VER.BOND_ISSUERID) {
        contract.description = this.readStr();
        contract.issuerId = this.readStr();
      }

      contractDescriptions[i] = {
        contract: contract,
        derivativeSecTypes: derivativeSecTypes,
      };
    }

    this.emit(EventName.symbolSamples, reqId, contractDescriptions);
  }

  /**
   * Decode a MKT_DEPTH_EXCHANGES message from data queue and emit a mktDepthExchanges event.
   */
  private decodeMsg_MKT_DEPTH_EXCHANGES(): void {
    const nDepthMktDataDescriptions = this.readInt();
    const depthMktDataDescriptions: DepthMktDataDescription[] = new Array(
      nDepthMktDataDescriptions,
    );
    for (let i = 0; i < nDepthMktDataDescriptions; i++) {
      if (this.serverVersion >= MIN_SERVER_VER.SERVICE_DATA_TYPE) {
        depthMktDataDescriptions[i] = {
          exchange: this.readStr(),
          secType: this.readStr() as SecType,
          listingExch: this.readStr(),
          serviceDataType: this.readStr(),
          aggGroup: this.readIntOrUndefined(),
        };
      } else {
        depthMktDataDescriptions[i] = {
          exchange: this.readStr(),
          secType: this.readStr() as SecType,
          listingExch: "",
          serviceDataType: this.readBool() ? "Deep2" : "Deep",
          aggGroup: undefined,
        };
      }
    }

    this.emit(EventName.mktDepthExchanges, depthMktDataDescriptions);
  }

  /**
   * Decode a TICK_REQ_PARAMS message from data queue and emit a tickReqParams event.
   */
  private decodeMsg_TICK_REQ_PARAMS(): void {
    const tickerId = this.readInt();
    const minTick = this.readInt();
    const bboExchange = this.readStr();
    const snapshotPermissions = this.readInt();

    this.emit(
      EventName.tickReqParams,
      tickerId,
      minTick,
      bboExchange,
      snapshotPermissions,
    );
  }

  /**
   * Decode a SMART_COMPONENTS message from data queue and emit a smartComponents event.
   */
  private decodeMsg_SMART_COMPONENTS(): void {
    const reqId = this.readInt();
    const nCount = this.readInt();

    const theMap: Map<number, [string, string]> = new Map();
    for (let i = 0; i < nCount; i++) {
      const bitNumber = this.readInt();
      const exchange = this.readStr();
      const exchangeLetter = this.readStr();
      theMap.set(bitNumber, [exchange, exchangeLetter]);
    }

    this.emit(EventName.smartComponents, reqId, theMap);
  }

  /**
   * Decode a NEWS_ARTICLE message from data queue and emit a newsArticle event.
   */
  private decodeMsg_NEWS_ARTICLE(): void {
    const reqId = this.readInt();
    const articleType = this.readInt();
    const articleText = this.readStr();

    this.emit(EventName.newsArticle, reqId, articleType, articleText);
  }

  /**
   * Decode a TICK_NEWS message from data queue and emit a tickNews event.
   */
  private decodeMsg_TICK_NEWS(): void {
    const tickerId = this.readInt();
    const timeStamp = this.readInt();
    const providerCode = this.readStr();
    const articleId = this.readStr();
    const headline = this.readStr();
    const extraData = this.readStr();

    this.emit(
      EventName.tickNews,
      tickerId,
      timeStamp,
      providerCode,
      articleId,
      headline,
      extraData,
    );
  }

  /**
   * Decode a NEWS_PROVIDERS message from data queue and emit a newsProviders event.
   */
  private decodeMsg_NEWS_PROVIDERS(): void {
    const nNewsProviders = this.readInt();
    const newProviders: NewsProvider[] = new Array(nNewsProviders);
    for (let i = 0; i < nNewsProviders; i++) {
      newProviders[i] = {
        providerCode: this.readStr(),
        providerName: this.readStr(),
      };
    }

    this.emit(EventName.newsProviders, newProviders);
  }

  /**
   * Decode a HISTORICAL_NEWS message from data queue and emit a historicalNews event.
   */
  private decodeMsg_HISTORICAL_NEWS(): void {
    const requestId = this.readInt();
    const time = this.readStr();
    const providerCode = this.readStr();
    const articleId = this.readStr();
    const headline = this.readStr();

    this.emit(
      EventName.historicalNews,
      requestId,
      time,
      providerCode,
      articleId,
      headline,
    );
  }

  /**
   * Decode a HISTORICAL_NEWS_END message from data queue and emit a historicalNewsEnd event.
   */
  private decodeMsg_HISTORICAL_NEWS_END(): void {
    const reqId = this.readInt();
    const hasMore = this.readBool();

    this.emit(EventName.historicalNewsEnd, reqId, hasMore);
  }

  /**
   * Decode a HEAD_TIMESTAMP message from data queue and emit a headTimestamp event.
   */
  private decodeMsg_HEAD_TIMESTAMP(): void {
    const reqId = this.readInt();
    const headTimestamp = this.readStr();

    this.emit(EventName.headTimestamp, reqId, headTimestamp);
  }

  /**
   * Decode a HISTOGRAM_DATA message from data queue and emit a histogramData event.
   */
  private decodeMsg_HISTOGRAM_DATA(): void {
    const reqId = this.readInt();
    const count = this.readInt();

    const items: HistogramEntry[] = new Array(count);
    for (let i = 0; i < count; i++) {
      items[i] = {
        price: this.readDouble(),
        size: this.readDecimal(),
      };
    }

    this.emit(EventName.histogramData, reqId, items);
  }

  /**
   * Decode a PNL message from data queue and emit a pnl event.
   */
  private decodeMsg_PNL(): void {
    const reqId = this.readInt();
    const dailyPnL = this.readDouble();

    let unrealizedPnL: number | undefined = undefined;
    let realizedPnL: number | undefined = undefined;

    if (this.serverVersion >= MIN_SERVER_VER.UNREALIZED_PNL) {
      unrealizedPnL = this.readDouble();
    }
    if (this.serverVersion >= MIN_SERVER_VER.REALIZED_PNL) {
      realizedPnL = this.readDouble();
    }

    this.emit(EventName.pnl, reqId, dailyPnL, unrealizedPnL, realizedPnL);
  }

  /**
   * Decode a PNL_SINGLE message from data queue and emit a pnlSingle event.
   */
  private decodeMsg_PNL_SINGLE(): void {
    const reqId = this.readInt();
    const pos = this.readDecimal();
    const dailyPnL = this.readDouble();

    let unrealizedPnL: number | undefined = undefined;
    let realizedPnL: number | undefined = undefined;

    if (this.serverVersion >= MIN_SERVER_VER.UNREALIZED_PNL) {
      unrealizedPnL = this.readDouble();
    }
    if (this.serverVersion >= MIN_SERVER_VER.REALIZED_PNL) {
      realizedPnL = this.readDouble();
    }

    const value = this.readDouble();

    this.emit(
      EventName.pnlSingle,
      reqId,
      pos,
      dailyPnL,
      unrealizedPnL,
      realizedPnL,
      value,
    );
  }

  /**
   * Decode a HISTORICAL_TICKS message from data queue and emit a historicalTicks event.
   */
  private decodeMsg_HISTORICAL_TICKS(): void {
    const reqId = this.readInt();
    const tickCount = this.readInt();
    const ticks: HistoricalTick[] = new Array(tickCount);
    for (let i = 0; i < tickCount; i++) {
      const time = this.readInt();
      this.readInt(); //for consistency
      const price = this.readDouble();
      const size = this.readDecimal();
      ticks[i] = {
        time,
        price,
        size,
      };
    }
    const done = this.readBool();

    this.emit(EventName.historicalTicks, reqId, ticks, done);
  }

  /**
   * Decode a HISTORICAL_TICKS_BID_ASK message from data queue and emit a historicalTicksBidAsk event.
   */
  private decodeMsg_HISTORICAL_TICKS_BID_ASK(): void {
    const reqId = this.readInt();
    const tickCount = this.readInt();
    const ticks: HistoricalTickBidAsk[] = new Array(tickCount);
    for (let i = 0; i < tickCount; i++) {
      const time = this.readInt();
      const flags = this.readInt();
      const priceBid = this.readDouble();
      const priceAsk = this.readDouble();
      const sizeBid = this.readDecimal();
      const sizeAsk = this.readDecimal();
      ticks[i] = {
        time: time,
        tickAttribBidAsk: {
          bidPastLow: (flags & (1 << 0)) != 0,
          askPastHigh: (flags & (1 << 1)) != 0,
        },
        priceBid: priceBid,
        priceAsk: priceAsk,
        sizeBid: sizeBid,
        sizeAsk: sizeAsk,
      };
    }
    const done = this.readBool();

    this.emit(EventName.historicalTicksBidAsk, reqId, ticks, done);
  }

  /**
   * Decode a HISTORICAL_TICKS_LAST message from data queue and emit a historicalTicksLast event.
   */
  private decodeMsg_HISTORICAL_TICKS_LAST(): void {
    const reqId = this.readInt();
    const tickCount = this.readInt();
    const ticks: HistoricalTickLast[] = new Array(tickCount);
    for (let i = 0; i < tickCount; i++) {
      const time = this.readInt();
      const mask = this.readInt();
      const price = this.readDouble();
      const size = this.readDecimal();
      const exchange = this.readStr();
      const specialConditions = this.readStr();
      ticks[i] = {
        time: time,
        tickAttribLast: {
          pastLimit: (mask & (1 << 0)) !== 0,
          unreported: (mask & (1 << 1)) !== 0,
        },
        price: price,
        size: size,
        exchange: exchange,
        specialConditions: specialConditions,
      };
    }
    const done = this.readBool();

    this.emit(EventName.historicalTicksLast, reqId, ticks, done);
  }

  /**
   * Decode a TICK_BY_TICK message from data queue and a emit tickByTickAllLast, tickByTickBidAsk or tickByTickMidPoint event.
   */
  private decodeMsg_TICK_BY_TICK(): void {
    const reqId = this.readInt();
    const tickType = this.readInt();
    const time = this.readStr();

    switch (tickType) {
      case 0: // None
        break;
      case 1: // Last
      case 2: {
        // All-last
        const price = this.readDouble();
        const size = this.readDecimal();
        const mask = this.readInt();
        const pastLimit = (mask & (1 << 0)) !== 0;
        const unreported = (mask & (1 << 1)) !== 0;
        const exchange = this.readStr();
        const specialConditions = this.readStr();

        this.emit(
          EventName.tickByTickAllLast,
          reqId,
          tickType,
          time,
          price,
          size,
          { pastLimit, unreported },
          exchange,
          specialConditions,
        );
        break;
      }
      case 3: {
        // BidAsk
        const bidPrice = this.readDouble();
        const askPrice = this.readDouble();
        const bidSize = this.readDecimal();
        const askSize = this.readDecimal();
        const mask = this.readInt();
        const bidPastLow = (mask & (1 << 0)) !== 0;
        const askPastHigh = (mask & (1 << 1)) !== 0;

        this.emit(
          EventName.tickByTickBidAsk,
          reqId,
          time,
          bidPrice,
          askPrice,
          bidSize,
          askSize,
          {
            bidPastLow,
            askPastHigh,
          },
        );
        break;
      }
      case 4: {
        // MidPoint
        const midPoint = this.readDouble();

        this.emit(EventName.tickByTickMidPoint, reqId, time, midPoint);
        break;
      }
    }
  }

  /**
   * Decode a ORDER_BOUND message from data queue and a emit orderBound event.
   */
  private decodeMsg_ORDER_BOUND(): void {
    const orderId = this.readInt();
    const apiClientId = this.readDouble();
    const apiOrderId = this.readInt();

    this.emit(EventName.orderBound, orderId, apiClientId, apiOrderId);
  }

  /**
   * Decode a COMPLETED_ORDER message from data queue and a emit completedOrder event.
   */
  private decodeMsg_COMPLETED_ORDER(): void {
    const contract: Contract = {};
    const order: Order = {};
    const orderState: OrderState = {};
    const orderDecoder = new OrderDecoder(
      this,
      contract,
      order,
      orderState,
      Number.MAX_VALUE,
      this.serverVersion,
    );

    // read contract fields
    orderDecoder.readContractFields();

    // read order fields
    orderDecoder.readAction();
    orderDecoder.readTotalQuantity();
    orderDecoder.readOrderType();
    orderDecoder.readLmtPrice();
    orderDecoder.readAuxPrice();
    orderDecoder.readTIF();
    orderDecoder.readOcaGroup();
    orderDecoder.readAccount();
    orderDecoder.readOpenClose();
    orderDecoder.readOrigin();
    orderDecoder.readOrderRef();
    orderDecoder.readPermId();
    orderDecoder.readOutsideRth();
    orderDecoder.readHidden();
    orderDecoder.readDiscretionaryAmount();
    orderDecoder.readGoodAfterTime();
    orderDecoder.readFAParams();
    orderDecoder.readModelCode();
    orderDecoder.readGoodTillDate();
    orderDecoder.readRule80A();
    orderDecoder.readPercentOffset();
    orderDecoder.readSettlingFirm();
    orderDecoder.readShortSaleParams();
    orderDecoder.readBoxOrderParams();
    orderDecoder.readPegToStkOrVolOrderParams();
    orderDecoder.readDisplaySize();
    orderDecoder.readSweepToFill();
    orderDecoder.readAllOrNone();
    orderDecoder.readMinQty();
    orderDecoder.readOcaType();
    orderDecoder.readTriggerMethod();
    orderDecoder.readVolOrderParams(false);
    orderDecoder.readTrailParams();
    orderDecoder.readComboLegs();
    orderDecoder.readSmartComboRoutingParams();
    orderDecoder.readScaleOrderParams();
    orderDecoder.readHedgeParams();
    orderDecoder.readClearingParams();
    orderDecoder.readNotHeld();
    orderDecoder.readDeltaNeutral();
    orderDecoder.readAlgoParams();
    orderDecoder.readSolicited();
    orderDecoder.readOrderStatus();
    orderDecoder.readVolRandomizeFlags();
    orderDecoder.readPegToBenchParams();
    orderDecoder.readConditions();
    orderDecoder.readStopPriceAndLmtPriceOffset();
    orderDecoder.readCashQty();
    orderDecoder.readDontUseAutoPriceForHedge();
    orderDecoder.readIsOmsContainer();
    orderDecoder.readAutoCancelDate();
    orderDecoder.readFilledQuantity();
    orderDecoder.readRefFuturesConId();
    orderDecoder.readAutoCancelParent();
    orderDecoder.readShareholder();
    orderDecoder.readImbalanceOnly();
    orderDecoder.readRouteMarketableToBbo();
    orderDecoder.readParentPermId();
    orderDecoder.readCompletedTime();
    orderDecoder.readCompletedStatus();
    orderDecoder.readPegBestPegMidOrderAttributes();
    orderDecoder.readCustomerAccount();
    orderDecoder.readProfessionalCustomer();

    this.emit(EventName.completedOrder, contract, order, orderState);
  }

  /**
   * Decode a COMPLETED_ORDER_END message from data queue and a emit completedOrdersEnd event.
   */
  private decodeMsg_COMPLETED_ORDERS_END(): void {
    this.emit(EventName.completedOrdersEnd);
  }

  /**
   * Decode a REPLACE_FA_END message from data queue and a emit replaceFAEnd event.
   */
  private decodeMsg_REPLACE_FA_END(): void {
    const reqId = this.readInt();
    const text = this.readStr();
    this.emit(EventName.replaceFAEnd, reqId, text);
  }

  /**
   * Decode a WSH_META_DATA message from data queue and a emit wshMetaData event.
   */
  private decodeMsg_WSH_META_DATA(): void {
    const reqId = this.readInt();
    const dataJson = this.readStr();
    this.emit(EventName.wshMetaData, reqId, dataJson);
  }

  /**
   * Decode a WSH_EVENT_DATA message from data queue and a emit wshEventData event.
   */
  private decodeMsg_WSH_EVENT_DATA(): void {
    const reqId = this.readInt();
    const dataJson = this.readStr();
    this.emit(EventName.wshEventData, reqId, dataJson);
  }

  /**
   * Decode a HISTORICAL_SCHEDULE message from data queue and a emit historicalSchedule event.
   */
  private decodeMsg_HISTORICAL_SCHEDULE(): void {
    const reqId = this.readInt();
    const startDateTime = this.readStr();
    const endDateTime = this.readStr();
    const timeZone = this.readStr();

    const sessionsCount = this.readInt();
    const sessions: HistoricalSession[] = new Array(sessionsCount);

    for (let i = 0; i < sessionsCount; i++) {
      const sessionStartDateTime = this.readStr();
      const sessionEndDateTime = this.readStr();
      const sessionRefDate = this.readStr();
      sessions[i] = {
        startDateTime: sessionStartDateTime,
        endDateTime: sessionEndDateTime,
        refDate: sessionRefDate,
      };
    }

    this.emit(
      EventName.historicalSchedule,
      reqId,
      startDateTime,
      endDateTime,
      timeZone,
      sessions,
    );
  }

  /**
   * Decode a USER_INFO message from data queue and a emit userInfo event.
   */
  private decodeMsg_USER_INFO(): void {
    const reqId = this.readInt();
    const whiteBrandingId = this.readStr();

    this.emit(EventName.userInfo, reqId, whiteBrandingId);
  }

  /**
   * Read last trade date, parse it and assign to proper [[ContractDetails]] attributes.
   */
  private readLastTradeDate(contract: ContractDetails, isBond: boolean): void {
    const lastTradeDateOrContractMonth = this.readStr();
    if (lastTradeDateOrContractMonth?.length) {
      const tokens =
        lastTradeDateOrContractMonth.indexOf("-") > 0
          ? lastTradeDateOrContractMonth.split("-")
          : lastTradeDateOrContractMonth.split("\\s+");

      if (tokens.length > 0) {
        if (isBond) {
          contract.maturity = tokens[0];
        } else {
          contract.contract.lastTradeDateOrContractMonth = tokens[0];
        }
      }

      if (tokens.length > 1) {
        contract.lastTradeTime = tokens[1];
      }

      if (isBond && tokens.length > 2) {
        contract.timeZoneId = tokens[2];
      }
    }
  }
}

/**
 * @internal
 *
 * This class implements the Order-decoder, similar to IB's EOrderDecoder on
 * https://github.com/stoqey/ib/blob/master/ref/client/EOrderDecoder.java
 */

class OrderDecoder {
  constructor(
    private decoder: Decoder,
    private contract: Contract,
    private order: Order,
    private orderState: OrderState,
    private version: number,
    private serverVersion: number,
  ) {}

  readOrderId(): void {
    this.order.orderId = this.decoder.readInt();
  }

  readContractFields(): void {
    if (this.version >= 17) {
      this.contract.conId = this.decoder.readInt();
    }
    this.contract.symbol = this.decoder.readStr();
    this.contract.secType = this.decoder.readStr() as SecType;
    this.contract.lastTradeDateOrContractMonth = this.decoder.readStr();
    this.contract.strike = this.decoder.readDouble();
    this.contract.right = validateOptionType(
      this.decoder.readStr() as OptionType,
    );
    if (this.version >= 32) {
      this.contract.multiplier = +this.decoder.readStr();
    }
    this.contract.exchange = this.decoder.readStr();
    this.contract.currency = this.decoder.readStr();
    if (this.version >= 2) {
      this.contract.localSymbol = this.decoder.readStr();
    }
    if (this.version >= 32) {
      this.contract.tradingClass = this.decoder.readStr();
    }
  }

  readAction(): void {
    this.order.action = this.decoder.readStr() as OrderAction;
  }

  readTotalQuantity(): void {
    this.order.totalQuantity = this.decoder.readDecimal();
  }

  readOrderType(): void {
    this.order.orderType = this.decoder.readStr() as OrderType;
  }

  readLmtPrice(): void {
    if (this.version < 29) {
      this.order.lmtPrice = this.decoder.readDouble();
    } else {
      this.order.lmtPrice = this.decoder.readDoubleOrUndefined();
    }
  }

  readAuxPrice(): void {
    if (this.version < 30) {
      this.order.auxPrice = this.decoder.readDouble();
    } else {
      this.order.auxPrice = this.decoder.readDoubleOrUndefined();
    }
  }

  readTIF(): void {
    this.order.tif = this.decoder.readStr() as TimeInForce;
  }

  readOcaGroup(): void {
    this.order.ocaGroup = this.decoder.readStr();
  }

  readAccount(): void {
    this.order.account = this.decoder.readStr();
  }

  readOpenClose(): void {
    this.order.openClose = this.decoder.readStr();
  }

  readOrigin(): void {
    this.order.origin = this.decoder.readInt();
  }

  readOrderRef(): void {
    this.order.orderRef = this.decoder.readStr();
  }

  readClientId(): void {
    if (this.version >= 3) {
      this.order.clientId = this.decoder.readInt();
    }
  }

  readPermId(): void {
    if (this.version >= 4) {
      this.order.permId = this.decoder.readInt();
    }
  }

  readOutsideRth(): void {
    if (this.version >= 4) {
      if (this.version < 18) {
        // will never happen
        /* order.m_ignoreRth = */ this.decoder.readBool();
      } else {
        this.order.outsideRth = this.decoder.readBool();
      }
    }
  }

  readHidden(): void {
    if (this.version >= 4) {
      this.order.hidden = this.decoder.readInt() == 1;
    }
  }

  readDiscretionaryAmount(): void {
    if (this.version >= 4) {
      this.order.discretionaryAmt = this.decoder.readDouble();
    }
  }

  readGoodAfterTime(): void {
    if (this.version >= 5) {
      this.order.goodAfterTime = this.decoder.readStr();
    }
  }

  skipSharesAllocation(): void {
    if (this.version >= 6) {
      // skip deprecated sharesAllocation field
      this.decoder.readStr();
    }
  }

  readFAParams(): void {
    if (this.version >= 7) {
      this.order.faGroup = this.decoder.readStr();
      this.order.faMethod = this.decoder.readStr();
      this.order.faPercentage = this.decoder.readStr();
      if (this.version < MIN_SERVER_VER.FA_PROFILE_DESUPPORT)
        this.order.faProfile = this.decoder.readStr();
    }
  }

  readModelCode(): void {
    if (this.version >= MIN_SERVER_VER.MODELS_SUPPORT) {
      this.order.modelCode = this.decoder.readStr();
    }
  }

  readGoodTillDate(): void {
    if (this.version >= 8) {
      this.order.goodTillDate = this.decoder.readStr();
    }
  }

  readRule80A(): void {
    if (this.version >= 9) {
      this.order.rule80A = this.decoder.readStr();
    }
  }

  readPercentOffset() {
    if (this.version >= 9) {
      this.order.percentOffset = this.decoder.readDoubleOrUndefined();
    }
  }

  readSettlingFirm(): void {
    if (this.version >= 9) {
      this.order.settlingFirm = this.decoder.readStr();
    }
  }

  readShortSaleParams(): void {
    if (this.version >= 9) {
      this.order.shortSaleSlot = this.decoder.readInt();
      this.order.designatedLocation = this.decoder.readStr();
      if (this.version == 51) {
        this.decoder.readInt(); // exemptCode
      } else if (this.version >= 23) {
        this.order.exemptCode = this.decoder.readInt();
      }
    }
  }

  readAuctionStrategy(): void {
    if (this.version >= 9) {
      this.order.auctionStrategy = this.decoder.readInt();
    }
  }

  readBoxOrderParams(): void {
    if (this.version >= 9) {
      this.order.startingPrice = this.decoder.readDoubleOrUndefined();
      this.order.stockRefPrice = this.decoder.readDoubleOrUndefined();
      this.order.delta = this.decoder.readDoubleOrUndefined();
    }
  }

  readPegToStkOrVolOrderParams(): void {
    if (this.version >= 9) {
      this.order.stockRangeLower = this.decoder.readDoubleOrUndefined();
      this.order.stockRangeUpper = this.decoder.readDoubleOrUndefined();
    }
  }

  readDisplaySize(): void {
    if (this.version >= 9) {
      this.order.displaySize = this.decoder.readIntOrUndefined();
    }
  }

  readOldStyleOutsideRth(): void {
    if (this.version >= 9) {
      if (this.version < 18) {
        // will never happen
        /* order.m_rthOnly = */ this.decoder.readBool();
      }
    }
  }

  readBlockOrder(): void {
    if (this.version >= 9) {
      this.order.blockOrder = this.decoder.readBool();
    }
  }

  readSweepToFill(): void {
    if (this.version >= 9) {
      this.order.sweepToFill = this.decoder.readBool();
    }
  }

  readAllOrNone(): void {
    if (this.version >= 9) {
      this.order.allOrNone = this.decoder.readBool();
    }
  }

  readMinQty(): void {
    if (this.version >= 9) {
      this.order.minQty = this.decoder.readIntOrUndefined();
    }
  }

  readOcaType(): void {
    if (this.version >= 9) {
      this.order.ocaType = this.decoder.readInt();
    }
  }

  readETradeOnly(): void {
    if (this.version >= 9) {
      this.order.eTradeOnly = this.decoder.readBool();
    }
  }

  readFirmQuoteOnly(): void {
    if (this.version >= 9) {
      this.order.firmQuoteOnly = this.decoder.readBool();
    }
  }

  readNbboPriceCap(): void {
    if (this.version >= 9) {
      this.order.nbboPriceCap = this.decoder.readDoubleOrUndefined();
    }
  }

  readParentId(): void {
    if (this.version >= 10) {
      this.order.parentId = this.decoder.readInt();
    }
  }

  readTriggerMethod(): void {
    if (this.version >= 10) {
      this.order.triggerMethod = this.decoder.readInt();
    }
  }

  readVolOrderParams(readOpenOrderAttribs: boolean): void {
    if (this.version >= 11) {
      this.order.volatility = this.decoder.readDoubleOrUndefined();
      this.order.volatilityType = this.decoder.readInt();
      if (this.version == 11) {
        const receivedInt = this.decoder.readInt();
        this.order.deltaNeutralOrderType = receivedInt == 0 ? "NONE" : "MKT";
      } else {
        this.order.deltaNeutralOrderType = this.decoder.readStr();
        this.order.deltaNeutralAuxPrice = this.decoder.readDoubleOrUndefined();

        if (
          this.version >= 27 &&
          this.order.deltaNeutralOrderType &&
          this.order.deltaNeutralOrderType !== ""
        ) {
          this.order.deltaNeutralConId = this.decoder.readInt();
          if (readOpenOrderAttribs) {
            this.order.deltaNeutralSettlingFirm = this.decoder.readStr();
            this.order.deltaNeutralClearingAccount = this.decoder.readStr();
            this.order.deltaNeutralClearingIntent = this.decoder.readStr();
          }
        }

        if (
          this.version >= 31 &&
          this.order.deltaNeutralOrderType &&
          this.order.deltaNeutralOrderType !== ""
        ) {
          if (readOpenOrderAttribs) {
            this.order.deltaNeutralOpenClose = this.decoder.readStr();
          }
          this.order.deltaNeutralShortSale = this.decoder.readBool();
          this.order.deltaNeutralShortSaleSlot = this.decoder.readInt();
          this.order.deltaNeutralDesignatedLocation = this.decoder.readStr();
        }
      }
      this.order.continuousUpdate = this.decoder.readInt();
      if (this.version == 26) {
        this.order.stockRangeLower = this.decoder.readDouble();
        this.order.stockRangeUpper = this.decoder.readDouble();
      }
      this.order.referencePriceType = this.decoder.readInt();
    }
  }

  readTrailParams(): void {
    if (this.version >= 13) {
      this.order.trailStopPrice = this.decoder.readDoubleOrUndefined();
    }

    if (this.version >= 30) {
      this.order.trailingPercent = this.decoder.readDoubleOrUndefined();
    }
  }

  readBasisPoints(): void {
    if (this.version >= 14) {
      this.order.basisPoints = this.decoder.readDoubleOrUndefined();
      this.order.basisPointsType = this.decoder.readIntOrUndefined();
    }
  }

  readComboLegs(): void {
    if (this.version >= 14) {
      this.contract.comboLegsDescription = this.decoder.readStr();
    }

    if (this.version >= 29) {
      const comboLegsCount = this.decoder.readInt();
      if (comboLegsCount > 0) {
        this.contract.comboLegs = [];
        for (let i = 0; i < comboLegsCount; ++i) {
          const conId = this.decoder.readInt();
          const ratio = this.decoder.readInt();
          const action = this.decoder.readStr() as OrderAction;
          const exchange = this.decoder.readStr();
          const openClose = this.decoder.readInt();
          const shortSaleSlot = this.decoder.readInt();
          const designatedLocation = this.decoder.readStr();
          const exemptCode = this.decoder.readInt();

          this.contract.comboLegs.push({
            conId,
            ratio,
            action,
            exchange,
            openClose,
            shortSaleSlot,
            designatedLocation,
            exemptCode,
          });
        }
      }

      const orderComboLegsCount = this.decoder.readInt();
      if (orderComboLegsCount > 0) {
        this.order.orderComboLegs = [];
        for (let i = 0; i < orderComboLegsCount; ++i) {
          const price = this.decoder.readDoubleOrUndefined();

          this.order.orderComboLegs.push({
            price,
          });
        }
      }
    }
  }

  readSmartComboRoutingParams(): void {
    if (this.version >= 26) {
      const smartComboRoutingParamsCount = this.decoder.readInt();
      if (smartComboRoutingParamsCount > 0) {
        this.order.smartComboRoutingParams = [];
        for (let i = 0; i < smartComboRoutingParamsCount; ++i) {
          const tag = this.decoder.readStr();
          const value = this.decoder.readStr();
          this.order.smartComboRoutingParams.push({
            tag,
            value,
          });
        }
      }
    }
  }

  readScaleOrderParams(): void {
    if (this.version >= 15) {
      if (this.version >= 20) {
        this.order.scaleInitLevelSize = this.decoder.readIntOrUndefined();
        this.order.scaleSubsLevelSize = this.decoder.readIntOrUndefined();
      } else {
        /* int notSuppScaleNumComponents = */ this.decoder.readIntOrUndefined();
        this.order.scaleInitLevelSize = this.decoder.readIntOrUndefined();
      }
      this.order.scalePriceIncrement = this.decoder.readDoubleOrUndefined();
    }

    if (
      this.version >= 28 &&
      this.order.scalePriceIncrement &&
      this.order.scalePriceIncrement > 0.0
    ) {
      this.order.scalePriceAdjustValue = this.decoder.readDoubleOrUndefined();
      this.order.scalePriceAdjustInterval = this.decoder.readIntOrUndefined();
      this.order.scaleProfitOffset = this.decoder.readDoubleOrUndefined();
      this.order.scaleAutoReset = this.decoder.readBool();
      this.order.scaleInitPosition = this.decoder.readIntOrUndefined();
      this.order.scaleInitFillQty = this.decoder.readIntOrUndefined();
      this.order.scaleRandomPercent = this.decoder.readBool();
    }
  }

  readHedgeParams(): void {
    if (this.version >= 24) {
      this.order.hedgeType = this.decoder.readStr();
      if (this.order.hedgeType && this.order.hedgeType !== "") {
        this.order.hedgeParam = this.decoder.readStr();
      }
    }
  }

  readOptOutSmartRouting(): void {
    if (this.version >= 25) {
      this.order.optOutSmartRouting = this.decoder.readBool();
    }
  }

  readClearingParams(): void {
    if (this.version >= 19) {
      this.order.clearingAccount = this.decoder.readStr();
      this.order.clearingIntent = this.decoder.readStr();
    }
  }

  readNotHeld(): void {
    if (this.version >= 22) {
      this.order.notHeld = this.decoder.readBool();
    }
  }

  readDeltaNeutral(): void {
    if (this.version >= 20) {
      if (this.decoder.readBool()) {
        const conId = this.decoder.readInt();
        const delta = this.decoder.readDouble();
        const price = this.decoder.readDouble();
        this.contract.deltaNeutralContract = {
          conId,
          delta,
          price,
        };
      }
    }
  }

  readAlgoParams(): void {
    if (this.version >= 21) {
      this.order.algoStrategy = this.decoder.readStr();
      if (this.order.algoStrategy && this.order.algoStrategy !== "") {
        const algoParamsCount = this.decoder.readInt();
        if (algoParamsCount > 0) {
          this.order.algoParams = [];
          for (let i = 0; i < algoParamsCount; ++i) {
            const tag = this.decoder.readStr();
            const value = this.decoder.readStr();
            this.order.algoParams.push({
              tag,
              value,
            });
          }
        }
      }
    }
  }

  readSolicited(): void {
    if (this.version >= 33) {
      this.order.solicited = this.decoder.readBool();
    }
  }

  readWhatIfInfoAndCommission(): void {
    if (this.version >= 16) {
      this.order.whatIf = this.decoder.readBool();

      this.readOrderStatus();

      if (this.serverVersion >= MIN_SERVER_VER.WHAT_IF_EXT_FIELDS) {
        this.orderState.initMarginBefore = this.decoder.readDoubleOrUndefined();
        this.orderState.maintMarginBefore =
          this.decoder.readDoubleOrUndefined();
        this.orderState.equityWithLoanBefore =
          this.decoder.readDoubleOrUndefined();
        this.orderState.initMarginChange = this.decoder.readDoubleOrUndefined();
        this.orderState.maintMarginChange =
          this.decoder.readDoubleOrUndefined();
        this.orderState.equityWithLoanChange =
          this.decoder.readDoubleOrUndefined();
      }

      this.orderState.initMarginAfter = this.decoder.readDoubleOrUndefined();
      this.orderState.maintMarginAfter = this.decoder.readDoubleOrUndefined();
      this.orderState.equityWithLoanAfter =
        this.decoder.readDoubleOrUndefined();
      this.orderState.commission = this.decoder.readDoubleOrUndefined();
      this.orderState.minCommission = this.decoder.readDoubleOrUndefined();
      this.orderState.maxCommission = this.decoder.readDoubleOrUndefined();
      this.orderState.commissionCurrency = this.decoder.readStr();
      this.orderState.warningText = this.decoder.readStr();
    }
  }

  readOrderStatus(): void {
    this.orderState.status = this.decoder.readStr() as OrderStatus;
  }

  readVolRandomizeFlags(): void {
    if (this.version >= 34) {
      this.order.randomizeSize = this.decoder.readBool();
      this.order.randomizePrice = this.decoder.readBool();
    }
  }

  readPegToBenchParams(): void {
    if (this.serverVersion >= MIN_SERVER_VER.PEGGED_TO_BENCHMARK) {
      if (isPegBenchOrder(this.order.orderType)) {
        this.order.referenceContractId = this.decoder.readInt();
        this.order.isPeggedChangeAmountDecrease = this.decoder.readBool();
        this.order.peggedChangeAmount = this.decoder.readDouble();
        this.order.referenceChangeAmount = this.decoder.readDouble();
        this.order.referenceExchangeId = this.decoder.readStr();
      }
    }
  }

  readConditions(): void {
    if (this.serverVersion >= MIN_SERVER_VER.PEGGED_TO_BENCHMARK) {
      const nConditions = this.decoder.readInt();

      if (nConditions > 0) {
        this.order.conditions = new Array(nConditions);

        for (let i = 0; i < nConditions; i++) {
          const orderConditionType = this.decoder.readInt();

          // OrderCondition
          const conjunctionConnection = this.decoder
            .readStr()
            ?.toLocaleLowerCase();

          switch (orderConditionType) {
            case OrderConditionType.Execution: {
              const secType = this.decoder.readStr() as SecType;
              const exchange = this.decoder.readStr();
              const symbol = this.decoder.readStr();

              this.order.conditions[i] = new ExecutionCondition(
                exchange,
                secType,
                symbol,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }

            case OrderConditionType.Margin: {
              // OperatorCondition
              const isMore = this.decoder.readBool();
              const value = this.decoder.readInt();

              this.order.conditions[i] = new MarginCondition(
                value,
                isMore,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }

            case OrderConditionType.PercentChange: {
              // OperatorCondition
              const isMore = this.decoder.readBool();
              const value = this.decoder.readDouble();
              // ContractCondition
              const condId = this.decoder.readInt();
              const exchange = this.decoder.readStr();

              this.order.conditions[i] = new PercentChangeCondition(
                value,
                condId,
                exchange,
                isMore,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }

            case OrderConditionType.Price: {
              // OperatorCondition
              const isMore = this.decoder.readBool();
              const value = this.decoder.readDouble();
              // ContractCondition
              const condId = this.decoder.readInt();
              const exchange = this.decoder.readStr();
              // PriceCondition
              const triggerMethod = this.decoder.readInt() as TriggerMethod;

              this.order.conditions[i] = new PriceCondition(
                value,
                triggerMethod,
                condId,
                exchange,
                isMore,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }

            case OrderConditionType.Time: {
              // OperatorCondition
              const isMore = this.decoder.readBool();
              const value = this.decoder.readStr();

              this.order.conditions[i] = new TimeCondition(
                value,
                isMore,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }

            case OrderConditionType.Volume: {
              // OperatorCondition
              const isMore = this.decoder.readBool();
              const value = this.decoder.readInt();
              // ContractCondition
              const condId = this.decoder.readInt();
              const exchange = this.decoder.readStr();

              this.order.conditions[i] = new VolumeCondition(
                value,
                condId,
                exchange,
                isMore,
                conjunctionConnection as ConjunctionConnection,
              );
              break;
            }
          }
        }

        this.order.conditionsIgnoreRth = this.decoder.readBool();
        this.order.conditionsCancelOrder = this.decoder.readBool();
      }
    }
  }

  readAdjustedOrderParams(): void {
    if (this.serverVersion >= MIN_SERVER_VER.PEGGED_TO_BENCHMARK) {
      this.order.adjustedOrderType = this.decoder.readStr();
      this.order.triggerPrice = this.decoder.readDoubleOrUndefined();
      this.readStopPriceAndLmtPriceOffset();
      this.order.adjustedStopPrice = this.decoder.readDoubleOrUndefined();
      this.order.adjustedStopLimitPrice = this.decoder.readDoubleOrUndefined();
      this.order.adjustedTrailingAmount = this.decoder.readDoubleOrUndefined();
      this.order.adjustableTrailingUnit = this.decoder.readInt();
    }
  }

  readStopPriceAndLmtPriceOffset(): void {
    this.order.trailStopPrice = this.decoder.readDoubleOrUndefined();
    this.order.lmtPriceOffset = this.decoder.readDoubleOrUndefined();
  }

  readSoftDollarTier(): void {
    if (this.serverVersion >= MIN_SERVER_VER.SOFT_DOLLAR_TIER) {
      const name = this.decoder.readStr();
      const value = this.decoder.readStr();
      const displayName = this.decoder.readStr();
      this.order.softDollarTier = {
        name,
        value,
        displayName,
      };
    }
  }

  readCashQty(): void {
    if (this.serverVersion >= MIN_SERVER_VER.CASH_QTY) {
      this.order.cashQty = this.decoder.readDoubleOrUndefined();
    }
  }

  readDontUseAutoPriceForHedge(): void {
    if (this.serverVersion >= MIN_SERVER_VER.AUTO_PRICE_FOR_HEDGE) {
      this.order.dontUseAutoPriceForHedge = this.decoder.readBool();
    }
  }

  readIsOmsContainer(): void {
    if (this.serverVersion >= MIN_SERVER_VER.ORDER_CONTAINER) {
      this.order.isOmsContainer = this.decoder.readBool();
    }
  }

  readDiscretionaryUpToLimitPrice(): void {
    if (this.serverVersion >= MIN_SERVER_VER.D_PEG_ORDERS) {
      this.order.discretionaryUpToLimitPrice = this.decoder.readBool();
    }
  }

  readAutoCancelDate(): void {
    this.order.autoCancelDate = this.decoder.readStr();
  }

  readFilledQuantity(): void {
    this.order.filledQuantity = this.decoder.readDecimal();
  }

  readRefFuturesConId(): void {
    this.order.refFuturesConId = this.decoder.readInt();
  }

  readAutoCancelParent(minVersionAutoCancelParent?: number): void {
    if (
      minVersionAutoCancelParent === undefined ||
      this.serverVersion >= minVersionAutoCancelParent
    ) {
      this.order.autoCancelParent = this.decoder.readBool();
    }
  }

  readShareholder(): void {
    this.order.shareholder = this.decoder.readStr();
  }

  readImbalanceOnly(): void {
    this.order.imbalanceOnly = this.decoder.readBool();
  }

  readRouteMarketableToBbo(): void {
    this.order.routeMarketableToBbo = this.decoder.readBool();
  }

  readParentPermId(): void {
    this.order.parentPermId = this.decoder.readInt();
  }

  readCompletedTime(): void {
    this.orderState.completedTime = this.decoder.readStr();
  }

  readCompletedStatus(): void {
    this.orderState.completedStatus = this.decoder.readStr();
  }

  readUsePriceMgmtAlgo(): void {
    if (this.serverVersion >= MIN_SERVER_VER.PRICE_MGMT_ALGO) {
      this.order.usePriceMgmtAlgo = this.decoder.readBool();
    }
  }

  readDuration(): void {
    if (this.serverVersion >= MIN_SERVER_VER.DURATION) {
      this.order.duration = this.decoder.readInt();
    }
  }

  readPostToAts(): void {
    if (this.serverVersion >= MIN_SERVER_VER.POST_TO_ATS) {
      this.order.postToAts = this.decoder.readIntOrUndefined();
    }
  }

  readPegBestPegMidOrderAttributes() {
    if (this.serverVersion >= MIN_SERVER_VER.PEGBEST_PEGMID_OFFSETS) {
      this.order.minTradeQty = this.decoder.readIntOrUndefined();
      this.order.minCompeteSize = this.decoder.readIntOrUndefined();
      this.order.competeAgainstBestOffset =
        this.decoder.readDoubleOrUndefined();
      this.order.midOffsetAtWhole = this.decoder.readDoubleOrUndefined();
      this.order.midOffsetAtHalf = this.decoder.readDoubleOrUndefined();
    }
  }

  readCustomerAccount() {
    if (this.serverVersion >= MIN_SERVER_VER.CUSTOMER_ACCOUNT) {
      this.order.customerAccount = this.decoder.readStr();
    }
  }

  readProfessionalCustomer() {
    if (this.serverVersion >= MIN_SERVER_VER.PROFESSIONAL_CUSTOMER) {
      this.order.professionalCustomer = this.decoder.readBool();
    }
  }
}