Hello! Welcome back to the course.
In our last lesson, we successfully built the IBConnectionManager, giving our framework a stable connection to the outside world. This was a crucial step, but a connection is only useful if we can send and receive meaningful information through it. Before we can ask the broker for data, we need to decide how we're going to represent that data inside our Python application.
Today's lesson focuses on precisely that. Our learning outcome is to define data classes to represent a single option contract, an options chain, and a credit spread position. We will create a clean, robust, and efficient data model that will serve as the backbone for all subsequent logic, from fetching market data to executing trades.
To achieve this, we will be using Python's dataclasses module. This modern feature allows us to create classes that are optimized for storing data with minimal boilerplate code, which is perfect for our needs.
Why Dataclasses are a Great Fit for Financial Modeling
When you write a standard Python class to hold data, you often end up writing a lot of repetitive "boilerplate" code: the __init__ method to assign attributes, the __repr__ method to get a readable string representation, and the __eq__ method to compare two objects. For data-centric objects like financial instruments, this can become tedious and error-prone.
Python dataclasses act as code generators. By simply adding the @dataclass decorator and declaring your fields with type hints, Python automatically writes these methods for you. This leads to cleaner code and, more importantly for our goal of a "foolproof" system, allows us to easily create immutable objects.
The following video from the ArjanCodes channel provides an excellent overview of what makes dataclasses so powerful.
This Is Why Python Data Classes Are Awesome
Watch this video, 'This Is Why Python Data Classes Are Awesome', to get a quick and practical introduction to the key features of dataclasses.
Please watch the following segments: Introduction (00:51 - 05:07): Understand how dataclasses reduce boilerplate code for __init__ and __repr__. Default Values (05:38 - 08:51): Pay close attention to field(default_factory=...), which is essential for fields that are mutable types like lists. Immutability with frozen=True (12:51 - 14:05): This is a critical concept for us. Understand how it makes instances read-only after creation. Performance with slots=True (17:01 - 19:44): Learn about this optimization that improves memory usage and attribute access speed, which is beneficial in data-intensive applications.
1. Defining a Single Option Contract
Our first and most fundamental data structure is the one for a single option contract. To define a contract that Interactive Brokers can understand, we need to provide a specific set of attributes.
The IBKR API documentation shows exactly which fields are needed to uniquely identify an option. Let's use that as our guide.
TWS API v9.72+: Basic Contracts
This documentation page, 'TWS API v9.72+: Basic Contracts', details the fields required to define various financial instruments. We will focus on the 'Options' section.
Scan the Python code example under the 'Options' heading. Note the attributes being set on the contract object: symbol, secType, exchange, currency, lastTradeDateOrContractMonth, strike, right, and multiplier. These are the fields we need to include in our data class.
Based on this, let's create our OptionContract data class. We will place this and the other data classes in a new file, framework/data_models.py.
A crucial design choice here is to make our data objects immutable by setting frozen=True. Once an OptionContract object is created, its definition (strike, expiry, etc.) cannot be changed. This prevents a whole class of bugs where contract details might be accidentally modified elsewhere in the program, contributing directly to our "foolproof" system design.
We'll also use slots=True for the performance benefits you learned about in the video.
# In framework/data_models.py
from dataclasses import dataclass, field
from datetime import date
from typing import Literal
@dataclass(frozen=True, slots=True)
class OptionContract:
"""
Represents a single, uniquely identifiable option contract.
The fields are based on the Interactive Brokers `Contract` object
to ensure compatibility. The combination of these fields defines
a unique instrument.
"""
symbol: str # The underlying symbol, e.g., 'SPY'
secType: str = field(default="OPT", init=False)
exchange: str = "SMART"
currency: str = "USD"
lastTradeDateOrContractMonth: str # Expiration date in 'YYYYMMDD' format
strike: float
right: Literal["C", "P"] # 'C' for Call, 'P' for Put
multiplier: str = "100"
# IBKR requires a unique conId to identify a contract.
# We will fetch this from the API later.
conId: int | None = field(default=None, compare=False)
Key design points in this class:
frozen=True: Ensures that once you create anOptionContractinstance, it cannot be accidentally modified. This is key for data integrity.slots=True: A performance optimization that reduces memory footprint.secType = field(default="OPT", init=False): Since our framework only deals with options, this is a fixed value.init=Falsemeans we don't have to provide it when creating an instance; it's set automatically.right: Literal["C", "P"]: Usingtyping.Literalprovides static type checking, ensuring this field can only be 'C' or 'P'.conId: int | None = field(default=None, compare=False): TheconIdis a unique ID assigned by Interactive Brokers. We will fetch this later. By settingcompare=False, we are telling the dataclass to ignore this field when comparing twoOptionContractinstances. This is correct, as two objects can represent the same contract even if we haven't fetched theirconIdyet.
2. Defining an Options Chain
An options chain is the set of all available option contracts (calls and puts) for a given underlying security for a specific expiration date.

We can model this as a dataclass that holds the underlying's symbol, the expiration, and two lists of our OptionContract objects.
# Continuing in framework/data_models.py
from dataclasses import dataclass, field
from datetime import date
from typing import List, Literal
# ... (OptionContract class from above) ...
@dataclass(frozen=True, slots=True)
class OptionsChain:
"""
Represents the full options chain for a single underlying and expiration date.
"""
underlying_symbol: str
expiration: date
calls: List[OptionContract] = field(default_factory=list)
puts: List[OptionContract] = field(default_factory=list)
Notice the use of default_factory=list. As explained in the ArjanCodes video, this is the correct way to initialize mutable default values like lists or dictionaries in a dataclass. It ensures that each new OptionsChain instance gets its own empty list, rather than all instances sharing the same list.
Test your understanding!
The OptionContract dataclass we defined is immutable because we set frozen=True. This also has an important side effect: it makes the object hashable. This means you can use OptionContract instances as keys in a dictionary or elements in a set.
Why is making our data models hashable a significant advantage for our trading framework? Think about how you might need to store or look up data related to a specific contract.
Show answer
Hashability is crucial for performance and clean data management. For example, if you are tracking real-time market data or open positions, you can use a dictionary where the OptionContract object itself is the key:
market_data_cache = {
# Key is the OptionContract object, value could be another dataclass with bid/ask/last prices
aapl_call_contract: MarketData(bid=1.50, ask=1.52, last=1.51),
spy_put_contract: MarketData(bid=2.10, ask=2.11, last=2.10)
}
# This allows for very fast and intuitive lookups:
current_price = market_data_cache[aapl_call_contract].last
Without hashable contract objects, you would have to create an artificial key (like a string concatenating the symbol, strike, and expiry), which is less elegant and potentially slower. The MathByte Academy video A Deep Dive into Python's Dataclasses (Part 1) (resource LINK) provides a very detailed explanation of equality, hashability, and immutability from 13:21 to 30:37 if you're interested in the deep mechanics.
3. Defining a Credit Spread Position
Finally, we need a way to represent the actual position we want to trade: a credit spread. A credit spread consists of two legs: a short option (the one you sell) and a long option (the one you buy).
Our CreditSpreadPosition will group these two OptionContract objects and include metadata about the spread itself.
# Continuing in framework/data_models.py
import uuid
# ... (previous classes) ...
def generate_position_id():
"""Generates a unique ID for a position."""
return f"pos_{uuid.uuid4().hex[:8]}"
@dataclass(frozen=True, slots=True)
class CreditSpreadPosition:
"""
Represents a two-legged credit spread position.
"""
position_id: str = field(default_factory=generate_position_id, init=False)
underlying_symbol: str
spread_type: Literal["Bull Put", "Bear Call"]
short_leg: OptionContract
long_leg: OptionContract
Here, we've introduced a position_id field. We use default_factory with a helper function to assign a unique ID to every new position automatically. Setting init=False means it's an internal detail that we don't need to provide when creating a CreditSpreadPosition. This encapsulates the logic for ID generation within the data model itself.
Conclusion
In this lesson, you have laid the data foundation for our entire trading framework. By leveraging Python's dataclasses, we have created clear, robust, and efficient models for the core entities in our domain.
Key Takeaways:
- Data Classes for Modeling:
@dataclassis the ideal tool for creating data-centric classes likeOptionContract,OptionsChain, andCreditSpreadPosition, as it drastically reduces boilerplate code. - Immutability is Key: Using
frozen=Truemakes our data models immutable. This is a powerful feature for building a reliable ("foolproof") system, as it prevents accidental state changes and makes instances hashable, allowing them to be used as dictionary keys. - Tie Models to Reality: Our
OptionContractdefinition is directly based on the requirements of the Interactive Brokers API, ensuring that our internal representation can be easily translated into a format the broker understands. - Smart Defaults: Features like
default_factoryandinit=Falseallow for powerful and safe default values, such as auto-generated IDs and empty lists for collections.
Preview of the Next Lesson:
Our OptionContract model includes an expiration date, but many trading decisions rely on the time remaining until expiration (Days to Expiration, or DTE). In the next lesson, we will build a utility for managing time-related logic, such as market hours and calculating DTE. This utility will consume the OptionContract objects we defined today and add critical, time-sensitive information to them.