Skip to main content

Strategy Tracker — Overview

Three PostgreSQL tables that define what a strategy is and what regime filters exist. This is a master/definition layer only — computing actual regime values (daily or any other timeframe) is a separate pipeline, designed later.

Managed with plain SQLAlchemy 2.0 models + Alembic, not raw .sql files. An earlier, much larger st_-prefixed schema (st_strategy, st_regime_def, run/trade/metrics tables, a bespoke strategies/db/migrate.py) was scoped out in backend/strategies/ before this was built and has since been removed. This package is a smaller, independent replacement, not an implementation of that plan — it now lives in the same backend/strategies/ path (reused after the removal, not a leftover of it).


Package File Map

FileRole
strategies/models.pySQLAlchemy models — StrategyClassification (table st_group), Strategy (table st_master), RegimeMaster
alembic/env.py, alembic.iniAlembic config (backend root)
alembic/versions/d7379ebee87b_create_strategy_tracker_tables.pyCreates all 3 tables (as strategy_classification/strategy/regime_master)
alembic/versions/2141dce5b2f0_rename_strategy_classification_and_.pyRenames strategy_classification/strategyst_group/st_master (regime_master untouched)
strategies/seed_strategy_classification.pyIdempotent seed for st_group (13 rows)
tests/conftest.py, tests/test_strategy_models.pypytest — asserts hypothesis NOT NULL is enforced at the DB level

Why a separate declarative base

strategies/models.py defines its own StrategyBase(DeclarativeBase) instead of reusing the shared app.database.session.Base used by the pre-existing NSE/MCX models in app/models/models.py.

Those models map tables Alembic never created (scr_master, scr_nseeq_eod, holidays, ...) and their ORM definitions can drift from the live Postgres schema in ways that are harmless at runtime. They are not harmless to alembic --autogenerate: pointing autogenerate at the full shared metadata diffs every table in the connected schema against it, and queues DROP TABLE for every production table it doesn't recognize. This actually happened once while building this migration — caught before applying, never shipped. alembic/env.py now does two things to keep this from recurring:

  1. target_metadata = StrategyBase.metadata — only the 3 strategy-tracker tables.
  2. An include_object filter that skips any reflected table/index not in that metadata, so autogenerate never proposes touching anything it doesn't own.

Regular queries/inserts work identically regardless of which declarative base a model uses — only Base.metadata (autogenerate/create_all) needs the split.


Schema

st_group (class StrategyClassification)

Controlled vocabulary for tagging a strategy. Seeded, never computed. Named strategy_classification until migration 2141dce5b2f0 renamed it (Python class name unchanged).

ColumnTypeNotes
idSERIAL PK
axisVARCHAR(20) NOT NULLCHECK IN ('horizon','nature','family')
codeVARCHAR(50) NOT NULLUNIQUE (see below)
labelVARCHAR(100) NOT NULLhuman-readable

UNIQUE(axis, code) as specified, plus a single-column UNIQUE(code) — Postgres can only FK to a column set that carries its own unique/PK constraint, and the composite unique above doesn't cover code by itself. st_master.horizon/nature/family FK to code directly, which relies on this. Codes are distinct across axes in the seed data, so this doesn't loosen anything the composite unique implies. One known gap: a plain FK to code doesn't check that e.g. st_master.horizon only ever holds an axis='horizon' code — enforcing that would need a composite FK against a generated axis column, which was deliberately left out to keep this master table lean.

Seed values:

axiscodes
horizonintraday, swing, positional
naturetime_based, level_based, indicator_based, chart_based, statistical
familybreakout, mean_reversion, trend_following, volatility, arbitrage

st_master (class Strategy)

One tracked, parameterized strategy instance. Named strategy until migration 2141dce5b2f0 renamed it (Python class name unchanged).

ColumnTypeNotes
idSERIAL PK
strategy_codeVARCHAR(100) NOT NULL UNIQUE
param_hashVARCHAR(64) NOT NULL UNIQUEhash of canonical_json
canonical_jsonTEXT NOT NULLthe exact string that was hashed — kept as text, not JSONB, so it stays byte-stable
display_nameVARCHAR(200) NOT NULL
sourceVARCHAR(20) nullableCHECK IN ('amibroker','algotest','stockmock','tradingview','manual')
symbolVARCHAR(50) NOT NULLsingle instrument
timeframeVARCHAR(20) NOT NULLfree text (5min, daily, ...)
param_valuesJSONB NOT NULL DEFAULT '{}'GIN-indexed (ix_st_master_param_values)
horizon, nature, familyVARCHAR(50) nullableFK → st_group.code
hypothesisTEXT NOT NULLhard constraint — no backtest stats allowed as a substitute
statusVARCHAR(20) NOT NULL DEFAULT 'candidate'CHECK IN ('candidate','paper','live','retired')
created_atTIMESTAMPTZ NOT NULL DEFAULT now()
retired_atTIMESTAMPTZ nullable
retired_reasonTEXT nullable

regime_master

Defines WHAT a regime filter is — not its computed daily values. Starts empty; regime data generation is out of scope for this table.

ColumnTypeNotes
idSERIAL PK
nameVARCHAR(100) NOT NULL UNIQUEe.g. open_above_pdh, vix_high
categoryVARCHAR(20) NOT NULLCHECK IN ('gap','openrange','trend','volatility','calendar')
hypothesisTEXT NOT NULLeconomic reasoning — hard constraint
definitionTEXT NOT NULLhuman-readable rule description
timeframeVARCHAR(20) NOT NULLthe timeframe this regime is defined on
statusVARCHAR(20) NOT NULL DEFAULT 'proposed'CHECK IN ('proposed','active','rejected')
created_atTIMESTAMPTZ NOT NULL DEFAULT now()

Running Locally

See Local CLI for the exact commands to migrate, seed, and test this against your local tedb.