Quant Trading for Programmers 02: Build The Python Project Structure From Scratch
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.

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:

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.
Follow ZiCode on WeChat
If this post was useful, you can follow later updates on WeChat as well.
X / Twitter
Follow @ax2_zicode
Faster technical notes, short thoughts, and new-post alerts are posted on X.
More in this column
- Quant Trading for Programmers 06: Make The Database Schema Explicit First
- Quant Trading for Programmers 05: Data Matters More Than Strategy
- Quant Trading for Programmers 04: How A-Share Trading Rules Change Code
- Quant Trading for Programmers 03: Core Concepts In Quant Trading
- Quant Trading for Programmers 01: Why Programmers Are Well Suited To Quant Trading