Skip to content

Repository files navigation

TradingStrategy

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.

What it does

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

Quick start

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.py

Web interface

Run 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 dev

Open 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.

Mobile Scan (remote access)

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.sh

Or manually:

cd frontend && npm run build && cd ..
python -m uvicorn api.main:app --host 0.0.0.0 --port 8000

Open 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.

  1. Start the unified server (above). It binds to 0.0.0.0:8000.
  2. 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
  3. 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
  4. Optional but recommended: give the Mac a DHCP reservation so its LAN IP doesn’t change.
  5. 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.).
  6. On your phone (any network): http://<public-ip>:8000/scan
    If you used a non‑8000 external port: http://<public-ip>:<external-port>/scan
  7. macOS firewall: if enabled, allow incoming connections for Python when prompted, or add a rule in System Settings → Network → Firewall.
  8. 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:8000

Bookmark the generated https://….trycloudflare.com/scan URL. The link is public while the tunnel runs — treat it like an open port.

Project layout

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)

Data flow

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

Configuration

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.

Backtesting (unused/run_backtest.py)

Edit the config block, set RUN_MODE, and run:

python unused/run_backtest.py

python unused/IBConnect.py also works (legacy alias).

Run modes

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

Example configs

Single signal, one symbol:

RUN_MODE = 'single'
SYMBOL = 'SOXX'
SIGNAL = ind.buy_signal7
YEARS = 25

Combined 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)

Aggregate metrics & outlier exclusion

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.

Output

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).

Web interface

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.

Backend API

Start it with:

python -m uvicorn api.main:app --reload

Available 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.

Frontend

The React app lives in frontend/.

cd frontend
npm install
npm run dev

The 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

Signal combination (same symbol)

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 fires

Low-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).

Cross-symbol confirmation

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.

Other scripts

Daily signal check (Streamlit)

streamlit run signal_check.py

Scans ~18 symbols against 24 buy signals. Shows buy/hold/sell state plus SPY market summary.

Latest quotes

python quote.py

Prints today's close, RSI, EMA, Stochastic, breadth, and volume for config.ticker.

Compare all signals on one symbol

python unused/test_data.py

Edit yfticker in the file to change the symbol.

One signal across many symbols

python unused/test_indicator.py

Change buy_signal = ind.buy_signal11 at the top.

Running tests

python -m unittest discover -s tests -v

Signals

Each 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_signal124 Various Mixed See indicators.py for full list

Many signals have empty allowed_symbols, so they are skipped in signal_check.py unless you add your ticker to that list.

Strategy engine

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.

Sweep functions

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

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).

Data sources

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.

Tips

  • Start with unused/run_backtest.py — all backtest modes are configured in one place.
  • Sweep then detail — use symbol_confirm_sweep or signal_combo_sweep first, then drill in with single or symbol_confirm_detail.
  • SIGNAL vs SIGNAL_A/SIGNAL_Bsingle and confirm modes use SIGNAL; combo sweep uses SIGNAL_A and SIGNAL_B.
  • Leverage is applied in long_strat and %Change — adjust Leverage in config.py.
  • CSV output goes to CSV/ (gitignored).

Disclaimer

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages