Quant Trading for Programmers 02: Build The Python Project Structure From Scratch
· 41 min read · Views --
Last updated on

Quant Trading for Programmers 02: Build The Python Project Structure From Scratch

Author: Alex Xiang


Quant Trading for Programmers 02: Build The Python Project Structure From Scratch

In the previous article, we first made the boundary clear: this series only does research, backtesting, alerts, and paper trading. It does not connect to brokerages and does not submit real orders.

This article starts the engineering skeleton. A quant project easily begins as a notebook or one main.py, then becomes chaotic: data sync scripts live on the desktop, strategy parameters are hardcoded, database connections are scattered across functions, and testing depends on clicking pages manually. Once the strategy grows slightly complex, the project becomes hard to reproduce.

So article 2 does not rush into strategy. We first make ZiQuant an installable, runnable, configurable, testable Python project.

Cover image for article 2 in Quant Trading for Programmers

Why A Quant Project Needs Engineering Discipline

Many introductory quant tutorials start with a short backtest:

prices = get_price("600519.SH")
signal = prices.close > prices.close.rolling(20).mean()

That is intuitive, but it only answers “can I calculate one indicator?” A real platform needs to answer more:

  • Where does the data come from, and how do we know if it fails?
  • How are database schema changes upgraded?
  • Were strategy parameters changed, and by whom?
  • Can backtest results be reproduced?
  • How are failed scheduled jobs rerun?
  • Why was a paper order rejected?
  • Can the project start on another machine?

These questions are not finance questions. They are engineering questions. This is where programmers have an advantage: do not treat strategy as isolated code; put it inside a runnable system.

First Directory Structure

The current core directory structure of ZiQuant is:

zi-quant-platform/
  app/
    __init__.py
    db.py
    main.py
    models.py
    services.py
    settings.py
    static/
  scripts/
    check_migrations.py
    db_backup.py
    production_smoke.py
    run_due_jobs.py
  tests/
    test_db_backup.py
    test_production_smoke.py
    test_run_due_jobs.py
    test_services.py
  migrations/
    env.py
    versions/
  .env.example
  .gitignore
  alembic.ini
  pyproject.toml
  README.md
  uv.lock

This structure is not for appearance. Each layer has a role.

app/ is the main service. FastAPI entry points, configuration, database connections, ORM models, and core business services live here. Later stock pools, market data, factors, strategies, backtests, paper trading, and admin APIs will grow from here.

scripts/ contains repeatable operations: due-job runners, database backup, production smoke checks, and migration checks. Scripts are as important as Web APIs because a quant platform runs scheduled jobs every day.

tests/ contains unit tests and lightweight integration tests. This series will often change trading rules, factor logic, and strategy admission conditions. Without tests, it is easy to break earlier chapters.

migrations/ contains database migrations. A quant platform’s data model evolves continuously. “Drop and recreate the database” is not a maintenance strategy.

pyproject.toml and uv.lock pin dependencies and command entry points. Once a project enters long-term maintenance, dependency reproducibility is not optional.

Manage Dependencies With uv

ZiQuant uses uv for Python environments. The main dependencies in pyproject.toml include:

[project]
name = "zi-quant-platform"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "alembic>=1.13.0",
  "asyncpg>=0.31.0",
  "fastapi>=0.115.0",
  "httpx>=0.27.0",
  "pydantic>=2.8.0",
  "pydantic-settings>=2.4.0",
  "sqlalchemy[asyncio]>=2.0.30",
  "uvicorn[standard]>=0.30.0",
]

Development dependencies are in the dev extra:

[project.optional-dependencies]
dev = [
  "pytest>=8.0",
  "pytest-asyncio>=0.24.0",
  "httpx>=0.27.0",
]

Install:

cd ~/projects/zi-quant-platform
uv sync --extra dev

I do not recommend putting pandas, machine-learning frameworks, backtesting frameworks, and plotting libraries into the first version. More dependencies make the project heavier and environment issues harder to debug. Add them when needed, and explain what problem each dependency solves.

Configuration Must Be Layered From Day One

A quant project touches databases, data-source API keys, DeepSeek keys, Lark chat IDs, production-mode switches, and access tokens. If configuration is scattered in code, it will eventually cause trouble.

ZiQuant reads .env through pydantic-settings:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    database_url: str = "postgresql+asyncpg:///postgres?host=/var/run/postgresql"
    host: str = "127.0.0.1"
    port: int = 8092
    qveris_data_api_key: str = ""
    deepseek_api_key: str = ""
    zi_api_token: str = ""
    zi_deployment_mode: str = "development"
    auto_create_schema: bool = True

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")


settings = Settings()

This code looks ordinary, but it establishes several rules.

First, defaults let local development start quickly.

Second, secrets default to empty strings and are not hardcoded.

Third, production mode and development mode are separated by configuration. AUTO_CREATE_SCHEMA=true is convenient locally, but production should run Alembic migrations explicitly and should not let service startup quietly change tables.

Fourth, .env.example can be committed. .env must not be committed.

.gitignore should at least include:

.env
.venv/
__pycache__/
.pytest_cache/

Later QVeris, DeepSeek, and Lark integrations will reuse this configuration boundary.

Keep Database Connections Simple First

The current database connection lives in app/db.py:

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.models import Base
from app.settings import settings

engine = create_async_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)


async def init_schema() -> None:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


async def get_session():
    async with SessionLocal() as session:
        yield session

We start with SQLAlchemy async because later Web APIs, data sync jobs, and task runners will all access the database frequently. The async stack aligns better with FastAPI.

init_schema() is a local-development convenience. It helps a new environment start quickly. It does not mean production should create tables automatically. Production deployment should run:

uv run alembic upgrade head

The project also has a migration check:

uv run python scripts/check_migrations.py

This is the difference between an engineering skeleton and a one-off script. A one-off script cares only about “can it run now?” An engineering project asks “how do we upgrade it next time?”

Add Health Checks Early

A service that starts is not necessarily a service that works. ZiQuant begins with two check endpoints.

/health is lightweight:

@app.get("/health")
async def health(session: AsyncSession = Depends(get_session)) -> dict:
    db_ok = True
    try:
        await session.execute(select(Stock).limit(1))
    except Exception:
        db_ok = False
    return {"status": "ok" if db_ok else "degraded", "database": db_ok, "tables": table_names()}

It does not make complex business judgments. It answers one question: are the service and database basically available?

/ready will be stricter. Later it will check migrations, stock universe, data sources, strategies, paper trading, and task status. Before going online, do not only check that a process is alive. Check whether the system is actually ready to run.

Start the service:

uv run uvicorn app.main:app --host 127.0.0.1 --port 8092

Check status:

curl http://127.0.0.1:8092/health
curl http://127.0.0.1:8092/ready

If /health is not stable in the first stage, do not rush into strategy code.

Write A Minimal Test

Article 2 is accepted not when files exist, but when tests can run.

The current pytest config is in pyproject.toml:

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

Run:

uv run pytest

Project tag for this chapter:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-02
uv sync --extra dev
uv run pytest

The first tests do not need to be complex. Seed stock-pool count, core table presence, config masking, and permission checks are all good early tests.

This kind of test is useful:

def test_seed_pool_has_500_unique_stocks():
    stocks = build_seed_stocks()
    assert len(stocks) == 500
    assert len({s["symbol"] for s in stocks}) == 500
    assert stocks[0]["symbol"] == "600519.SH"

It does not look like glamorous quant work, but it protects the running range of all later strategies. If the stock universe has duplicates, missing symbols, or invalid codes, later factors and backtests are polluted.

Why Console Scripts Matter Now

pyproject.toml also has command entry points:

[project.scripts]
zi-quant-platform = "app.main:run"
zi-quant-run-due-jobs = "scripts.run_due_jobs:main"
zi-quant-production-smoke = "scripts.production_smoke:main"
zi-quant-db-backup = "scripts.db_backup:main"

This is not only to type fewer characters. Its value is stable operational entry points.

When the project is deployed, a cron job is added, a systemd service is written, or another person takes over, they should not copy long Python file paths from the README. Stable entry points help operations settle.

Later scheduled jobs will run like this:

uv run zi-quant-run-due-jobs --limit 3

Production smoke checks will run like this:

uv run zi-quant-production-smoke --base-url http://127.0.0.1:8092

Database backup will run like this:

uv run zi-quant-db-backup --output-dir /var/backups/zi-quant

These commands will be expanded in later articles. For now, keep the entry points in place.

Runnable Foundation Check

This article is about the engineering skeleton. The minimal check is not whether a page looks nice, but whether default configuration and common commands are clear. The current project uses this command to reproduce the foundation capabilities from articles 01-08:

uv run python -m scripts.chapter_examples foundation-check

The output for this chapter is:

Python project-structure foundation-check output

The screenshot prints configuration defaults without reading local .env, so readers see the same semantics on different machines. uv sync --extra dev, uv run pytest, and uv run uvicorn app.main:app are the first three commands that must become stable in this project.

Hands-On Task

If you follow from scratch, article 2’s task is to build a project skeleton that can be accepted.

Create directories:

mkdir -p zi-quant-platform/{app,scripts,tests,migrations/versions}
touch zi-quant-platform/app/__init__.py
touch zi-quant-platform/scripts/__init__.py

Initialize:

cd zi-quant-platform
uv init --package

Add dependencies:

uv add fastapi "uvicorn[standard]" "sqlalchemy[asyncio]" asyncpg alembic pydantic pydantic-settings httpx
uv add --dev pytest pytest-asyncio

Write .env.example:

DATABASE_URL=postgresql+asyncpg:///postgres?host=/var/run/postgresql
HOST=127.0.0.1
PORT=8092
ZI_DEPLOYMENT_MODE=development
AUTO_CREATE_SCHEMA=true
ZI_API_TOKEN=

Create .gitignore:

.env
.venv/
__pycache__/
.pytest_cache/

Then run:

uv sync --extra dev
uv run pytest

If no tests exist yet, at least make pytest start and add one minimal test. An empty project with 0 tests is not really done.

Common Pitfalls

The first pitfall is committing .env.

Even if it only has a local database address today, real API keys will appear later. Commit .env.example, not .env.

The second pitfall is making defaults too “production-like”.

Local development can auto-create tables, use default ports, and allow an empty token. Production should be the opposite: explicit migrations, required token, no auto-create schema. Later /ready and production smoke checks will verify these differences.

The third pitfall is treating notebooks as project structure.

Notebooks are good for exploration, not for long-running platforms. Once we sync real market data, write to databases, and run scheduled jobs, core logic must return to app/ and scripts/.

The fourth pitfall is testing only HTTP 200.

An API returning 200 does not mean the strategy is correct. Quant projects need tests for business constraints: stock-universe count, A-share board lots, fee calculation, data deduplication, backtest metrics, and strategy promotion rules. Building this habit early saves time later.

Chapter Update And Repository

This chapter adds:

  • ZiQuant’s Python project skeleton.
  • uv, pyproject.toml, .env.example, FastAPI, and pytest.
  • Verification that the project can install, start, and run tests.

Repository:

https://github.com/ax2/zi-quant-platform

Code for this chapter:

git clone https://github.com/ax2/zi-quant-platform.git
cd zi-quant-platform
git checkout chapter-02
uv sync --extra dev
uv run pytest

Summary

Article 2 does not write a money-making strategy. It finishes something more important: putting ZiQuant into a maintainable Python engineering structure.

The project should now have four minimum capabilities:

  • Install with uv sync --extra dev.
  • Manage local configuration through .env.
  • Start FastAPI and access /health.
  • Run regression checks with uv run pytest.

The next article enters core quant concepts and translates stock universe, K-lines, factors, signals, positions, and backtest metrics into data structures that will later be stored and passed around.