A fully automated virtual swing-trading system for Canadian (TSX) stocks. Screens the market daily, detects technical entry patterns, executes virtual buys and sells, and emails a report after every trade.
Data source: Yahoo Finance via
yfinance. All transactions are virtual — no real brokerage connection.
Three scheduled services cooperate across the trading day:
| Time (ET) | Service | What it does |
|---|---|---|
| 4:30 PM | main.py |
Checks market regime → rebuilds universe → runs screener → detects patterns → queues buy candidates |
| 9:30 AM | virtual_buy.py |
Reads the candidate queue, fetches live prices, sizes and executes virtual buys → sends trade email |
| 3:30 PM | position_monitor.py |
Evaluates open positions against exit rules using intraday data → executes virtual sells → sends trade email |
All state — cash, positions, trades, signals, intents — lives in a single
DuckDB database at data/trading.db.
Requirements: Python 3.10+, internet access (Yahoo Finance).
git clone <repo>
cd StockScanner
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtRun once before anything else. Replace 50000 with your paper-trading capital:
python -c "from db import init_db, set_cash; init_db(); set_cash(50_000)"Verify the database was created and the balance is correct:
python -c "
from db import init_db, get_cash, get_open_positions
init_db()
print('Cash :', get_cash())
print('Positions:', get_open_positions())
"The system sends a trade notification email after every buy or sell. Gmail requires an App Password (not your regular password).
- Enable 2-Step Verification at https://myaccount.google.com/security
- Create an App Password → copy the 16-character code
- Create a
.envfile in the repo root:
GMAIL_SENDER=you@gmail.com
GMAIL_APP_PASSWORD=xxxx xxxx xxxx xxxx
GMAIL_RECIPIENT=you@gmail.com
If .env is absent the system runs normally — it just skips emails.
Run this on the weekend before you want to start:
python main.pyThis populates data/trading.db with the first batch of signals and queues
any CONFIRMED setups as pending buy intents. virtual_buy.py will consume
them Monday morning.
Once the system is running, the three services execute automatically on their schedule. To run them manually:
# End-of-day (after 4:30 PM ET)
python main.py
# Next morning (at/after 9:30 AM ET open)
python virtual_buy.py
# Pre-close (around 3:30 PM ET, market still open)
python position_monitor.py --mode pre-close
# Optional post-close informational run
python position_monitor.py --mode post-closeBoth virtual_buy.py and position_monitor.py support --dry-run to
preview activity without touching the database:
python virtual_buy.py --dry-run
python position_monitor.py --mode pre-close --dry-run # not yet wired — informational onlyThe system/ directory contains .service and .timer unit files for
running the three services automatically on a Linux host.
# Copy unit files to systemd
sudo cp system/*.service system/*.timer /etc/systemd/system/
# Reload systemd so it sees the new units
sudo systemctl daemon-reload
# Enable timers so they survive reboots
sudo systemctl enable stockscanner-main.timer
sudo systemctl enable stockscanner-buy.timer
sudo systemctl enable stockscanner-monitor.timer
# Start the timers now
sudo systemctl start stockscanner-main.timer
sudo systemctl start stockscanner-buy.timer
sudo systemctl start stockscanner-monitor.timer# Copy updated files
sudo cp system/stockscanner-main.service /etc/systemd/system/
# (repeat for whichever files changed)
# Tell systemd to reload its configuration
sudo systemctl daemon-reload
# Restart the affected service if it is currently running
sudo systemctl restart stockscanner-main.service# Check whether timers are active and when they next fire
systemctl list-timers stockscanner-*
# View recent output for a service
journalctl -u stockscanner-main.service -n 50
# Check the current status of a service
systemctl status stockscanner-main.service
# Run a service immediately (outside its schedule)
sudo systemctl start stockscanner-main.serviceThe main service is started with --tickers-url pointing to a remote file
(one ticker per line). To change the URL, edit
system/stockscanner-main.service and re-run the install steps above.
The default URL is also set in config.py (CAN_TICKERS_URL) and is used
when running services manually from the command line without --tickers-url.
Use DuckDB directly for ad-hoc queries:
import duckdb
conn = duckdb.connect("data/trading.db")
conn.execute("SELECT * FROM transactions ORDER BY trade_date").df() # full ledger
conn.execute("SELECT * FROM positions").df() # open positions
conn.execute("SELECT * FROM trades ORDER BY sell_date").df() # closed trades
conn.execute("SELECT * FROM account").df() # cash balance
conn.execute("SELECT * FROM intents WHERE intent_status = 'PENDING'").df()
conn.close()| Table | Contents |
|---|---|
account |
Current cash balance |
positions |
Open virtual positions (ticker, entry date, price, shares) |
trades |
Permanent closed-trade log with full P&L |
transactions |
Unified ledger — every BUY and SELL in chronological order |
signals |
Pipeline signal state machine (rebuilt each run) |
intents |
Buy candidate queue with execution history |
canadian_stock_screener.py scores each ticker in the TSX universe with a
weighted factor stack:
- Weinstein Stage II alignment
- Relative Strength vs XIU.TO
- MACD momentum
- OBV slope (volume accumulation)
- ADX trend strength
- Volatility-adjusted momentum (VAM)
- 52-week high proximity / breakout pressure
Universe: loaded from CAN_TICKERS_URL in config.py (one ticker per line).
Output: out/screener_out/YYYYMMDD_HHMM.csv
Configure via CONFIG dict inside canadian_stock_screener.py:
top_n, min_price, min_avg_volume, weights, lookback_days.
auto_pipeline.py reads screener CSVs, tracks tickers across days, and runs
three pattern detectors:
- VCP — Volatility Contraction Pattern
- EMA pullback reclaim — EMA21 and EMA50 variants
- Base breakout — tight range + volume confirmation
FORMING → AT_PIVOT → CONFIRMED → ACTIVE
↘ ↘ FAILED / EXPIRED
CONFIRMED setups are written to the intents table as PENDING buy
candidates. virtual_buy.py processes them the following morning.
Before generating new signals, main.py checks whether XIU.TO is above
its 200-day SMA. In a bear regime, signal generation is skipped — no new
positions are opened. Existing positions continue to be monitored.
Each position is evaluated against four exit rules (defaults):
| Rule | Trigger |
|---|---|
| Initial stop | Entry − 1.5 × ATR(14) |
| Chandelier trail | Highest high since entry − 2.5 × ATR(14) |
| Profit giveback | Max profit ≥ 6% and current profit drops ≥ 3% below peak |
| Time stop | ≥ 20 trading days held with profit < 0% |
Run a historical backtest over any date range:
# Single run
python run_backtest.py --start 2022-01-01 --end 2024-01-01
# Custom tickers (URL or file)
python run_backtest.py --tickers https://example.com/tickers.txt --start 2022-01-01 --end 2024-01-01
# Exit-param sweep: time_stop_days × stop_atr (4×4=16 combinations)
python run_backtest.py --start 2022-01-01 --end 2024-01-01 --sweep
# Walk-forward gap filter optimization (find best GAP_FILTER_PCT via sliding windows)
python run_backtest.py --start 2022-01-01 --end 2025-01-01 --walk-forward-gap
python run_backtest.py --start 2022-01-01 --end 2025-01-01 --walk-forward-gap --wf-in-days 84 --wf-out-days 21
python run_backtest.py --helpThe backtest simulates the live gap filter when gap_filter_pct is set in BacktestConfig
(default None = no filter, matching historical behaviour before May 2026).
Output files are written to out/:
backtest_DATES_TIMESTAMP.html— HTML report with equity curve and trade logbacktest_trades_TIMESTAMP.csv— full trade logbacktest_equity_TIMESTAMP.csv— day-by-day equity curvebacktest_wf_gap_DATES_TIMESTAMP.csv— walk-forward gap optimization results
.
├── main.py # end-of-day service (4:30 PM)
├── virtual_buy.py # morning buy execution (9:30 AM)
├── position_monitor.py # pre/post-close monitor (3:30 PM)
├── auto_pipeline.py # pattern detection + signal state machine
├── canadian_stock_screener.py
├── run_backtest.py # backtest CLI
├── db.py # DuckDB persistence layer
├── send_report.py # Gmail email sender
├── config.py # path constants + trading parameters
├── data/
│ └── trading.db # all live state (auto-created on first run)
└── out/
├── screener_out/ # daily screener CSVs
├── alerts/ # daily alert CSVs + HTML report
├── logs/ # service run logs
└── locks/ # process lock files
pytest tests/ -v
# By phase gate (backtest refactor)
pytest -v -m phase1 # clock injection
pytest -v -m phase2 # MarketDataProvider
pytest -v -m phase3 # PortfolioState
pytest -v -m phase4 # BacktestRunner
pytest -v -m phase5 # HTML report
pytest -v -m phase6 # CLI entry point
# Specific files
pytest tests/test_db.py -v # database layer
pytest tests/test_integration.py -v # service integrationyfinancedepends on Yahoo Finance endpoints — intermittent rate limits or missing data can occur.- The screener and benchmark (
XIU.TO) are designed for TSX tickers. - The pipeline caps tracked tickers (default 40) to limit API calls.
- All transactions are virtual — this system does not connect to any brokerage.
For research and education only. Does not constitute financial advice. Trading involves risk of loss.
MIT