A Python-based trading signal and backtesting system. This repo does not place trades — it fetches end-of-day market data, computes technical indicators, evaluates buy/hold/sell signals, and backtests strategies against historical data.
| Capability | Description |
|---|---|
| Daily signal scan | Pulls EOD prices from Yahoo Finance and reports which strategies are firing buy, hold, or sell across a watchlist of symbols |
| Backtesting | Simulates strategies over years of history with PnL, drawdown, Sharpe/Sortino, Kelly, and CAGR |
| Latest quotes | Prints current price and key indicators for the configured ticker |
| Signal exploration | Combine signals (AND/OR), sweep indicator filters, and require cross-symbol confirmation before entry |
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
pip install streamlit # for signal_check.py only
# Edit the config block in unused/run_backtest.py, then:
python unused/run_backtest.pyRun the local API and React frontend in two terminals:
# Terminal 1: backend
source venv/bin/activate
python -m uvicorn api.main:app --reload# Terminal 2: frontend
cd frontend
npm install
npm run devOpen the Vite URL (usually http://localhost:5173). The frontend talks to the backend at http://localhost:8000 via frontend/.env.development. Override with VITE_API_URL if needed.
Access Scan from your phone while the Mac stays running at home.
1. Start the unified server (builds frontend + serves API + UI on one port):
source venv/bin/activate
./scripts/serve-mobile.shOr manually:
cd frontend && npm run build && cd ..
python -m uvicorn api.main:app --host 0.0.0.0 --port 8000Open http://localhost:8000/scan on desktop, or use remote access below.
2. Remote access with port forwarding
No third-party accounts. Your router forwards traffic from the internet to the Mac.
- Start the unified server (above). It binds to
0.0.0.0:8000. - Find your Mac’s LAN IP: System Settings → Network (e.g.
192.168.1.42), or:ipconfig getifaddr en0 # Wi‑Fi; use en1 etc. if needed - In your router admin UI, add a port forwarding rule:
- External port:
8000(or another port if your ISP blocks 8000) - Internal IP: your Mac’s LAN IP
- Internal port:
8000 - Protocol: TCP
- External port:
- Optional but recommended: give the Mac a DHCP reservation so its LAN IP doesn’t change.
- Find your public IP (whatismyip.com, or
curl -s ifconfig.me). If it changes often, set up dynamic DNS on your router (No-IP, DuckDNS, etc.). - On your phone (any network):
http://<public-ip>:8000/scan
If you used a non‑8000 external port:http://<public-ip>:<external-port>/scan - macOS firewall: if enabled, allow incoming connections for Python when prompted, or add a rule in System Settings → Network → Firewall.
- Add to Home Screen (Safari Share → Add to Home Screen) for an app-like experience.
Security: The app has no login. Anyone who can reach that URL can see your scan data. Use a non-obvious external port, restrict by IP on the router if supported, or add auth before exposing broadly.
Same Wi‑Fi only (no port forwarding): skip steps 3–5 and use http://<mac-lan-ip>:8000/scan on your phone while at home.
Keep the Mac awake while away: System Settings → Battery/Energy → disable sleep on power adapter, or run caffeinate -dims in a terminal.
Alternative: Tailscale (private, no port forwarding)
Install Tailscale on Mac and phone, sign in, then open http://<mac-tailscale-ip>:8000/scan. No public exposure; requires a free account.
Alternative: Cloudflare quick tunnel (HTTPS, no router config)
brew install cloudflared
cloudflared tunnel --url http://localhost:8000Bookmark the generated https://….trycloudflare.com/scan URL. The link is public while the tunnel runs — treat it like an open port.
TradingStrategy/
├── api/ ← FastAPI backend for the web UI
├── frontend/ ← React/Vite frontend
├── tests/ ← unittest suite
├── unused/ ← standalone / legacy scripts (not used by web stack)
├── config.py ← strategy parameters (ticker, RSI, leverage, etc.)
├── getdata.py ← Yahoo Finance fetch, breadth, holiday filtering
├── indicators.py ← indicators + 24+ buy signal definitions
├── backtest.py ← strategy engine (og_strat, long_strat) + sweeps
├── stats.py ← aggregate metrics, yearly breakdown, outlier exclusion
├── backtest_runners.py ← load data, single-symbol & cross-symbol runners
├── indicator_sweep.py ← grid-search indicator filters on a signal
├── signal_check.py ← Streamlit daily signal dashboard
├── quote.py ← quick indicator snapshot
├── warn_config.py ← suppresses third-party FutureWarnings
├── requirements.txt
└── CSV/ ← backtest output (gitignored)
Yahoo Finance (yfinance)
↓
getdata.py — OHLCV + VIX + sector breadth ratios (RSP/SPY, QQQ/SPY, …)
↓
indicators.add_indicators() — RSI, EMA, IBR, ValueCharts, VFI, …
↓
buy_signalN() / combined_signal() — Buy/Sell booleans + hold rules
↓
backtest.execute_strategy() — entries, exits, RollingPnL
↓
stats.compute_aggregate_metrics() — Sharpe, CAGR, etc. (optional outlier exclusion)
↓
Output — summary table, print_stats, or CSV
Strategy parameters live in config.py:
| Parameter | Default | Purpose |
|---|---|---|
ticker |
SOXX |
Default symbol for quote / backtest scripts |
RSI2Buy / RSI5Buy |
15 / 35 | OG strategy oversold thresholds |
RSI2Sell / RSI5Sell |
95 / 70 | OG strategy overbought thresholds |
stop_loss |
0.15 | Exit when trade PnL drops below −15% |
Leverage |
3 | Multiplier applied to returns |
VolumeEMAThreashold |
0.6 | Volume vs 8-day EMA filter |
VolatilityThreashold |
0.1 | Annualized volatility exit filter |
MondayBuy / LowVolumeBuy |
True | One-day buy rules in OG strategy |
UseProxyUnderlying |
False | Track PnL via leveraged proxy (e.g. SOXX) |
ExcludeBestReturnYear |
True | Drop best positive year from aggregate metrics |
api_key |
"" |
Pushbullet key (commented out in signal_check.py) |
Backtest run settings live in the config block at the top of unused/run_backtest.py.
Edit the config block, set RUN_MODE, and run:
python unused/run_backtest.pypython unused/IBConnect.py also works (legacy alias).
RUN_MODE |
What it does |
|---|---|
single |
One signal, one symbol — full stats + yearly breakdown + CSV |
indicator_sweep |
Grid-search indicator filters layered on a signal (ranked by Sharpe) |
signal_combo_sweep |
Compare 4 AND/OR combos of two signals on the same symbol |
symbol_confirm_sweep |
Sweep all cross-symbol confirmation subsets from a pool |
symbol_confirm_detail |
One primary + chosen confirm symbols — full yearly breakdown |
hold_days_sweep |
Search days-in-trade × profitable closes |
Single signal, one symbol:
RUN_MODE = 'single'
SYMBOL = 'SOXX'
SIGNAL = ind.buy_signal7
YEARS = 25Combined signal (AND/OR on same symbol):
RUN_MODE = 'single'
SIGNAL = ind.combined_signal(ind.buy_signal16, ind.buy_signal7, 'or')Compare all 4 AND/OR combinations:
RUN_MODE = 'signal_combo_sweep'
SIGNAL_A = ind.buy_signal16
SIGNAL_B = ind.buy_signal7
SYMBOL = 'SOXX'Cross-symbol confirmation sweep (primary traded at leverage; all confirm symbols must also show buy):
RUN_MODE = 'symbol_confirm_sweep'
SIGNAL = ind.combined_signal(ind.buy_signal16, ind.buy_signal7, 'or')
PRIMARY_SYMBOL = 'SOXX'
SYMBOL_POOL = ['SOXX', 'SMH', 'QQQ', 'SPY']Auto-generates rows for (none), SMH, QQQ, SPY, SMH+QQQ, … sorted by Sharpe.
Drill into one confirm set (after picking from sweep):
RUN_MODE = 'symbol_confirm_detail'
PRIMARY_SYMBOL = 'SOXX'
CONFIRM_SYMBOLS = ['SMH', 'QQQ']Indicator filter sweep:
RUN_MODE = 'indicator_sweep'
SIGNAL = ind.buy_signal7
INDICATOR_SWEEP = dict(is_sell=False, check_breadth=False, check_both=False)When ExcludeBestReturnYear = True in config.py, aggregate stats (Sharpe, Sortino, CAGR, MaxDD, trade count, win rate, Kelly) exclude the single calendar year with the highest positive return. This keeps one extreme upside year from dominating comparisons.
- Latest Rolling PnL always reflects the full backtest (real total)
- Yearly breakdown always shows every year
- A note is printed when a year is excluded, e.g.
(Aggregate metrics exclude 2020 — best year at 906.88%)
Set ExcludeBestReturnYear = False to restore the original behavior.
Summary table (sweeps): Sharpe-sorted rows with PnL, MaxDD, Trades, %Pstv, CAGR.
Detailed stats (single, symbol_confirm_detail):
Number of trades: 318
Latest Rolling PnL: $1,539,583.00
Maximum drawdown: 30.81%
CAGR: 32.54%
Sharpe ratio: 0.45
(Aggregate metrics exclude 2020 — best year at 906.88%)
PnL% Drawdown% Num_Trades Positive_Trades
Date
2019 216.07% 16.81% 61 47
2020 906.88% 32.89% 64 50 ← still shown here
...
CSV files are saved to CSV/ (create the folder if needed).
The web app is local-first and stateless: each run is configured in the browser, sent to FastAPI, and returned directly as JSON. No database or auth is used in v1.
Start it with:
python -m uvicorn api.main:app --reloadAvailable endpoints:
| Endpoint | Purpose |
|---|---|
GET /health |
API health check |
GET /signals |
Signal names for dropdowns |
GET /config |
Current defaults from config.py |
POST /backtests/single |
One signal or combined signal on one symbol |
POST /backtests/signal-combo-sweep |
Four AND/OR combos between two signals |
POST /backtests/symbol-confirm-sweep |
Confirmation subset sweep from a symbol pool |
POST /backtests/symbol-confirm-detail |
One primary + chosen confirmations with yearly breakdown |
POST /backtests/hold-days-sweep |
Hold-days/profitable-close grid |
POST /backtests/indicator-sweep |
Indicator threshold sweep |
Signal expressions sent to the API look like:
{ "kind": "single", "name": "buy_signal7" }or:
{ "kind": "combined", "primary": "buy_signal16", "secondary": "buy_signal7", "mode": "or" }Detailed backtests return summary, yearly, equity_curve, and trades. Sweep endpoints return Sharpe-sorted rows with numeric values so the frontend can format them.
The React app lives in frontend/.
cd frontend
npm install
npm run devThe UI includes:
- Backtest Builder with mode-specific fields
- Summary cards for PnL, CAGR, Sharpe, Sortino, MaxDD, trades, and excluded year
- Equity curve chart
- Yearly breakdown table
- Sweep results table
- Trade list for detailed runs
Two signals on the same symbol can be merged with AND or OR. Days/profit/sell come from the primary signal.
# In indicators.py:
SIGNAL = ind.combined_signal(ind.buy_signal16, ind.buy_signal7, 'and') # both must fire
SIGNAL = ind.combined_signal(ind.buy_signal16, ind.buy_signal7, 'or') # either firesLow-level API:
buy, sell, days, profit, desc, _, is_long, _ = ind.combine_buy_signals(
ind.buy_signal16, ind.buy_signal7, data, mode='and'
)signal_combo_sweep runs all four variants (A primary AND B, A primary OR B, B primary AND A, B primary OR A).
Trade the primary symbol at leverage. Primary buy only fires when every confirm symbol is active that day: either its entry signal is true, or it is still in a simulated hold from a prior entry and has no sell signal. Sell is evaluated on the primary only.
# Sweep — compare confirm combinations
symbol_confirmation_tryout(SIGNAL, 'SOXX', ['SOXX', 'SMH', 'QQQ'], years=25)
# Detail — one chosen combo with yearly breakdown
symbol_confirmation_detail(SIGNAL, 'SOXX', confirm_symbols=['SMH', 'QQQ'], years=25)Typical workflow: symbol_confirm_sweep → pick winner → symbol_confirm_detail.
streamlit run signal_check.pyScans ~18 symbols against 24 buy signals. Shows buy/hold/sell state plus SPY market summary.
python quote.pyPrints today's close, RSI, EMA, Stochastic, breadth, and volume for config.ticker.
python unused/test_data.pyEdit yfticker in the file to change the symbol.
python unused/test_indicator.pyChange buy_signal = ind.buy_signal11 at the top.
python -m unittest discover -s tests -vEach signal in indicators.py returns an 8-tuple:
buy, sell, days, profit, description, verdict, is_long, ignore = buy_signalN(data, symbol)| Field | Meaning |
|---|---|
buy |
Entry condition per bar |
sell |
Exit condition (False if none) |
days |
Max hold days (0 = OG hold-until-sell) |
profit |
Exit after N profitable closes |
is_long |
Long vs short |
ignore |
Skip in signal scan if symbol not in allowed_symbols |
| Signal | Allowed symbols | Style | Summary |
|---|---|---|---|
buy_signal7 |
SMH, QQQ, FXI, SOXX, SPY | 2d/1p | Close pullback + IBR ≤ 0.4 |
buy_signal10 |
SMH, SPY, SOXX, QQQ | 3d/1p | New low + IBR |
buy_signal16 |
SMH, QQQ, SOXX | 4d/1p | High > prior close + IBR |
og_buy_signal |
SPY | OG | RSI2/RSI5 oversold + volume filter |
og_new_buy_signal |
SPY, IWM, QQQ | OG | OG buy + Stoch + SMA trend |
buy_signal1–24 |
Various | Mixed | See indicators.py for full list |
Many signals have empty
allowed_symbols, so they are skipped insignal_check.pyunless you add your ticker to that list.
backtest.execute_strategy() routes to:
| Engine | When | Behavior |
|---|---|---|
long_strat |
days > 0 |
Enter on Buy, exit on Sell / max days / N profitable closes |
og_strat |
days == 0 |
Hold until sell signal, stop loss, or one-day buy |
long_og_strat_proxy |
UseProxyUnderlying=True |
OG logic, PnL via leveraged proxy |
Key output columns: LongTradeIn, LongTradeOut, HoldLong, TradePnL, RollingPnL, Drawdown.
| Function | Purpose |
|---|---|
backtest_days(data, max_days) |
Grid search hold days × profitable closes |
backtest_ind(…) |
Filter buys by indicator threshold |
backtest_sell_ind(…) |
Filter sells by indicator threshold |
backtest_signal_combinations(a, b, data) |
4 AND/OR combos of two signals |
backtest_symbol_confirmation_sweep(…) |
All confirm subsets from a symbol pool |
indicators.add_indicators() adds:
- Trend: SMA, EMA, Bollinger Bands, Donchian (20/55), Keltner (TTM standard), Parabolic SAR, ATR
- Momentum: RSI (2/5/14), Stochastic, CCI, MACD, ADX(14), Williams %R, ROC(20), TRIX, linear regression slope
- Volatility: Bollinger width/%B, realized volatility, volatility percentile, BB/Keltner squeeze flag
- Volume / flow: OBV, OBV slope, Chaikin Money Flow, VFI, volume vs EMA
- Custom: IBR, Kaufman ER, ValueCharts, Hurst, Change Velocity
- Breadth: sector/index ratios vs SPY with RSI overlays
- Context: VIX, SPY 50/200 bull flag, Donchian/Keltner/PSAR breakout flags
Session VWAP is not available (daily EOD data only).
Yahoo Finance via yfinance. getdata.py provides:
- Single-ticker fetch (
get_data_yf) and bulk download (get_bulk_data) - Shared market context (VIX, breadth ratios) via
load_symbol_dataset - Futures/FX suffix mapping (
NQ=F,GBPUSD=X) - NYSE holiday filtering
Legacy Interactive Brokers code is commented out and unused.
- Start with
unused/run_backtest.py— all backtest modes are configured in one place. - Sweep then detail — use
symbol_confirm_sweeporsignal_combo_sweepfirst, then drill in withsingleorsymbol_confirm_detail. SIGNALvsSIGNAL_A/SIGNAL_B—singleand confirm modes useSIGNAL; combo sweep usesSIGNAL_AandSIGNAL_B.- Leverage is applied in
long_stratand%Change— adjustLeverageinconfig.py. - CSV output goes to
CSV/(gitignored).
This software is for research and education only. It generates signals and simulates historical performance — it does not execute live trades. Past backtest performance does not guarantee future results. Use at your own risk.