Skip to content

feat(analysis): add technical indicators module#41

Merged
karlwaldman merged 1 commit into
mainfrom
sprint-loop/issue-3
Jun 22, 2026
Merged

feat(analysis): add technical indicators module#41
karlwaldman merged 1 commit into
mainfrom
sprint-loop/issue-3

Conversation

@karlwaldman

@karlwaldman karlwaldman commented Jun 22, 2026

Copy link
Copy Markdown
Member

Closes #3

What

Adds a Technical Indicators module exposed as client.analysis (AnalysisResource), implementing the indicators requested in the issue with a pure pandas/numpy implementation (no new hard dependencies).

Indicators

  • SMA (Simple Moving Average)
  • EMA (Exponential Moving Average)
  • RSI (Relative Strength Index, Wilder smoothing, bounded 0-100)
  • MACD (+ signal + histogram)
  • Bollinger Bands (bb_upper/bb_middle/bb_lower)
  • ATR (Average True Range)

Two usage styles (both from the issue)

# Method 1: DataFrame helper (non-mutating)
df = client.analysis.with_indicators(
    df, indicators=["sma_20", "sma_50", "rsi", "bollinger_bands", "macd"]
)

# Method 2: direct calculation
df["sma_20"] = client.analysis.sma(df["value"], period=20)
df["rsi"]    = client.analysis.rsi(df["value"], period=14)

pandas stays the optional [pandas] extra and is guarded with the same friendly ImportError used elsewhere in the SDK.

TDD evidence (strict red-green-refactor)

RED — wrote tests/unit/test_analysis_resource.py first and confirmed it fails on unmodified code:

12 failed in 0.33s
AttributeError: 'OilPriceAPI' object has no attribute 'analysis'. Did you mean: 'analytics'?

GREEN — after adding AnalysisResource and wiring it onto the client:

12 passed in 0.40s

Full gate (mirrors .github/workflows/test.yml):

  • ruff check oilpriceapi/ → All checks passed
  • mypy oilpriceapi/ --ignore-missing-imports → Success: no issues found in 44 source files
  • CI-style pytest (no pandas, as CI installs .[dev] only): 326 passed, 10 skipped (the new pandas-gated test skips when pandas is absent, matching the existing to_dataframe test pattern), coverage 58.17% ≥ 50% gate
  • Full pytest with pandas installed: 347 passed, analysis.py at 86.67% coverage, total 61.27%

Scope / safety

  • Additive only. No changes to migrations, Stripe, auth, routes, or marketing copy.
  • README + CHANGELOG updated to reflect the new feature (per issue success criteria), including replacing the stale "technical indicators planned for future releases" note.

Success criteria checklist

  • Implement SMA, RSI, Bollinger Bands (also EMA, MACD, ATR)
  • Full test coverage
  • Documentation with examples
  • No breaking changes to existing API
  • Update README examples to match reality

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added technical analysis indicators (SMA, EMA, RSI, MACD, Bollinger Bands, ATR) to the SDK via a new Analysis Resource.
    • Introduced a DataFrame helper to compute and append indicator columns to data.
  • Documentation

    • Updated README with technical indicators examples and feature listing.
    • Updated CHANGELOG documenting the new Analysis Resource.
  • Tests

    • Added comprehensive unit test suite for the analysis resource.

Add `client.analysis` (AnalysisResource) with SMA, EMA, RSI, MACD,
Bollinger Bands, and ATR. Supports both the DataFrame helper
(`with_indicators(df, indicators=[...])`, non-mutating) and direct
per-Series calculation. Pure pandas/numpy implementation with no new
hard dependencies; pandas remains the optional `[pandas]` extra and is
guarded with the same friendly ImportError used elsewhere in the SDK.

Includes full unit coverage (12 tests, analysis.py at ~87%), README
documentation with examples for both usage styles, and a CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new AnalysisResource class to the SDK implementing six technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands, ATR) as pandas-based computations, accessible via a with_indicators DataFrame helper or direct Series methods. The resource is wired into OilPriceAPI as client.analysis, exported from the resources package, covered by a full unit test suite, and documented in the README and CHANGELOG.

Changes

Technical Indicators Feature

Layer / File(s) Summary
AnalysisResource class and indicator methods
oilpriceapi/resources/analysis.py
New module with a _require_pandas() lazy-import guard, AnalysisResource class, six indicator methods (sma, ema, rsi, macd, bollinger_bands, atr), the with_indicators DataFrame helper that tokenizes indicator names and dispatches to the appropriate method, and _validate_period/_parse_period internal helpers. RSI uses Wilder-style EWM smoothing with edge-case pinning and warm-up masking via pd.NA.
Client wiring and package exports
oilpriceapi/client.py, oilpriceapi/resources/__init__.py
OilPriceAPI.__init__ imports AnalysisResource and assigns self.analysis; resources/__init__.py adds the import and appends "AnalysisResource" to __all__.
Unit tests
tests/unit/test_analysis_resource.py
Full test suite with deterministic fixtures, correctness checks against pandas reference implementations for SMA/EMA/RSI/Bollinger Bands, with_indicators non-mutation and column-presence tests, ValueError on unknown indicators, custom column= support, and an ImportError guard test via sys.modules monkeypatching.
README and CHANGELOG
README.md, CHANGELOG.md
README adds a "Technical Indicators (New in v1.9.0)" section with example snippets and a supported-indicators list, updates the Documentation Status banner to v1.9.0, and adds a Features bullet. CHANGELOG adds an Unreleased/Added entry.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant OilPriceAPI
    participant AnalysisResource
    participant PandasSeries

    User->>OilPriceAPI: client.analysis.with_indicators(df, ["sma_20", "rsi", "bb"])
    OilPriceAPI->>AnalysisResource: with_indicators(df, indicators, column="value")
    AnalysisResource->>AnalysisResource: _require_pandas()
    AnalysisResource->>AnalysisResource: validate column exists in df
    loop each indicator token
        AnalysisResource->>AnalysisResource: _parse_period(token, default)
        AnalysisResource->>PandasSeries: sma / rsi / bollinger_bands(series, period)
        PandasSeries-->>AnalysisResource: computed Series or DataFrame columns
        AnalysisResource->>AnalysisResource: append columns to copied df
    end
    AnalysisResource-->>User: df copy with indicator columns added
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hop hop, the numbers flow,
Through rolling windows high and low,
RSI and Bollinger too,
MACD lines painted anew,
With pandas magic, bears and bulls align —
The rabbit crunched the data, everything's fine! 📈

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding a technical indicators module with the new AnalysisResource and related functionality.
Linked Issues check ✅ Passed All core requirements from issue #3 are met: six indicators (SMA, EMA, RSI, MACD, Bollinger Bands, ATR) are implemented, both DataFrame helper and direct methods are provided, comprehensive tests are included, documentation is updated, and backward compatibility is maintained.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the technical indicators module as specified in issue #3; no unrelated modifications to migrations, authentication, routes, or other existing APIs are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sprint-loop/issue-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
README.md (1)

66-95: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Clarify ATR availability in with_indicators() vs. direct methods.

The supported indicators list (line 95) includes ATR alongside the others, but the examples only show ATR being used as a direct method (client.analysis.atr(high, low, close, period=14)). ATR cannot be used via with_indicators() because it requires three input Series (high, low, close) rather than a single price column.

Consider adding a note to prevent users from trying indicators=["atr"] in the with_indicators() call, which would raise a ValueError.

💡 Example clarification to add after the Method 2 section
**Note:** ATR requires three inputs (high, low, close prices) and is only available as a direct method.
The `with_indicators()` DataFrame helper supports SMA, EMA, RSI, MACD, and Bollinger Bands.

Alternatively, add a comment to the indicator list to clarify the distinction:

Supported indicators in `with_indicators()`: SMA, EMA, RSI, MACD, Bollinger Bands.
Additional direct methods: ATR (requires high/low/close inputs).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 66 - 95, The supported indicators list at line 95
includes ATR alongside other indicators without clarifying that ATR cannot be
used with the with_indicators() method. Since ATR requires three separate inputs
(high, low, close prices) unlike the other indicators which work with a single
price column, add a clarifying note after the Method 2 section to explicitly
state that ATR is only available as a direct method (client.analysis.atr) and
that with_indicators() only supports SMA, EMA, RSI, MACD, and Bollinger Bands to
prevent users from attempting to use indicators=["atr"] which would raise a
ValueError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@oilpriceapi/resources/analysis.py`:
- Around line 231-250: The issue is that RSI, SMA, and EMA indicators are being
stored in the output dictionary using the raw indicator token as the key instead
of the normalized name. For consistency with Bollinger Bands and MACD, and to
avoid duplicate/unstable schema columns, replace all instances of out[indicator]
with out[name] in the conditional blocks for RSI, SMA, and EMA. The variable
name is already normalized via strip() and lower() at the beginning of the loop,
so using this normalized name ensures consistent, canonical column names
regardless of the case or whitespace in the input token.

In `@tests/unit/test_analysis_resource.py`:
- Around line 100-109: The test_with_indicators_adds_requested_columns test
verifies that MACD columns are included in the output when the MACD indicator is
requested, but it only checks for the macd and macd_signal columns. Add an
additional assertion to also verify that macd_histogram is present in
out.columns, following the same pattern as the existing assertions for macd and
macd_signal, so that any regression removing the macd_histogram output would be
caught by this test.

---

Nitpick comments:
In `@README.md`:
- Around line 66-95: The supported indicators list at line 95 includes ATR
alongside other indicators without clarifying that ATR cannot be used with the
with_indicators() method. Since ATR requires three separate inputs (high, low,
close prices) unlike the other indicators which work with a single price column,
add a clarifying note after the Method 2 section to explicitly state that ATR is
only available as a direct method (client.analysis.atr) and that
with_indicators() only supports SMA, EMA, RSI, MACD, and Bollinger Bands to
prevent users from attempting to use indicators=["atr"] which would raise a
ValueError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 528b806b-fa3d-40a6-8288-b3d52e2d3ddb

📥 Commits

Reviewing files that changed from the base of the PR and between f82de4d and 3e66837.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • README.md
  • oilpriceapi/client.py
  • oilpriceapi/resources/__init__.py
  • oilpriceapi/resources/analysis.py
  • tests/unit/test_analysis_resource.py

Comment on lines +231 to +250
for indicator in indicators:
name = indicator.strip().lower()

if name in ("bollinger_bands", "bollinger", "bb"):
bands = self.bollinger_bands(prices)
for col in bands.columns:
out[col] = bands[col]
elif name == "macd":
macd_df = self.macd(prices)
for col in macd_df.columns:
out[col] = macd_df[col]
elif name in ("rsi",) or name.startswith("rsi_"):
period = self._parse_period(name, default=14)
out[indicator] = self.rsi(prices, period=period)
elif name == "sma" or name.startswith("sma_"):
period = self._parse_period(name, default=20)
out[indicator] = self.sma(prices, period=period)
elif name == "ema" or name.startswith("ema_"):
period = self._parse_period(name, default=20)
out[indicator] = self.ema(prices, period=period)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize indicator-derived output column names.

with_indicators() parses indicator tokens case/whitespace-insensitively, but SMA/EMA/RSI columns are written using the raw token. This produces unstable schemas (e.g., " SMA_20 " or duplicate "SMA_20"/"sma_20" columns) instead of canonical names.

💡 Proposed fix
         for indicator in indicators:
             name = indicator.strip().lower()

             if name in ("bollinger_bands", "bollinger", "bb"):
                 bands = self.bollinger_bands(prices)
                 for col in bands.columns:
                     out[col] = bands[col]
             elif name == "macd":
                 macd_df = self.macd(prices)
                 for col in macd_df.columns:
                     out[col] = macd_df[col]
             elif name in ("rsi",) or name.startswith("rsi_"):
                 period = self._parse_period(name, default=14)
-                out[indicator] = self.rsi(prices, period=period)
+                out[name] = self.rsi(prices, period=period)
             elif name == "sma" or name.startswith("sma_"):
                 period = self._parse_period(name, default=20)
-                out[indicator] = self.sma(prices, period=period)
+                out[name] = self.sma(prices, period=period)
             elif name == "ema" or name.startswith("ema_"):
                 period = self._parse_period(name, default=20)
-                out[indicator] = self.ema(prices, period=period)
+                out[name] = self.ema(prices, period=period)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oilpriceapi/resources/analysis.py` around lines 231 - 250, The issue is that
RSI, SMA, and EMA indicators are being stored in the output dictionary using the
raw indicator token as the key instead of the normalized name. For consistency
with Bollinger Bands and MACD, and to avoid duplicate/unstable schema columns,
replace all instances of out[indicator] with out[name] in the conditional blocks
for RSI, SMA, and EMA. The variable name is already normalized via strip() and
lower() at the beginning of the loop, so using this normalized name ensures
consistent, canonical column names regardless of the case or whitespace in the
input token.

Comment on lines +100 to +109
def test_with_indicators_adds_requested_columns(self, client, price_df):
out = client.analysis.with_indicators(
price_df,
indicators=["sma_20", "sma_50", "rsi", "bollinger_bands", "macd"],
)
for col in ["sma_20", "sma_50", "rsi", "bb_upper", "bb_middle", "bb_lower"]:
assert col in out.columns, f"missing column {col}"
assert "macd" in out.columns
assert "macd_signal" in out.columns

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert macd_histogram in the helper output contract test.

This test checks only two MACD columns, so a regression removing macd_histogram would not be caught.

✅ Suggested test addition
         for col in ["sma_20", "sma_50", "rsi", "bb_upper", "bb_middle", "bb_lower"]:
             assert col in out.columns, f"missing column {col}"
         assert "macd" in out.columns
         assert "macd_signal" in out.columns
+        assert "macd_histogram" in out.columns
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_with_indicators_adds_requested_columns(self, client, price_df):
out = client.analysis.with_indicators(
price_df,
indicators=["sma_20", "sma_50", "rsi", "bollinger_bands", "macd"],
)
for col in ["sma_20", "sma_50", "rsi", "bb_upper", "bb_middle", "bb_lower"]:
assert col in out.columns, f"missing column {col}"
assert "macd" in out.columns
assert "macd_signal" in out.columns
def test_with_indicators_adds_requested_columns(self, client, price_df):
out = client.analysis.with_indicators(
price_df,
indicators=["sma_20", "sma_50", "rsi", "bollinger_bands", "macd"],
)
for col in ["sma_20", "sma_50", "rsi", "bb_upper", "bb_middle", "bb_lower"]:
assert col in out.columns, f"missing column {col}"
assert "macd" in out.columns
assert "macd_signal" in out.columns
assert "macd_histogram" in out.columns
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_analysis_resource.py` around lines 100 - 109, The
test_with_indicators_adds_requested_columns test verifies that MACD columns are
included in the output when the MACD indicator is requested, but it only checks
for the macd and macd_signal columns. Add an additional assertion to also verify
that macd_histogram is present in out.columns, following the same pattern as the
existing assertions for macd and macd_signal, so that any regression removing
the macd_histogram output would be caught by this test.

@karlwaldman karlwaldman merged commit a730c74 into main Jun 22, 2026
6 checks passed
@karlwaldman karlwaldman deleted the sprint-loop/issue-3 branch June 22, 2026 11:46
karlwaldman added a commit that referenced this pull request Jul 3, 2026
Add `client.analysis` (AnalysisResource) with SMA, EMA, RSI, MACD,
Bollinger Bands, and ATR. Supports both the DataFrame helper
(`with_indicators(df, indicators=[...])`, non-mutating) and direct
per-Series calculation. Pure pandas/numpy implementation with no new
hard dependencies; pandas remains the optional `[pandas]` extra and is
guarded with the same friendly ImportError used elsewhere in the SDK.

Includes full unit coverage (12 tests, analysis.py at ~87%), README
documentation with examples for both usage styles, and a CHANGELOG entry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Feature: Add Technical Indicators Module

1 participant