Quantitative Finance Insights

Quantitative Finance Insights

Building a Crypto Trading Bot from Scratch - 2: Designing Data Models for Trading Systems

QPY's avatar
QPY
Nov 19, 2025
∙ Paid

a computer screen displaying a stock market chart
Photo by Behnam Norouzi on Unsplash

In my last post, I talked about the overall architecture of my crypto trading bot. Today, I want to zoom in on something that might seem boring but is actually critical: data models.

Building a Crypto Trading Bot from Scratch - 1: Why Architecture Matters

When I first started planning this project, I underestimated how important it would be to get the data models right from the beginning. I thought, “They’re just data structures, I can always change them later.”

Well, yes and no. You can change them later, but it will be painful as hell. Your data models touch every part of your system. They’re the contracts between components. Get them wrong, and you’ll spend weeks refactoring instead of building your next features.

So let me show you the four core data models that form the foundation of my trading system, why they’re designed the way they are, and what I learned from getting them (mostly) right.

The Four Core Models

My trading system revolves around four data types:

  1. Candle: Market price data (OHLCV)

  2. Signal: Trading signals generated by strategies

  3. Trade: Executed orders with outcomes

  4. Position: Open positions being tracked

Every piece of data flowing through the system is one of these types. The Strategy Engine produces Signals. The Trade Executor produces Trades. The Position Manager tracks Positions. And everything uses Candles as the raw input.

Let’s walk through each one.

Candle: The Foundation of Everything

In trading, a “candle” (or candlestick) represents price movement over a specific time period. It tells you the opening price, highest price, lowest price, closing price, and trading volume for that period.

Here’s my Candle model:

@dataclass
class Candle:
    timestamp: datetime
    symbol: str
    timeframe: str
    open: float
    high: float
    low: float
    close: float
    volume: float

Simple, right? But there are some subtle decisions here.

Why timestamp as datetime?

I could have used Unix timestamps (integers), but datetime objects are way easier to work with in Python. Need to filter candles by date? Need to calculate time differences? datetime makes it trivial.

Why include symbol and timeframe in every candle?

This might seem redundant—why not just have a collection of candles and know what symbol they’re for?

The reason is flexibility. When you’re managing data for multiple symbols and timeframes, having this metadata in each candle prevents bugs. You never have to wonder “wait, which symbol is this candle for again?”

Why float for prices?

Some people prefer Decimal for financial calculations to avoid floating-point rounding errors. I went with float for simplicity and performance. For crypto prices, the rounding errors are negligible compared to the volatility. If I was building a system for forex or stocks with tighter spreads, I’d reconsider this.

Signal: Making Decisions Transparent

This is where things get interesting. The Signal model represents a trading decision made by a strategy. Here’s what it looks like:

@dataclass
class Signal:
    signal_id: str
    timestamp: datetime
    symbol: str
    strategy_name: str
    signal_type: SignalType  # BUY, SELL, HOLD
    strength: float  # 0.0 to 1.0
    reason: str
    indicators_state: Dict[str, Any]
    price: float
    action_taken: ActionType  # EXECUTED, IGNORED, PENDING

This model embodies one of my core principles: signal transparency. Every field here serves a purpose.

signal_id

A unique identifier. This lets us track a signal from generation to execution (or rejection). If an order fails, we can trace it back to the exact signal that triggered it.

strategy_name

When you’re running multiple strategies, you need to know which one generated each signal. This is crucial for debugging and performance analysis.

signal_type and strength

signal_type is straightforward—BUY, SELL, or HOLD. But strength is more nuanced.

Strength represents how confident the strategy is in this signal, from 0.0 (not confident) to 1.0 (very confident). Not all buy signals are created equal. Maybe your strategy sees a weak EMA crossover versus a strong one with confirming volume. The strength captures that.

You can use strength for risk sizing (bet more on high-confidence signals) or for filtering (ignore signals below a confidence threshold).

reason and indicators_state

This is the game-changer. Every signal includes:

  • A human-readable explanation (reason)

  • The full state of all indicators at signal time (indicators_state)

Here’s what that looks like in practice:

Signal(
    signal_id=”a7f23b...”,
    timestamp=datetime(2025, 11, 4, 14, 30),
    symbol=”BTCUSDT”,
    strategy_name=”EMA_Crossover_BTC”,
    signal_type=SignalType.BUY,
    strength=0.85,
    reason=”Fast EMA (12) crossed above Slow EMA (26)”,
    indicators_state={
        ‘fast_ema’: 43250.00,
        ‘slow_ema’: 43200.00,
        ‘volume’: 125000,
        ‘rsi’: 58.3
    },
    price=43250.00,
    action_taken=ActionType.PENDING
)

When I review this signal later, I know exactly what the bot was thinking. The fast EMA crossed above the slow EMA, here are the exact values, and the RSI was at 58.3. No mystery, no guesswork.

action_taken

Not every signal becomes a trade. Maybe it violated a risk rule. Maybe there was already an open position. Maybe the order failed.

action_taken tracks what happened:

  • PENDING: Signal just generated, not yet processed

  • EXECUTED: Order placed successfully

  • IGNORED: Signal rejected (risk rule, insufficient balance, etc.)

This creates a complete audit trail. You can answer questions like “How many signals did my strategy generate last week?” and “How many actually became trades?”

Trade: The Record of What Actually Happened

User's avatar

Continue reading this post for free, courtesy of QPY.

Or purchase a paid subscription.
© 2025 QPY · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture