Python CLI CSV-first Monthly screening Yahoo-backed datasets Matrix sweeps
Finance CLI is a CSV-first command-line tool for screening monthly ETF, stock, and similar market datasets with configurable indicators and rule-based signals.
It is built for four practical jobs:
| Job | What it gives you |
|---|---|
| Analyze a named dataset | Run repeatable analysis against CSVs discovered from data/generated/ |
| Analyze your own CSV | Run the same logic on any file path without importing it |
| Create and refresh live datasets | Pull monthly OHLCV history from Yahoo Finance and keep it refreshable |
| Sweep the full matrix | Run a fixed grid of indicators, rules, and month windows across all generated datasets |
Tip
First run? Open the guided wizard with python3 dynamic_range_average.py.
Important
This project is a screening and inspection tool. It is not a backtester, portfolio optimizer, or complete trading system.
- Start Here
- Docker and API
- Workflow Map
- Common Workflows
- Command Cookbook
- Files and Outputs
- Signal Logic
- Limits and Caveats
- Contributor Notes
Install the Python dependencies:
python3 -m pip install -r requirements.txtIf your system Python is externally managed and the command above fails:
python3 -m pip install --user --break-system-packages -r requirements.txtOpen the guided wizard:
python3 dynamic_range_average.pyOr go straight to the CLI:
python3 dynamic_range_average.py datasets list
python3 dynamic_range_average.py run --dataset spy --months 6
python3 dynamic_range_average.py matrixRun the FastAPI product locally:
uvicorn finance_cli.api_app:app --reloadCore endpoints:
GET /healthzGET /datasetsPOST /datasets/uploadPOST /datasets/create-from-symbolPOST /datasets/{dataset_id}/refreshDELETE /datasets/{dataset_id}POST /runsGET /runs/{run_id}GET /runs/{run_id}/artifactPOST /jobs/matrixGET /jobs/{job_id}GET /jobs/{job_id}/artifact
Default API state:
- SQLite state DB:
state/finance_api.db - Managed datasets:
data/generated/ - Analysis artifacts:
output/ - Refresh backups and uploads:
tmp/
Build and start the API:
docker compose up --buildThe API will be available at:
http://127.0.0.1:8000
Mounted persistent paths:
data/generated/-> managed datasetsoutput/-> run outputs and matrix artifactstmp/-> refresh backups and upload stagingstate/-> SQLite metadata database
Override ports or mount locations with environment variables:
FINANCE_API_PORT=18080 \
GENERATED_DATA_DIR="$PWD/data/generated" \
OUTPUT_DIR="$PWD/output" \
TMP_DIR="$PWD/tmp" \
STATE_DIR="$PWD/state" \
docker compose up --buildUse the shared image for one-off CLI execution:
docker compose run --rm finance-api python dynamic_range_average.py datasets list
docker compose run --rm finance-api python dynamic_range_average.py run --dataset spy --months 6Upload a CSV dataset:
curl -X POST http://127.0.0.1:8000/datasets/upload \
-F "file=@spy.csv"Run a synchronous dataset analysis:
curl -X POST http://127.0.0.1:8000/runs \
-H "Content-Type: application/json" \
-d '{"dataset_id":"spy","months":6}'Trigger an asynchronous matrix job:
curl -X POST http://127.0.0.1:8000/jobs/matrix| If you want to... | Run this | Result |
|---|---|---|
| Use the guided flow | python3 dynamic_range_average.py |
Interactive wizard for creating, picking, or running datasets |
| Analyze a generated dataset | python3 dynamic_range_average.py run --dataset spy --months 6 |
Writes output/spy_processed.csv |
| Analyze a custom CSV | python3 dynamic_range_average.py run --file path/to/custom.csv --months 12 |
Writes output/custom_processed.csv by default |
| Create a Yahoo-backed dataset | python3 dynamic_range_average.py datasets create --symbol SPY |
Writes data/generated/spy.csv |
| Refresh a generated dataset | python3 dynamic_range_average.py datasets refresh --id spy |
Updates the dataset and writes a backup |
| Sweep every generated dataset | python3 dynamic_range_average.py matrix |
Writes timestamped matrix output plus manifest.csv |
flowchart LR
A["Start"] --> B{"What do you have?"}
B -->|"A dataset id"| C["run --dataset [id] --months 6"]
B -->|"A CSV file"| D["run --file path/to/file.csv --months 12"]
B -->|"A Yahoo symbol"| E["datasets create --symbol SPY"]
E --> F["data/generated/spy.csv"]
F --> C
B -->|"Sweep everything"| G["matrix"]
C --> H["output/[stem]_processed.csv"]
D --> H
G --> I["output/matrix/[timestamp]/manifest.csv"]
| Best when | Core command | Default result |
|---|---|---|
| You want a repeatable dataset id and predictable output file | python3 dynamic_range_average.py run --dataset spy --months 6 |
output/spy_processed.csv |
List available datasets:
python3 dynamic_range_average.py datasets listRun one dataset:
python3 dynamic_range_average.py run --dataset 500_pa --months 6Use a different indicator and rule:
python3 dynamic_range_average.py run --dataset spy --months 6 --indicator ema --rule "indicator > close"Refresh first, then analyze:
python3 dynamic_range_average.py run --dataset aapl --months 6 --refresh| Best when | Core command | Notes |
|---|---|---|
| You want a one-off run on a file without importing it | python3 dynamic_range_average.py run --file path/to/custom.csv --months 12 |
The input file is not copied into data/generated/ |
Run a custom file:
python3 dynamic_range_average.py run --file path/to/custom.csv --months 12Write to a custom output path:
python3 dynamic_range_average.py run --file path/to/custom.csv --months 12 --output output/my_custom_screen.csvRequired columns:
dateopen
Extra columns are allowed and remain in the processed output.
| Best when | Core command | What happens |
|---|---|---|
| You want a named dataset that can later be refreshed with live data | python3 dynamic_range_average.py datasets create --symbol SPY |
Downloads monthly Yahoo Finance history into data/generated/ |
Create a dataset:
python3 dynamic_range_average.py datasets create --symbol SPYAnother example:
python3 dynamic_range_average.py datasets create --symbol 500.PAResulting ids:
SPY->data/generated/spy.csv500.PA->data/generated/500_pa.csv
The first symbol column stores the original Yahoo symbol, which is what later enables refresh.
| Best when | Core command | Notes |
|---|---|---|
| You already have a CSV and want it to behave like a named dataset | python3 dynamic_range_average.py datasets add --path path/to/custom.csv |
Uses the source filename as the dataset id |
Copy an existing CSV into generated storage:
python3 dynamic_range_average.py datasets add --path path/to/custom.csvImport a CSV and attach a refresh symbol:
python3 dynamic_range_average.py datasets add --path data/my_sp500.csv --refresh-symbol 500.PARemove a generated dataset:
python3 dynamic_range_average.py datasets remove --id spy| Best when | Core command | Notes |
|---|---|---|
| You want the latest available monthly data before screening | python3 dynamic_range_average.py datasets refresh --id spy |
Works only for datasets with a non-empty symbol column |
Refresh one dataset:
python3 dynamic_range_average.py datasets refresh --id 500_paRefresh every refreshable dataset:
python3 dynamic_range_average.py datasets refresh --allBackups are written to:
tmp/refresh_backups/<input_stem>.backup.<timestamp>.csv
| Best when | Core command | Output |
|---|---|---|
| You want a broad sweep instead of a single hand-picked run | python3 dynamic_range_average.py matrix |
Per-dataset CSVs plus manifest.csv under a timestamped directory |
Run with the default output directory:
python3 dynamic_range_average.py matrixRun with a custom output directory:
python3 dynamic_range_average.py matrix --output-dir output/matrix/latestThe matrix currently sweeps:
- month windows:
1,3,6,12,24 - indicators:
ema,sma,wma - rules:
indicatorcompared withopen,high,low, andcloseusing>,<,>=, and<=
Each run writes per-dataset CSVs plus a manifest summarizing status, output path, row count, and true-condition count.
Use this section when you want the shortest path from task to command.
| Task | Command | Notes |
|---|---|---|
| Open the wizard | python3 dynamic_range_average.py |
Best first-run entry point |
| Show CLI help | python3 dynamic_range_average.py --help |
Top-level commands: run, matrix, datasets |
| List generated datasets | python3 dynamic_range_average.py datasets list |
Reads data/generated/*.csv only |
| Analyze one dataset | python3 dynamic_range_average.py run --dataset spy --months 6 |
Default indicator is sma |
| Analyze one CSV file | python3 dynamic_range_average.py run --file path/to/custom.csv --months 12 |
Does not import the file |
| Change indicator and rule | python3 dynamic_range_average.py run --dataset spy --months 6 --indicator ema --rule "indicator > close" |
Rule format is left_operand operator right_operand |
| Refresh before running | python3 dynamic_range_average.py run --dataset nvda --months 6 --refresh |
Generated datasets only |
| Create a dataset from Yahoo | python3 dynamic_range_average.py datasets create --symbol AAPL |
Creates a refreshable generated dataset |
| Import a CSV as a dataset | python3 dynamic_range_average.py datasets add --path path/to/custom.csv |
Uses the source filename as dataset id |
| Import and attach refresh metadata | python3 dynamic_range_average.py datasets add --path data/my_sp500.csv --refresh-symbol 500.PA |
Enables live refresh |
| Remove a dataset | python3 dynamic_range_average.py datasets remove --id spy |
Deletes the generated CSV |
| Refresh one dataset | python3 dynamic_range_average.py datasets refresh --id spy |
Writes a backup first |
| Refresh all refreshable datasets | python3 dynamic_range_average.py datasets refresh --all |
Skips non-refreshable generated datasets |
| Run the full matrix | python3 dynamic_range_average.py matrix |
Writes outputs under output/matrix/<timestamp>/ |
Note
Generated datasets are discovered only from data/generated/*.csv. Files outside that directory are never auto-discovered, but they still work with run --file.
Dataset ids come directly from filenames:
data/generated/500_pa.csv->500_padata/generated/nvda.csv->nvdadata/generated/spy.csv->spy
| Location | Purpose |
|---|---|
data/generated/ |
Named generated datasets |
output/<input_stem>_processed.csv |
Default output for a single run command |
output/matrix/<timestamp>/ |
Default output root for matrix runs |
output/matrix/<timestamp>/manifest.csv |
Manifest for the matrix run |
tmp/refresh_backups/ |
Backups created before refresh writes |
Single-run examples:
data/generated/500_pa.csv->output/500_pa_processed.csvdata/generated/spy.csv->output/spy_processed.csv
You can override a single-run output path with --output, but it must still end in .csv.
Note
The output/ directory is treated as generated local output and is ignored by git for new files.
The terminal output shows all analyzed rows, not only rows where condition == 1.
Output columns are ordered dynamically:
- key fields first when present:
symbol, the two gap columns,date,open - then remaining source columns in their original CSV order
- then derived analysis columns such as the indicator column,
moving_average_window_months,condition, and optionalscreening_rule
Processed CSV files follow the same ordering as terminal output. That means they preserve:
- original source columns
- extra columns from custom CSV inputs
- derived analysis fields added by the app
| Column | Meaning |
|---|---|
Moving_Average |
Legacy default indicator column when you do not pass --indicator or --rule |
EMA_6_months, SMA_12_months, and similar |
Indicator-specific columns used for non-default runs |
moving_average_minus_open_over_open |
Primary gap ratio showing how far the selected indicator sits above open |
open_minus_moving_average_over_moving_average |
Secondary gap ratio showing how far open sits above the selected indicator |
moving_average_window_months |
Window size used for the run |
condition |
1 when the rule is true, otherwise 0 |
screening_rule |
Normalized rule string for non-default runs |
This project is centered on a simple monthly market-screening idea:
- Load price history.
- Calculate a selected indicator on monthly
open. - Compute two gap ratios between the indicator and
open. - Mark each row with a binary condition from a rule such as
indicator > open.
It is best understood as a lightweight trend-following or time-series-momentum style screen, not as a complete investment system.
The methodology is designed around monthly market data.
- symbol-created and live-refreshed generated datasets are monthly Yahoo Finance OHLCV CSVs with
symbol,date,open,high,low,close,volume - imported generated datasets and direct custom CSV files must contain at least
dateandopen - extra source columns are allowed and flow through to processed output
- the CLI does not enforce monthly frequency for custom files, but the methodology in this README assumes monthly datasets
Before analysis, the app:
- parses
date - converts
opento numeric - validates that required fields are present
- sorts rows in ascending date order
For a chosen --months window, the app computes:
- a selected indicator on monthly
openusing--indicatorwithema,sma, orwma condition = 1when the configured rule is true, else0moving_average_minus_open_over_open = (indicator - open) / openopen_minus_moving_average_over_moving_average = (open - indicator) / indicatormoving_average_window_monthsfor traceability
Default behavior stays backward-compatible:
- if you do not pass
--indicatoror--rule, the output keeps the legacyMoving_Averagecolumn - if you pass either option, the output uses indicator-specific column names such as
EMA_6_months - non-default runs also add a
screening_rulemetadata column
| Window | Use It For | Tradeoff | Example |
|---|---|---|---|
1-3 months |
faster regime detection and recent dislocation checks | more noise and more signal flips | python3 dynamic_range_average.py run --dataset nvda --months 3 |
6 months |
balanced screening for monthly workflows | still reacts slower than daily systems | python3 dynamic_range_average.py run --dataset spy --months 6 |
12-24 months |
slower trend context and broader regime direction | later reactions to change | python3 dynamic_range_average.py run --dataset 500_pa --months 12 |
For repeatable monthly screening on symbol-backed datasets, refresh first:
python3 dynamic_range_average.py run --dataset aapl --months 6 --refresh| Signal | Practical meaning |
|---|---|
condition = 1 |
The configured rule evaluated to true for that row |
Positive moving_average_minus_open_over_open |
The current open is below the selected indicator |
Positive open_minus_moving_average_over_moving_average |
The current open is above the selected indicator |
| Larger positive values in the primary gap column | The current open is further below the selected indicator |
In practice, the screen is most naturally used to find rows where price is trading below its recent average level and then inspect the size of that gap.
- moving averages smooth noisy price series
- comparing a current price level to its recent average is a common heuristic for trend or regime direction
- monthly data reduces some short-term noise, but it also reacts more slowly
This repo does not claim the rule is optimal. It exposes the signal clearly so you can inspect the data and build on it.
This project is intentionally narrow. It is not:
- a discounted cash-flow or fundamental valuation model
- a complete trading system with entries, exits, position sizing, slippage, taxes, fees, or risk controls
- a portfolio-construction engine
- a cross-sectional ranking model across many assets
- a benchmark-comparison or performance-attribution framework
- a statistical backtest engine with Sharpe ratio, drawdown, turnover, or transaction-cost analysis
Methodological caveats:
- the signal is driven only by the
openseries - monthly frequency can miss faster regime changes
- live refresh updates source data, but does not validate investment performance
- Yahoo Finance data quality and symbol coverage are external dependencies
Use the project as a transparent screening and inspection tool, not as proof of investability.
-
Brock, Lakonishok, and LeBaron (1992), Simple Technical Trading Rules and the Stochastic Properties of Stock Returns
JSTOR
Relevance: classic evidence paper on simple technical trading rules, including moving-average-style rules. -
Moskowitz, Ooi, and Pedersen (2012), Time Series Momentum
Yale-hosted PDF
Relevance: connects price-versus-history logic to time-series momentum intuition across assets. -
Han, Yang, Zhou, and Zhu (2019), Theoretical and practical motivations for the use of the moving average rule in the stock market
Oxford Academic PDF
Relevance: directly discusses why moving-average rules are used and how they can be interpreted. -
Lo, Mamaysky, and Wang (2000), Foundations of Technical Analysis
NBER
Relevance: broader background on systematic technical-analysis framing.
Useful paths:
- entrypoint:
dynamic_range_average.py - CLI implementation:
finance_cli/ - generated datasets:
data/generated/ - processed outputs and matrix manifests:
output/ - refresh backups:
tmp/refresh_backups/
Run tests:
python3 -m pytestHelpful help commands:
python3 dynamic_range_average.py --help
python3 dynamic_range_average.py run --help
python3 dynamic_range_average.py matrix --help
python3 dynamic_range_average.py datasets --help