Skip to content

Conversation

@tinnet
Copy link
Owner

@tinnet tinnet commented Jan 9, 2026

Summary

Add support for specifying entry prices directly with ticker symbols using TICKER:PRICE format (e.g., AAPL:150, SHOP.TO:200). Entry prices work exclusively with trailing mode to set a floor for high water mark calculations based on actual cost basis.

Usage

CLI:

# Entry prices with trailing mode
uv run stop-loss calculate AAPL:150 GOOGL:2800 --trailing -p 5

# Mixed format (some with entry prices, some without)
uv run stop-loss calculate AAPL:150 NVDA --trailing

Config:

tickers = [
    "AAPL:150.50",   # Entry price $150.50 sets HWM floor
    "SHOP.TO:200",   # Entry price $200 CAD sets HWM floor
    "NVDA",          # No entry price - uses database HWM only
]
trailing_enabled = true  # Required for entry prices

How It Works

Entry price sets the minimum high water mark in trailing mode:

  • hwm = max(database_hwm, entry_price)
  • Ensures stop never goes below your entry point
  • As price rises above entry, stop trails normally from the peak
  • Protects gains while respecting cost basis

Example:

  • Bought AAPL at $180
  • Stock went to $200 (peak), now at $195
  • HWM = max($200, $180) = $200
  • Stop = $200 × 0.95 = $190
  • Risk = $195 - $190 = $5 per share

Why Trailing Mode Only?

Entry prices are restricted to trailing mode because:

  • Simple/ATR modes calculate from current price where entry price creates confusing "risk" calculations
  • When stock is UP from entry: Shows huge misleading "risk"
  • When stock is DOWN: Shows negative risk or stop > current (triggers immediately)
  • Trailing mode naturally protects gains from cost basis point forward—the math is intuitive and correct

Implementation Details

Built in 7 incremental commits with full test coverage:

  1. Ticker parsing - parse_ticker_with_price() utility function
  2. Data layer - Extended StockPrice and PriceFetcher
  3. Calculator layer - Extended StopLossResult with entry_price field
  4. CLI integration - Argument parsing and calculation logic
  5. Display - Dynamic "Entry Price" column in results table (magenta)
  6. Documentation - README and config.toml updates
  7. Validation - Restrict to trailing mode with clear error messages

Testing

  • All 83 tests pass ✅
  • Backward compatible (plain tickers unchanged) ✅
  • Validation rejects simple/ATR modes with helpful error ✅
  • Entry Price column only appears when entry prices present ✅
  • Mixed format works (some tickers with prices, some without) ✅

Test Plan

# 1. Entry price with trailing mode (should work)
uv run stop-loss calculate AAPL:150 --trailing -p 5 --no-history

# 2. Entry price with simple mode (should reject)
uv run stop-loss calculate AAPL:150 --simple -p 5
# Expected: Error: Entry prices only supported with trailing mode

# 3. Mixed format
uv run stop-loss calculate AAPL:150 GOOGL --trailing -p 5 --no-history

# 4. Backward compatibility (no entry prices)
uv run stop-loss calculate AAPL GOOGL --simple -p 5 --no-history

# 5. Invalid format (should error)
uv run stop-loss calculate AAPL:abc --trailing
# Expected: Error: Invalid price in 'AAPL:abc'

Breaking Changes

None - fully backward compatible.


🤖 Generated with Claude Code

tinnet and others added 7 commits January 9, 2026 08:52
Add parse_ticker_with_price() utility function that parses tickers
in format 'TICKER' or 'TICKER:PRICE' (e.g., "AAPL:150", "SHOP.TO:200").

Add Config.tickers_with_prices property that returns list of
(ticker, entry_price) tuples, supporting mixed format usage.

Includes comprehensive test coverage for:
- Plain ticker parsing
- Ticker with price parsing
- Canadian tickers (e.g., SHOP.TO:200)
- Error cases (invalid price, negative price, empty strings)
- Config property integration
- Backward compatibility

Entry prices are runtime parameters only (not stored in database).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add entry_price field to StockPrice dataclass to store user-provided
entry prices alongside fetched market data.

Update PriceFetcher.fetch_price() to accept optional entry_price
parameter and pass it through to StockPrice construction.

Update PriceFetcher.fetch_multiple() to handle both string format
("AAPL") and tuple format (("AAPL", 150.0)) for mixed usage.

Add comprehensive tests for:
- Fetching with entry price
- Fetching without entry price (backward compatibility)
- Mixed format (some tickers with prices, some without)
- Entry price with cache updates
- Entry price persistence through data flow

All existing tests continue to pass, ensuring backward compatibility.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add entry_price field to StopLossResult dataclass to store and display
user-provided entry prices in calculation results.

Update all calculator methods to pass entry_price from StockPrice to
StopLossResult:
- calculate_simple() - passes entry_price through
- calculate_trailing() - passes entry_price through
- calculate_atr_stop_loss() - passes entry_price through

Add comprehensive tests verifying:
- Entry price flows through all calculation methods
- Entry price is display-only (doesn't affect calculations)
- Entry price works with all modes (simple, trailing, ATR)
- Entry price is independent of base_price parameter
- Backward compatibility (None when not provided)

Entry price is for display purposes only and does not affect
stop-loss calculations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update CLI argument parsing to parse TICKER:PRICE format from both
command line arguments and config file.

Integrate entry price into calculation logic:
- Simple/ATR modes: entry price becomes base_price (overrides 52-week high)
- Trailing mode: entry price sets floor for high water mark via max(db_hwm, entry_price)
- Mixed usage: tickers without entry prices use current price/database HWM

Key changes:
- Parse CLI args with parse_ticker_with_price()
- Use config.tickers_with_prices for config-based tickers
- Pass list of (ticker, entry_price) tuples to PriceFetcher.fetch_multiple()
- Extract ticker from tuples for historical data fetching
- Apply entry price logic in calculation section for all three modes

Entry price provides better stop-loss positioning when you know your
actual entry point, ensuring stops are calculated from your true cost basis.

All existing tests pass, maintaining full backward compatibility.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add dynamic Entry Price column to results table that appears only
when at least one ticker has an entry price specified.

Column displays in magenta color to distinguish from other price columns:
- Current Price (white)
- Entry Price (magenta) - NEW
- 52-Week High (cyan)
- Stop-Loss Price (green/red based on position)

Layout automatically adjusts based on data available:
- Entry Price column appears after Current Price
- Only shown when entry prices are present
- Shows "N/A" for tickers without entry prices in mixed usage

This provides clear visibility into which price basis was used for
calculations, helping users verify their stop-loss positioning.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add comprehensive documentation for entry price support in README.md:
- New "Entry Price Support" section after "Custom Percentage"
- CLI usage examples with TICKER:PRICE format
- How it works for each mode (simple, trailing, ATR)
- Example output showing Entry Price column
- Config file format examples

Update config.toml with entry price format:
- Add format comment explaining TICKER:PRICE syntax
- Show commented examples with entry prices
- Demonstrate mixed usage (some with prices, some without)

Entry price support allows users to calculate stop-losses based on
their actual entry point rather than current market price, providing
more accurate risk management based on true cost basis.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add validation to reject entry prices when using simple or ATR modes,
as the financial math produces misleading risk calculations in these modes.

Problem identified:
- Simple/ATR with entry price: stop = entry * (1 - %)
- Risk calculation: current - stop
- When stock is UP from entry: Shows huge "risk" (nonsensical)
- When stock is DOWN: Shows negative risk or stop > current

Only trailing mode makes financial sense:
- Entry price sets minimum high water mark: max(db_hwm, entry_price)
- Stop trails from highest point since entry
- Risk correctly shows distance from current to trailing stop

Changes:
- Add validation after mode determination in CLI
- Clear error message directing users to use --trailing
- Remove entry price logic from simple/ATR calculation paths
- Update documentation to clarify "trailing mode only"
- Update config.toml comments

All 83 tests pass. Backward compatibility maintained.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Jan 9, 2026

Code Review

Found 2 issues that need attention:


Issue 1: Cache Mutation Side Effect

File: src/trailing_stop_loss/fetcher.py (lines 47-52)
Severity: Bug

When using the cache with an entry_price, the code mutates the cached StockPrice object directly:

"""
if use_cache and ticker in self._cache:
cached = self._cache[ticker]
# Update entry price in cached result if provided
if entry_price is not None:
cached.entry_price = entry_price
return cached

The Problem:
The cached object's entry_price field is permanently modified. If a subsequent call fetches the same ticker from cache without providing an entry price, the previously set entry price will persist unexpectedly.

Example failure scenario:

  1. Call fetch_price("AAPL", use_cache=True, entry_price=150.0) - sets entry_price to 150.0 in cache
  2. Call fetch_price("AAPL", use_cache=True) (no entry_price) - returns cached object with entry_price=150.0 instead of None

Suggested fix:
Instead of mutating the cached object, create a new instance with the modified entry_price:

if use_cache and ticker in self._cache:
    cached = self._cache[ticker]
    # Return a copy with updated entry price if provided
    if entry_price is not None:
        return dataclasses.replace(cached, entry_price=entry_price)
    return cached

Issue 2: Type Annotation Mismatch

File: src/trailing_stop_loss/fetcher.py (lines 85-86)
Severity: Type Error

The type annotation doesn't match the actual implementation:

def fetch_multiple(
self, tickers: list[str] | list[tuple[str, float | None]], skip_errors: bool = True
) -> dict[str, StockPrice | Exception]:

The Problem:
The current type annotation list[str] | list[tuple[str, float | None]] indicates the parameter is either a list of all strings OR a list of all tuples. However, the implementation (lines 99-105) uses isinstance(item, tuple) to handle mixed lists containing both strings and tuples.

The test file even uses the correct type:

"""Test fetching with mixed format (some with entry prices, some without)."""
tickers: list[str | tuple[str, float]] = [("AAPL", 150.0), "GOOGL", ("MSFT", 400.0)]

Impact:
Type checkers like mypy would flag passing a mixed list to this function as a type error.

Suggested fix:

def fetch_multiple(
    self, tickers: list[str | tuple[str, float | None]], skip_errors: bool = True
) -> dict[str, StockPrice | Exception]:

Reviewed for bugs and CLAUDE.md compliance

- Use dataclasses.replace() to avoid mutating cached StockPrice objects
- Fix type annotation to allow mixed lists in fetch_multiple()

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@tinnet tinnet merged commit 3ce5867 into main Jan 10, 2026
1 check passed
@tinnet tinnet deleted the feature/entry-price-support branch January 10, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants