Quant500 Academy

1. State is the problem: what to store, where and why

Level 8: State and reliable execution

A backtest has no state. It starts, walks a vector of prices and finishes. If you kill it halfway you relaunch it and nothing has happened. A production system is precisely the opposite: it exists to accumulate state over time, and that state is worth money. If you lose it you do not lose a run, you lose the ability to know what you hold in the market.

What state is made of

When we say "the state" of a trading system we are talking about five different things, and it is best not to mix them up because each is stored and recovered differently:

Why the process's memory is no good

A process dies. This is not a remote hypothesis: it dies because you deploy a new version, because the operating system kills it when it runs out of memory, because an uncaught exception bubbles all the way up, because the server reboots for a kernel patch, or because somebody stops it by hand. And here is what matters: the market does not find out that your Python has died. The position stays open, the orders stay in the book and the fills keep happening while your process does not exist.

An in-memory dictionary also has a defect worse than disappearing: it lies quietly. While the process lives, that variable looks like absolute truth, and nobody ever checks whether it still matches reality.

The broker is the source of truth; your database is a cache

This is the rule that organizes the whole level: the broker is the source of truth for positions and orders; your database is a cache and a journal of intentions. In any discrepancy the broker wins, always, no argument. Your job is not to be right, it is to detect the difference and stop trading until you understand it. The formal process of comparing and adjusting has its own name, reconciliation, and it is covered in depth in Level 10; here it is enough that you accept who is in charge.

That said, there is information the broker does not have and that only exists on your side:

From that comes the practical rule: always ask the broker for positions, open orders and cash; store on disk everything the broker can never give back to you.

Writing to disk without leaving the file half-done

Opening a file in write mode empties it immediately. If the process dies between that truncation and the end of the dump, you are left with a truncated or empty file, and on startup the system does not find corrupt state: it finds none. The solution is never to write over the good one.

import json
import os
import tempfile

def guardar_atomico(ruta, datos):
    directorio = os.path.dirname(os.path.abspath(ruta))
    fd, temporal = tempfile.mkstemp(dir=directorio, suffix='.tmp')
    try:
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            json.dump(datos, f, ensure_ascii=False, indent=2)
            f.flush()
            os.fsync(f.fileno())
        os.replace(temporal, ruta)
    except BaseException:
        if os.path.exists(temporal):
            os.remove(temporal)
        raise

Three details that are not decoration. The temporary file is created in the same directory because the rename is only atomic within the same file system. The fsync forces the bytes onto the disk for real, not into the operating system's buffer, before the rename declares them official. And os.replace is atomic on both Linux and Windows: any reader will see either the old file whole or the new file whole, never a mixture.

Better still: let a database do it

If you are going to write several things that must change together, a JSON file falls short very quickly. SQLite ships in the standard library, needs no server and gives you transactions and durability ready made.

import sqlite3

con = sqlite3.connect('estado.db', timeout=30)
con.execute('PRAGMA journal_mode=WAL')
con.execute('PRAGMA synchronous=FULL')
con.execute("""
CREATE TABLE IF NOT EXISTS ordenes (
    client_order_id TEXT PRIMARY KEY,
    simbolo         TEXT NOT NULL,
    lado            TEXT NOT NULL,
    cantidad        REAL NOT NULL,
    estado          TEXT NOT NULL,
    barra           TEXT NOT NULL,
    broker_id       TEXT,
    creada_en       TEXT NOT NULL
)""")
con.commit()

Look at the first column: it is the primary key. That constraint, which looks like a footnote, is the pillar of the whole next module.

Worked scenario: forty seconds of blindness

A scenario built for the course, not a documented case. You deploy at 15:44:10 and the process takes forty seconds to come back. In that gap, a buy order for 300 units that was already in the book is partially filled: 120 units. The process starts up, reads its local file, which was saved before the deployment and says "0 filled, order live", and decides it is still flat. The next signal computes the size from scratch and sends another buy for 300. Result: 420 units where the model believed it had 300, 40% extra exposure that no risk rule authorized, and all because of trusting a forty-second-old cache instead of asking. The failure was not the market's nor the broker's: it was believing that a file knows what is going on outside.

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.