Calculate stop-loss prices for stock positions with a beautiful CLI interface.
- Simple Stop-Loss: Calculate stop-loss as a percentage below current price
- Trailing Stop-Loss: Track high-water marks and adjust stop-loss dynamically
- ATR-Based Stop-Loss: Volatility-adaptive stop-loss using Average True Range
- 52-Week High Mode: Base calculations on 52-week high for more conservative stops
- Historical Data: SQLite database stores price history and 52-week metrics
- Beautiful CLI: Rich table output with color-coded results using
typerandrich - Multi-Currency Support: Automatically handles USD, CAD, and other currencies
- TOML Configuration: Easy configuration for default tickers and settings
- Real-time Prices: Fetch current stock prices via
yfinance - Type-Safe: Full type hints throughout the codebase
- Well-Tested: Comprehensive test suite with
pytest
This project uses uv for package management and requires Python 3.14+.
# Install dependencies
uv sync
# Install with dev dependencies
uv sync --all-extrasCreate or edit config.toml in the project root:
# List of ticker symbols to track
# Works with US and Canadian stocks (and others supported by yfinance)
tickers = [
"AAPL", # Apple Inc. (USD)
"GOOGL", # Alphabet Inc. (USD)
"SHOP.TO", # Shopify (CAD - Toronto Stock Exchange)
"RY.TO", # Royal Bank of Canada (CAD)
]
# Default stop-loss percentage (0-100)
stop_loss_percentage = 5.0
# Enable trailing stop-loss by default
trailing_enabled = falseNote: Canadian stocks on the Toronto Stock Exchange use the .TO suffix (e.g., SHOP.TO, TD.TO).
Calculate stop-loss for configured tickers:
uv run stop-loss calculateOverride config and specify tickers directly:
uv run stop-loss calculate AAPL GOOGL SHOP.TOUse a different stop-loss percentage:
uv run stop-loss calculate --percentage 7.5
uv run stop-loss calculate -p 10Specify your entry price to set a floor for trailing stop-loss calculations based on your actual cost basis:
# CLI with entry prices (format: TICKER:PRICE) - requires --trailing
uv run stop-loss calculate AAPL:150 GOOGL:2800 SHOP.TO:200 --trailing -p 5
# Mixed format (some with entry prices, some without)
uv run stop-loss calculate AAPL:150 NVDA --trailing # NVDA uses database high water mark
# Entry prices work only with trailing mode
uv run stop-loss calculate AAPL:175.50 --trailing -p 5How it works in trailing mode:
- Entry price sets the minimum high water mark (
max(db_hwm, entry_price)) - Ensures your stop never goes below your entry point
- As price rises above entry, stop-loss trails normally
- Mixed usage: Tickers without entry prices use database high water mark only
Why trailing mode only? Entry prices represent your cost basis. Trailing mode naturally protects gains from that basis point forward, making the math intuitive. Simple and ATR modes calculate from current price, where entry price creates confusing "risk" calculations.
Example:
$ uv run stop-loss calculate AAPL:180 --trailing -p 5
# You bought at $180, stock went to $200, now at $195
# High water mark = max($200 from DB, $180 entry) = $200
# Stop-loss = $200 * 0.95 = $190
# Risk = $195 - $190 = $5 per shareEntry Price in Config:
tickers = [
"AAPL:150.50", # Entry price $150.50
"SHOP.TO:200", # Entry price $200 CAD
"NVDA", # No entry - uses current price
]Enable trailing stop-loss (tracks high-water mark):
uv run stop-loss calculate --trailing
uv run stop-loss calculate -tUse ATR (Average True Range) for volatility-adaptive stop-loss:
uv run stop-loss calculate --atr
uv run stop-loss calculate -aCustomize ATR parameters:
# Use tighter stop (1.5× ATR)
uv run stop-loss calculate --atr --atr-multiplier 1.5
uv run stop-loss calculate -a -m 1.5 # short form
# Use looser stop (3× ATR)
uv run stop-loss calculate --atr --atr-multiplier 3.0
uv run stop-loss calculate -a -m 3.0 # short form
# Change ATR period (default 14 days)
uv run stop-loss calculate --atr --atr-period 20
uv run stop-loss calculate -a -P 20 # short formUse simple stop-loss explicitly:
uv run stop-loss calculate --simple
uv run stop-loss calculate -suv run stop-loss calculate SHOP.TO NVDA -p 8 --trailinguv run stop-loss calculate --config /path/to/config.toml
uv run stop-loss calculate -c custom-config.tomlBase stop-loss calculations on the 52-week high instead of current price (more conservative):
# Simple mode with 52-week high (8% below peak)
uv run stop-loss calculate --week52-high --simple -p 8
uv run stop-loss calculate -w --simple -p 8 # short flag
# ATR mode with 52-week high
uv run stop-loss calculate --week52-high --atr
uv run stop-loss calculate -w --atr # short flag
# Works with multiple tickers
uv run stop-loss calculate AAPL MSFT NVDA -w --simple -p 10When to use: If you bought near the 52-week high and want to protect gains from the peak price rather than current price.
Example: AAPL at $259.04 with 52-week high of $288.62:
- Normal mode: 8% stop = $238.32 (risk $20.72/share)
- 52-week mode: 8% stop = $265.53 (risk -$6.49/share, above current price)
Calculate from a specific date (useful if you bought at a known date):
uv run stop-loss calculate --trailing --since 2024-01-15
uv run stop-loss calculate -t -d 2024-01-15 # short formSkip historical data fetching (use only in-memory tracking):
uv run stop-loss calculate --trailing --no-history
uv run stop-loss calculate -t -H # short formThe Guidance column provides suggestions based on the 50-day Simple Moving Average (SMA):
Possible Values:
- "
⚠️ Above current" (red) - Stop-loss is above current price (would trigger immediately) - "Raise stop" (yellow) - Your stop-loss is below the 50-day SMA
- "Keep current" (green) - Your stop-loss is at or above the 50-day SMA
- "N/A" - No 50-day SMA available (insufficient historical data)
How it works: The guidance checks your stop-loss price in this order:
- If
stop-loss > current price: Shows "⚠️ Above current" (warning - can't set stop above current at most brokers) - If
stop-loss < SMA: Suggests "Raise stop" (price has support from SMA, you could tighten) - If
stop-loss >= SMA: Suggests "Keep current" (stop is appropriately positioned)
When it makes sense:
- ✅ Simple mode: If price is trending above SMA, you might tighten your stop
- ✅ Trailing mode: Same logic - tighten if price holds above SMA
- ✅ ATR mode: If price respects SMA support, consider tighter stop
Limitations:
⚠️ Assumes SMA acts as support (technical analysis assumption, not always true)⚠️ Only available when 50+ days of historical data exists⚠️ Should be used as a suggestion, not a rule - always consider your own risk tolerance
Special case - 52-week high mode:
When using --week52-high, the stop-loss may be above the current price. The guidance will show "
Example:
Current Price: $259.04
50-Day SMA: $272.79 (price below average - caution)
Stop-Loss: $246.09
Guidance: "Raise stop" (stop well below SMA, could tighten)
The guidance is educational - use your own judgment based on market conditions and your risk tolerance.
All flags with their short forms:
| Flag | Short | Description |
|---|---|---|
--config |
-c |
Path to configuration file |
--percentage |
-p |
Stop-loss percentage (0-100) |
--simple |
-s |
Use simple stop-loss mode |
--trailing |
-t |
Use trailing stop-loss mode |
--atr |
-a |
Use ATR-based stop-loss mode |
--atr-period |
-P |
ATR calculation period in trading days (default: 14) |
--atr-multiplier |
-m |
ATR multiplier for stop distance (default: 2.0) |
--since |
-d |
Start date for trailing calculation (YYYY-MM-DD) |
--no-history |
-H |
Skip historical data fetching |
--week52-high |
-w |
Base calculations on 52-week high |
uv run stop-loss versionNote: If you install the package globally or in a virtual environment, you can use stop-loss directly without uv run.
This project uses mise for Python version management:
# Python 3.14 will be automatically activated via .python-version
mise install python@3.14This project includes automated tasks via mise for common development workflows:
# Run tests with coverage
mise run test
# Format code with ruff
mise run format
# Lint code with ruff
mise run lint
# Check types with ty
mise run typecheck
# Run all checks (lint + typecheck + test)
mise run checkAll tasks use uv under the hood, so dependencies are automatically managed.
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov
# Run specific test file
uv run pytest tests/test_calculator.py# Format code
uv run ruff format
# Or with mise
mise run format
# Lint code
uv run ruff check
# Or with mise
mise run lint
# Check types
uv run ty check
# Or with mise
mise run typecheck
# Fix linting issues
uv run ruff check --fixtrailing-stop-loss/
├── src/
│ └── trailing_stop_loss/
│ ├── __init__.py # Package initialization
│ ├── cli.py # Typer CLI interface
│ ├── config.py # TOML configuration loader
│ ├── fetcher.py # yfinance price fetching
│ ├── calculator.py # Stop-loss calculation logic
│ └── history.py # SQLite price history database
├── tests/
│ ├── test_config.py # Config tests
│ ├── test_fetcher.py # Fetcher tests (integration)
│ ├── test_calculator.py # Calculator tests
│ └── test_history.py # History database tests
├── .data/ # SQLite database (gitignored)
│ └── price_history.db # Historical OHLC data
├── config.toml # Configuration file
├── pyproject.toml # Project metadata and dependencies
└── .python-version # Python version for mise
Calculates stop-loss as a fixed percentage below the current price:
Stop-Loss Price = Current Price × (1 - Percentage / 100)
Example: If AAPL is at $150 USD with 5% stop-loss:
- Stop-Loss Price = $150 × 0.95 = $142.50 USD
Tracks the highest price seen (high-water mark) from historical data and calculates stop-loss from that:
Stop-Loss Price = High-Water Mark × (1 - Percentage / 100)
How it works:
- First run: Fetches 3 months of historical OHLC data from yfinance
- Stores data in SQLite database (
.data/price_history.db) - Finds the maximum high price since you started tracking
- Subsequent runs: Only fetches new data since last update
- Current price is appended to history on each run
Example: If AMD went from $220 → $267 → $255 over 3 months with 5% trailing stop:
- Historical High: $267
- Current Price: $255
- Stop-Loss: $267 × 0.95 = $253.65 (only $1.35 at risk!)
- Simple mode would give: $255 × 0.95 = $242.25 ($12.75 at risk)
The trailing mode protects your gains by locking in profits as the price rises.
Uses Average True Range (ATR) to adapt stop-loss distance to each stock's volatility:
ATR = 14-day moving average of True Range
True Range = max(High-Low, |High-PrevClose|, |Low-PrevClose|)
Stop-Loss Price = Current Price - (ATR × Multiplier)
How it works:
- Fetches historical OHLC data (same as trailing mode)
- Calculates True Range for each day (captures daily volatility)
- Takes 14-day moving average of True Range = ATR
- Stop-loss is placed at Current Price - (ATR × Multiplier)
Example: AMD at $254.84 with ATR = $10.89:
- ATR (2.0×): $254.84 - (2.0 × $10.89) = $233.06 stop ($21.78 at risk)
- Simple (5%): $254.84 × 0.95 = $242.10 stop ($12.74 at risk)
- Trailing (5%): $267 (historical high) × 0.95 = $253.65 stop ($1.19 at risk)
When to use ATR:
- Volatile stocks need wider stops to avoid being stopped out by normal price swings
- ATR automatically adapts: volatile stocks get wider stops, stable stocks get tighter stops
- Standard multipliers: 1.5× (tight), 2.0× (normal), 3.0× (loose)
Key difference from percentage modes:
- Percentage modes: Fixed % regardless of volatility
- ATR mode: Adapts to each stock's actual price movement patterns
Uses the 52-week high as the base price for calculating stop-losses:
# With Simple Strategy
Stop-Loss Price = 52-Week High × (1 - Percentage / 100)
# With ATR Strategy
Stop-Loss Price = 52-Week High - (ATR × Multiplier)
How it works:
- Fetches current 52-week high from yfinance on each run
- Stores the value in SQLite database with the price snapshot
- Uses the most recent 52-week high as the base for calculations
- Dollar risk is still calculated relative to current price
Example: AAPL at $259.04 with 52-week high of $288.62:
| Mode | Calculation | Stop-Loss | Risk/Share |
|---|---|---|---|
| Simple (8%) | $259.04 × 0.92 | $238.32 | $20.72 |
| 52-week Simple (8%) | $288.62 × 0.92 | $265.53 | -$6.49* |
| ATR (2.0×) | $259.04 - ($3.85 × 2.0) | $251.33 | $7.71 |
| 52-week ATR (2.0×) | $288.62 - ($3.85 × 2.0) | $280.91 | -$21.87* |
Note on negative risk: When 52-week high mode places the stop-loss above current price, the risk appears negative. This indicates a more conservative position where you'd exit if the price doesn't recover to near its peak.
When to use 52-week high mode:
- You bought near the peak and want to break even or minimize losses
- You're protecting paper gains from a stock that's pulled back from highs
- You prefer a more conservative approach that doesn't chase price declines
When NOT to use:
- Stock is at or near its 52-week high (use normal mode instead)
- You bought significantly below current price (trailing mode is better)
- Stock has strong downtrend from peak (may get stopped out immediately)
The tool automatically detects and displays the currency for each stock:
- US stocks typically show prices in USD
- Canadian stocks (
.TO) show prices in CAD - Each stock's currency is fetched from yfinance and displayed in the output
You can also use the package programmatically:
from trailing_stop_loss.config import Config
from trailing_stop_loss.fetcher import PriceFetcher
from trailing_stop_loss.calculator import StopLossCalculator
from trailing_stop_loss.history import PriceHistoryDB
# Load config
config = Config("config.toml")
# Fetch prices
fetcher = PriceFetcher()
price = fetcher.fetch_price("AAPL")
print(f"Current: ${price.current_price}, 52-week high: ${price.week_52_high}")
# Simple stop-loss
calculator = StopLossCalculator()
simple_result = calculator.calculate_simple(price, percentage=8.0)
print(f"Simple: ${simple_result.stop_loss_price:.2f} (${simple_result.dollar_risk:.2f} risk)")
# 52-week high mode
week52_result = calculator.calculate_simple(price, percentage=8.0, base_price=price.week_52_high)
print(f"52-week: ${week52_result.stop_loss_price:.2f} (${week52_result.dollar_risk:.2f} risk)")
# ATR-based with 52-week high
history_db = PriceHistoryDB()
history_df = history_db.get_recent_history_df("AAPL", days=15)
atr = calculator.calculate_atr(history_df, period=14)
atr_result = calculator.calculate_atr_stop_loss(
price, percentage=8.0, atr=atr, atr_multiplier=2.0,
base_price=price.week_52_high # Optional: use 52-week high
)
print(f"ATR: ${atr_result.stop_loss_price:.2f} (${atr_result.dollar_risk:.2f} risk)")- yfinance: Real-time stock price data
- typer: Modern CLI framework
- rich: Beautiful terminal output
- pytest: Testing framework
- ruff: Fast Python linter and formatter
- ty: Extremely fast Python type checker
The information provided by this tool is for educational purposes only and does not constitute financial advice. Always do your own research and consult a qualified financial advisor before making investment decisions.
MIT
Contributions welcome! Please ensure:
- All tests pass (
uv run pytest) - Code is formatted (
uv run ruff format) - Code passes linting (
uv run ruff check) - Type hints are used throughout