heyremora 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- heyremora-0.5.1/.gitignore +70 -0
- heyremora-0.5.1/CHANGELOG.md +58 -0
- heyremora-0.5.1/PKG-INFO +79 -0
- heyremora-0.5.1/PUBLISHING.md +87 -0
- heyremora-0.5.1/README.md +65 -0
- heyremora-0.5.1/pyproject.toml +24 -0
- heyremora-0.5.1/src/heyremora/__init__.py +3 -0
- heyremora-0.5.1/src/heyremora/client.py +212 -0
- heyremora-0.5.1/src/heyremora/config.py +36 -0
- heyremora-0.5.1/src/heyremora/server.py +1707 -0
- heyremora-0.5.1/tests/__init__.py +0 -0
- heyremora-0.5.1/tests/test_client.py +107 -0
- heyremora-0.5.1/tests/test_tools.py +831 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Environment variables
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
.env.*.local
|
|
5
|
+
|
|
6
|
+
# Data cache
|
|
7
|
+
data_cache/
|
|
8
|
+
data_cache/*.json
|
|
9
|
+
|
|
10
|
+
# Ignore JSON files except in frontend
|
|
11
|
+
*.json
|
|
12
|
+
!frontend/*.json
|
|
13
|
+
!frontend/**/*.json
|
|
14
|
+
|
|
15
|
+
# Python
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.py[cod]
|
|
18
|
+
*$py.class
|
|
19
|
+
*.so
|
|
20
|
+
.Python
|
|
21
|
+
build/
|
|
22
|
+
develop-eggs/
|
|
23
|
+
dist/
|
|
24
|
+
downloads/
|
|
25
|
+
eggs/
|
|
26
|
+
.eggs/
|
|
27
|
+
lib/
|
|
28
|
+
lib64/
|
|
29
|
+
parts/
|
|
30
|
+
sdist/
|
|
31
|
+
var/
|
|
32
|
+
wheels/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.installed.cfg
|
|
35
|
+
*.egg
|
|
36
|
+
MANIFEST
|
|
37
|
+
|
|
38
|
+
# Virtual environments
|
|
39
|
+
venv/
|
|
40
|
+
env/
|
|
41
|
+
ENV/
|
|
42
|
+
env.bak/
|
|
43
|
+
venv.bak/
|
|
44
|
+
|
|
45
|
+
# IDEs
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
*~
|
|
51
|
+
.DS_Store
|
|
52
|
+
|
|
53
|
+
# Logs
|
|
54
|
+
*.log
|
|
55
|
+
logs/
|
|
56
|
+
|
|
57
|
+
# Testing
|
|
58
|
+
.pytest_cache/
|
|
59
|
+
.coverage
|
|
60
|
+
htmlcov/
|
|
61
|
+
|
|
62
|
+
# Jupyter Notebook
|
|
63
|
+
.ipynb_checkpoints
|
|
64
|
+
friday_options_income/*.db
|
|
65
|
+
*.db
|
|
66
|
+
|
|
67
|
+
# Python cache
|
|
68
|
+
__pycache__/
|
|
69
|
+
*.py[cod]
|
|
70
|
+
*$py.class
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0 — 2026-06-10
|
|
4
|
+
|
|
5
|
+
- New: `analyze_strategy` tool — payoff analysis for any multi-leg strategy (up to 24 legs): max gain/loss, breakevens, net debit/credit, detected strategy name.
|
|
6
|
+
- New: `save_screener_config` tool — save the current filter set as a named preset (pairs with `list_saved_screeners`).
|
|
7
|
+
- New: `get_trade_dna` tool — the user's trading-style profile built from their own closed trades (sweet-spot delta/DTE/IV, best underlyings); use it to tailor screens.
|
|
8
|
+
- 30 tools and 2 resources total (was 27).
|
|
9
|
+
|
|
10
|
+
## 0.4.0 — 2026-06-05
|
|
11
|
+
|
|
12
|
+
- New: `get_technicals` tool — trend, 50/200-day MAs, 13-week range, and support/resistance levels for any ticker.
|
|
13
|
+
- New: `get_unusual_activity` tool — options flow showing where big premium is moving, by ticker or market-wide.
|
|
14
|
+
- New: `scan_stocks` tool — scan stocks by IV rank, valuation, market cap, dividend, sector, signal, and earnings proximity.
|
|
15
|
+
- New: `get_portfolio_analytics` tool — track-record analytics: win rate, avg P&L, and which entry conditions (delta/DTE/IV/ticker/sector) are working.
|
|
16
|
+
- New: `list_portfolios` tool — list your portfolios with their IDs, for discovery by the portfolio-scoped tools.
|
|
17
|
+
- New: `get_morning_briefing` tool — daily snapshot of open positions, unrealized P&L, expirations, and action items.
|
|
18
|
+
- New: `list_saved_screeners` tool — list your saved screener configs and their filter settings.
|
|
19
|
+
- 27 tools and 2 resources total (was 20).
|
|
20
|
+
|
|
21
|
+
## 0.3.0 — 2026-06-04
|
|
22
|
+
|
|
23
|
+
- Rebrand: package renamed from `options-trader-x-mcp` to `heyremora`. Install with `pip install heyremora`; import as `heyremora`.
|
|
24
|
+
- Rebrand: env vars renamed to `REMORA_API_KEY` / `REMORA_API_URL`. The legacy `OTX_API_KEY` / `OTX_API_URL` names are still accepted as a fallback, so existing setups keep working.
|
|
25
|
+
- Rebrand: MCP server display name is now "Remora"; resource URIs moved from `otx://` to `remora://`.
|
|
26
|
+
- No tool signatures, endpoints, or behavior changed — this is a naming-only release.
|
|
27
|
+
|
|
28
|
+
## 0.2.1 — 2026-05-18
|
|
29
|
+
|
|
30
|
+
- Fix: `get_chain` default sort changed from volume to strike — ATM strikes were crowded out by deep OTM high-volume contracts
|
|
31
|
+
- Fix: `get_chain` default `top` increased from 50 to 200 for better strike coverage
|
|
32
|
+
- New: `sort` parameter on `get_chain` — choose 'strike' (default), 'volume', or 'oi'
|
|
33
|
+
- Fix: Polygon API now queries with `sort=strike_price&order=asc` for complete chain data
|
|
34
|
+
|
|
35
|
+
## 0.2.0 — 2026-05-18
|
|
36
|
+
|
|
37
|
+
- New: `get_chain` tool — browse raw options chains with strike/expiry/type filters
|
|
38
|
+
- New: `get_option_quote` tool — detailed quote for a single contract
|
|
39
|
+
- New: staleness timestamps (`as_of`) on all read tool responses
|
|
40
|
+
- New: filter summaries explaining why results were filtered out
|
|
41
|
+
- Fix: earnings calendar returned empty — wrong dict keys for upstream response
|
|
42
|
+
- Fix: `get_chain` crashed on None strike/IV values — added null guards
|
|
43
|
+
- Fix: `_fmt_filter_summary` KeyError on malformed data — switched to `.get()` with defaults
|
|
44
|
+
- Fix: MCP screener `max_bid_ask_spread_pct` default relaxed from 0.15 to 0.25 (less aggressive filtering)
|
|
45
|
+
- Fix: `find_trades` progressive fallback — widens DTE, tries all strategies before returning empty
|
|
46
|
+
- Fix: earnings data 5-level fallback chain (yfinance calendar → earnings_dates → info → Polygon → quarterly estimate)
|
|
47
|
+
|
|
48
|
+
## 0.1.1 — 2026-05-15
|
|
49
|
+
|
|
50
|
+
- Fix: `screen_long_calls` and `screen_long_puts` DTE parameter names now match backend (`dte_min`/`dte_max` instead of `min_dte`/`max_dte`). Previously, custom DTE values were silently ignored.
|
|
51
|
+
- Fix: DTE defaults updated from 60/730 to 30/180 to match the web screener.
|
|
52
|
+
- Fix: `compare_strategies` tool refactored to use the trade finder endpoint.
|
|
53
|
+
- Remove dead `compare_strategies()` client method.
|
|
54
|
+
|
|
55
|
+
## 0.1.0 — 2026-05-15
|
|
56
|
+
|
|
57
|
+
- Initial release.
|
|
58
|
+
- Tools: `screen_csps`, `screen_covered_calls`, `screen_long_calls`, `screen_long_puts`, `screen_credit_spreads`, `screen_debit_spreads`, `find_trades`, `compare_strategies`, `get_portfolio`, `get_positions`, `get_open_trades`, `get_closed_trades`, `get_metrics`.
|
heyremora-0.5.1/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: heyremora
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Summary: MCP server for Remora — options screening, portfolio analytics, and strategy comparison via Claude Desktop
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx<1.0,>=0.24
|
|
8
|
+
Requires-Dist: mcp<2.0,>=1.6
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: anyio; extra == 'test'
|
|
11
|
+
Requires-Dist: pytest; extra == 'test'
|
|
12
|
+
Requires-Dist: pytest-anyio; extra == 'test'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Remora MCP Server
|
|
16
|
+
|
|
17
|
+
MCP server for Remora — 30 tools and 2 resources for options screening, chain browsing, trade finding, portfolio management, analytics, and market data. Connect Claude Desktop, Cursor, or any MCP-compatible AI assistant.
|
|
18
|
+
|
|
19
|
+
## Tools (30)
|
|
20
|
+
|
|
21
|
+
| Category | Tool | Description |
|
|
22
|
+
|----------|------|-------------|
|
|
23
|
+
| Screening | `screen_csp` | Screen for cash-secured puts |
|
|
24
|
+
| Screening | `screen_cc` | Screen for covered calls |
|
|
25
|
+
| Screening | `screen_credit_spreads` | Screen for credit spreads (bull put / bear call) |
|
|
26
|
+
| Screening | `screen_long_calls` | Screen for long call options |
|
|
27
|
+
| Screening | `screen_long_puts` | Screen for long put options |
|
|
28
|
+
| Screening | `screen_debit_spreads` | Screen for debit spreads (bull call / bear put) |
|
|
29
|
+
| Screening | `list_saved_screeners` | List your saved screener configs and their filter settings |
|
|
30
|
+
| Trade Finding | `find_trades` | Find best strategies for a ticker given thesis, goal, and capital. Progressive fallback — never returns empty. |
|
|
31
|
+
| Trade Finding | `compare_strategies` | Compare CSP vs LEAP strategies side-by-side |
|
|
32
|
+
| Market Data | `get_chain` | Browse raw options chain for a ticker with filtering by expiration, type, volume, and open interest |
|
|
33
|
+
| Market Data | `get_option_quote` | Get detailed quote for a single option contract — pricing, greeks, activity, valuation, probabilities |
|
|
34
|
+
| Market Data | `get_iv_rank` | Get IV rank and IV percentile for any ticker |
|
|
35
|
+
| Market Data | `get_technicals` | Get trend, 50/200-day MAs, 13-week range, and support/resistance levels |
|
|
36
|
+
| Market Data | `get_earnings` | Get earnings data or upcoming earnings calendar. 5-level fallback chain for reliable data. |
|
|
37
|
+
| Market Data | `get_unusual_activity` | Options flow — where big premium is moving, by ticker or market-wide |
|
|
38
|
+
| Market Data | `scan_stocks` | Scan stocks by IV rank, valuation, market cap, dividend, sector, signal, and earnings proximity |
|
|
39
|
+
| Portfolio | `list_portfolios` | List your portfolios with their IDs (discovery for the tools below) |
|
|
40
|
+
| Portfolio | `get_portfolio` | Get portfolio details, metrics, open trades, positions, and spreads |
|
|
41
|
+
| Portfolio | `get_positions` | Get stock positions (shares held) |
|
|
42
|
+
| Portfolio | `get_portfolio_analytics` | Track-record analytics — win rate, avg P&L, and what entry conditions are working |
|
|
43
|
+
| Portfolio | `get_morning_briefing` | Daily snapshot — open positions, unrealized P&L, expirations, and action items |
|
|
44
|
+
| Portfolio | `add_trade` | Add a trade (BUY/SELL, PUT/CALL) |
|
|
45
|
+
| Portfolio | `close_trade` | Close a trade with exit price |
|
|
46
|
+
| Portfolio | `add_spread` | Add a spread (12 types supported) |
|
|
47
|
+
| Portfolio | `close_spread` | Close a spread with exit price |
|
|
48
|
+
| Portfolio | `add_position` | Add a stock position |
|
|
49
|
+
| Portfolio | `refresh_prices` | Refresh live market prices |
|
|
50
|
+
| Strategy | `analyze_strategy` | Payoff analysis for any multi-leg strategy — max gain/loss, breakevens, net debit/credit, strategy name |
|
|
51
|
+
| Screening | `save_screener_config` | Save the current screener filter set as a named preset |
|
|
52
|
+
| Portfolio | `get_trade_dna` | Your trading-style profile from closed trades — sweet-spot delta/DTE/IV and best underlyings |
|
|
53
|
+
|
|
54
|
+
## Example prompts
|
|
55
|
+
|
|
56
|
+
- "What should I look at today?" → `list_portfolios`, then `get_morning_briefing(portfolio_id)`
|
|
57
|
+
- "Build a bull put spread on AAPL at 180/175 for Jul 17 and show me the payoff" → `analyze_strategy(legs=[...])`
|
|
58
|
+
- "Screen for CSPs the way I usually trade" → `get_trade_dna()`, then `screen_csp` with the sweet-spot delta/DTE
|
|
59
|
+
- "Save this screen as 'weekly income'" → `save_screener_config(name, config)`
|
|
60
|
+
|
|
61
|
+
## Resources (2)
|
|
62
|
+
|
|
63
|
+
- `remora://watchlists` — List all watchlists and tickers
|
|
64
|
+
- `remora://watchlist/{id}` — Get a specific watchlist
|
|
65
|
+
|
|
66
|
+
## Key Features
|
|
67
|
+
|
|
68
|
+
- **Filter rejection summaries** — Screeners explain WHY contracts were filtered out when results are empty
|
|
69
|
+
- **Progressive fallback** — `find_trades` widens DTE, tries all defined-risk strategies, and returns diagnostics with suggestions
|
|
70
|
+
- **5-level earnings fallback** — `get_earnings` chains multiple data sources for reliable coverage
|
|
71
|
+
- **Staleness timestamps** — All read tools show when data was last retrieved
|
|
72
|
+
|
|
73
|
+
## Setup
|
|
74
|
+
|
|
75
|
+
1. Generate an API key at your account page (Pro subscription required)
|
|
76
|
+
2. Install: `pip install heyremora`
|
|
77
|
+
3. Set environment variables (`REMORA_API_KEY`, `REMORA_API_URL`) and add to Claude Desktop config
|
|
78
|
+
|
|
79
|
+
See the full setup guide at `/docs/mcp-setup` on the Remora website.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Publishing to PyPI
|
|
2
|
+
|
|
3
|
+
## When to publish a new version
|
|
4
|
+
|
|
5
|
+
Publish a new PyPI release any time you change files under `mcp-server/src/` that affect what MCP users get when they `pip install`. Specifically:
|
|
6
|
+
|
|
7
|
+
- **Tool signatures changed** — parameter names, types, defaults, or descriptions in `server.py`
|
|
8
|
+
- **Client endpoints changed** — URLs, request body keys, or new/removed methods in `client.py`
|
|
9
|
+
- **Dependencies changed** — anything in `pyproject.toml` `[project.dependencies]`
|
|
10
|
+
- **Bug fixes** in server or client code
|
|
11
|
+
|
|
12
|
+
Do NOT need a publish:
|
|
13
|
+
- Changes to docs only (`README.md`, `PUBLISHING.md`, `CHANGELOG.md`)
|
|
14
|
+
- Changes to backend API code (users hit the live API, not a local copy)
|
|
15
|
+
- Changes to `frontend/docs/mcp-setup.html` (that's the website, not the package)
|
|
16
|
+
|
|
17
|
+
## How to publish
|
|
18
|
+
|
|
19
|
+
### 1. Bump the version
|
|
20
|
+
|
|
21
|
+
Edit `mcp-server/pyproject.toml`:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
version = "0.1.2" # bump from previous
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use semver:
|
|
28
|
+
- **Patch** (0.1.x): bug fixes, default changes, field renames
|
|
29
|
+
- **Minor** (0.x.0): new tools, new parameters, new endpoints
|
|
30
|
+
- **Major** (x.0.0): breaking changes to existing tool signatures
|
|
31
|
+
|
|
32
|
+
### 2. Update the changelog
|
|
33
|
+
|
|
34
|
+
Add an entry to `mcp-server/CHANGELOG.md` with the version, date, and what changed.
|
|
35
|
+
|
|
36
|
+
### 3. Build
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd mcp-server
|
|
40
|
+
uv build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This creates `.tar.gz` and `.whl` files in `mcp-server/dist/`.
|
|
44
|
+
|
|
45
|
+
### 4. Publish
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
twine upload dist/heyremora-<VERSION>* \
|
|
49
|
+
-u __token__ \
|
|
50
|
+
-p "$(security find-generic-password -a pypi -s pypi-api-token -w)"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The PyPI API token is stored in the macOS Keychain under:
|
|
54
|
+
- **Account:** `pypi`
|
|
55
|
+
- **Service:** `pypi-api-token`
|
|
56
|
+
|
|
57
|
+
### 5. Verify
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install --upgrade heyremora
|
|
61
|
+
python -c "import heyremora; print('ok')"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 6. Commit and push
|
|
65
|
+
|
|
66
|
+
Commit the version bump, changelog, and any source changes together. Push to `feature/tier-limiting` (or whatever the active dev branch is).
|
|
67
|
+
|
|
68
|
+
## Token management
|
|
69
|
+
|
|
70
|
+
The PyPI token was saved to macOS Keychain with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
security add-generic-password -a "pypi" -s "pypi-api-token" -w "pypi-TOKEN_HERE"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To retrieve it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
security find-generic-password -a "pypi" -s "pypi-api-token" -w
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To rotate the token: generate a new one at https://pypi.org/manage/account/token/, then update the Keychain entry:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
security delete-generic-password -a "pypi" -s "pypi-api-token"
|
|
86
|
+
security add-generic-password -a "pypi" -s "pypi-api-token" -w "pypi-NEW_TOKEN"
|
|
87
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Remora MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server for Remora — 30 tools and 2 resources for options screening, chain browsing, trade finding, portfolio management, analytics, and market data. Connect Claude Desktop, Cursor, or any MCP-compatible AI assistant.
|
|
4
|
+
|
|
5
|
+
## Tools (30)
|
|
6
|
+
|
|
7
|
+
| Category | Tool | Description |
|
|
8
|
+
|----------|------|-------------|
|
|
9
|
+
| Screening | `screen_csp` | Screen for cash-secured puts |
|
|
10
|
+
| Screening | `screen_cc` | Screen for covered calls |
|
|
11
|
+
| Screening | `screen_credit_spreads` | Screen for credit spreads (bull put / bear call) |
|
|
12
|
+
| Screening | `screen_long_calls` | Screen for long call options |
|
|
13
|
+
| Screening | `screen_long_puts` | Screen for long put options |
|
|
14
|
+
| Screening | `screen_debit_spreads` | Screen for debit spreads (bull call / bear put) |
|
|
15
|
+
| Screening | `list_saved_screeners` | List your saved screener configs and their filter settings |
|
|
16
|
+
| Trade Finding | `find_trades` | Find best strategies for a ticker given thesis, goal, and capital. Progressive fallback — never returns empty. |
|
|
17
|
+
| Trade Finding | `compare_strategies` | Compare CSP vs LEAP strategies side-by-side |
|
|
18
|
+
| Market Data | `get_chain` | Browse raw options chain for a ticker with filtering by expiration, type, volume, and open interest |
|
|
19
|
+
| Market Data | `get_option_quote` | Get detailed quote for a single option contract — pricing, greeks, activity, valuation, probabilities |
|
|
20
|
+
| Market Data | `get_iv_rank` | Get IV rank and IV percentile for any ticker |
|
|
21
|
+
| Market Data | `get_technicals` | Get trend, 50/200-day MAs, 13-week range, and support/resistance levels |
|
|
22
|
+
| Market Data | `get_earnings` | Get earnings data or upcoming earnings calendar. 5-level fallback chain for reliable data. |
|
|
23
|
+
| Market Data | `get_unusual_activity` | Options flow — where big premium is moving, by ticker or market-wide |
|
|
24
|
+
| Market Data | `scan_stocks` | Scan stocks by IV rank, valuation, market cap, dividend, sector, signal, and earnings proximity |
|
|
25
|
+
| Portfolio | `list_portfolios` | List your portfolios with their IDs (discovery for the tools below) |
|
|
26
|
+
| Portfolio | `get_portfolio` | Get portfolio details, metrics, open trades, positions, and spreads |
|
|
27
|
+
| Portfolio | `get_positions` | Get stock positions (shares held) |
|
|
28
|
+
| Portfolio | `get_portfolio_analytics` | Track-record analytics — win rate, avg P&L, and what entry conditions are working |
|
|
29
|
+
| Portfolio | `get_morning_briefing` | Daily snapshot — open positions, unrealized P&L, expirations, and action items |
|
|
30
|
+
| Portfolio | `add_trade` | Add a trade (BUY/SELL, PUT/CALL) |
|
|
31
|
+
| Portfolio | `close_trade` | Close a trade with exit price |
|
|
32
|
+
| Portfolio | `add_spread` | Add a spread (12 types supported) |
|
|
33
|
+
| Portfolio | `close_spread` | Close a spread with exit price |
|
|
34
|
+
| Portfolio | `add_position` | Add a stock position |
|
|
35
|
+
| Portfolio | `refresh_prices` | Refresh live market prices |
|
|
36
|
+
| Strategy | `analyze_strategy` | Payoff analysis for any multi-leg strategy — max gain/loss, breakevens, net debit/credit, strategy name |
|
|
37
|
+
| Screening | `save_screener_config` | Save the current screener filter set as a named preset |
|
|
38
|
+
| Portfolio | `get_trade_dna` | Your trading-style profile from closed trades — sweet-spot delta/DTE/IV and best underlyings |
|
|
39
|
+
|
|
40
|
+
## Example prompts
|
|
41
|
+
|
|
42
|
+
- "What should I look at today?" → `list_portfolios`, then `get_morning_briefing(portfolio_id)`
|
|
43
|
+
- "Build a bull put spread on AAPL at 180/175 for Jul 17 and show me the payoff" → `analyze_strategy(legs=[...])`
|
|
44
|
+
- "Screen for CSPs the way I usually trade" → `get_trade_dna()`, then `screen_csp` with the sweet-spot delta/DTE
|
|
45
|
+
- "Save this screen as 'weekly income'" → `save_screener_config(name, config)`
|
|
46
|
+
|
|
47
|
+
## Resources (2)
|
|
48
|
+
|
|
49
|
+
- `remora://watchlists` — List all watchlists and tickers
|
|
50
|
+
- `remora://watchlist/{id}` — Get a specific watchlist
|
|
51
|
+
|
|
52
|
+
## Key Features
|
|
53
|
+
|
|
54
|
+
- **Filter rejection summaries** — Screeners explain WHY contracts were filtered out when results are empty
|
|
55
|
+
- **Progressive fallback** — `find_trades` widens DTE, tries all defined-risk strategies, and returns diagnostics with suggestions
|
|
56
|
+
- **5-level earnings fallback** — `get_earnings` chains multiple data sources for reliable coverage
|
|
57
|
+
- **Staleness timestamps** — All read tools show when data was last retrieved
|
|
58
|
+
|
|
59
|
+
## Setup
|
|
60
|
+
|
|
61
|
+
1. Generate an API key at your account page (Pro subscription required)
|
|
62
|
+
2. Install: `pip install heyremora`
|
|
63
|
+
3. Set environment variables (`REMORA_API_KEY`, `REMORA_API_URL`) and add to Claude Desktop config
|
|
64
|
+
|
|
65
|
+
See the full setup guide at `/docs/mcp-setup` on the Remora website.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "heyremora"
|
|
7
|
+
version = "0.5.1"
|
|
8
|
+
description = "MCP server for Remora — options screening, portfolio analytics, and strategy comparison via Claude Desktop"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp>=1.6,<2.0",
|
|
14
|
+
"httpx>=0.24,<1.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = ["pytest", "pytest-anyio", "anyio"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
heyremora = "heyremora.server:main"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/heyremora"]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""HTTP client for the Remora API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OTXClientError(Exception):
|
|
8
|
+
"""Raised on API errors with a user-friendly message."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OTXClient:
|
|
16
|
+
"""Async HTTP client wrapping the Remora REST API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_key: str, base_url: str):
|
|
19
|
+
self._client = httpx.AsyncClient(
|
|
20
|
+
base_url=base_url,
|
|
21
|
+
headers={"X-API-Key": api_key},
|
|
22
|
+
timeout=120.0,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def close(self):
|
|
26
|
+
await self._client.aclose()
|
|
27
|
+
|
|
28
|
+
async def _request(self, method: str, path: str, **kwargs) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
resp = await self._client.request(method, path, **kwargs)
|
|
31
|
+
except httpx.ConnectError:
|
|
32
|
+
raise OTXClientError(
|
|
33
|
+
"Cannot connect to Remora API. Check your REMORA_API_URL environment variable."
|
|
34
|
+
)
|
|
35
|
+
except httpx.TimeoutException:
|
|
36
|
+
raise OTXClientError("Request timed out. The API may be under heavy load — try again.")
|
|
37
|
+
|
|
38
|
+
if resp.status_code == 401:
|
|
39
|
+
raise OTXClientError(
|
|
40
|
+
"Invalid API key. Check your REMORA_API_KEY matches what was created at /account.",
|
|
41
|
+
status_code=401,
|
|
42
|
+
)
|
|
43
|
+
if resp.status_code == 403:
|
|
44
|
+
detail = resp.json().get("detail", "") if resp.headers.get("content-type", "").startswith("application/json") else ""
|
|
45
|
+
if "trial" in detail.lower() or "expired" in detail.lower():
|
|
46
|
+
raise OTXClientError(
|
|
47
|
+
"Trial expired. A Pro subscription is required. Manage at /account.",
|
|
48
|
+
status_code=403,
|
|
49
|
+
)
|
|
50
|
+
raise OTXClientError(
|
|
51
|
+
"API key inactive — Pro subscription required. Manage at /account.",
|
|
52
|
+
status_code=403,
|
|
53
|
+
)
|
|
54
|
+
if resp.status_code == 429:
|
|
55
|
+
raise OTXClientError(
|
|
56
|
+
"Rate limit exceeded. Please wait before making more requests.",
|
|
57
|
+
status_code=429,
|
|
58
|
+
)
|
|
59
|
+
if resp.status_code >= 400:
|
|
60
|
+
detail = ""
|
|
61
|
+
try:
|
|
62
|
+
detail = resp.json().get("detail", "")
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
raise OTXClientError(
|
|
66
|
+
f"API error ({resp.status_code}): {detail or resp.text[:200]}",
|
|
67
|
+
status_code=resp.status_code,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return resp.json()
|
|
71
|
+
|
|
72
|
+
async def _get(self, path: str, params: Optional[Dict] = None) -> Any:
|
|
73
|
+
return await self._request("GET", path, params=params)
|
|
74
|
+
|
|
75
|
+
async def _post(self, path: str, json: Optional[Dict] = None) -> Any:
|
|
76
|
+
return await self._request("POST", path, json=json)
|
|
77
|
+
|
|
78
|
+
# ── Screener endpoints ──
|
|
79
|
+
|
|
80
|
+
async def screen_options(self, body: Dict) -> Dict:
|
|
81
|
+
return await self._post("/api/screener/screen", json=body)
|
|
82
|
+
|
|
83
|
+
async def screen_long_calls(self, body: Dict) -> Dict:
|
|
84
|
+
return await self._post("/api/screener/long-calls", json=body)
|
|
85
|
+
|
|
86
|
+
async def screen_long_puts(self, body: Dict) -> Dict:
|
|
87
|
+
return await self._post("/api/screener/long-puts", json=body)
|
|
88
|
+
|
|
89
|
+
async def screen_spreads(self, body: Dict) -> Dict:
|
|
90
|
+
return await self._post("/api/spreads/screen", json=body)
|
|
91
|
+
|
|
92
|
+
async def find_trades(self, body: Dict) -> Dict:
|
|
93
|
+
return await self._post("/api/trade-finder/find", json=body)
|
|
94
|
+
|
|
95
|
+
# ── Portfolio endpoints ──
|
|
96
|
+
|
|
97
|
+
async def list_portfolios(self) -> Any:
|
|
98
|
+
return await self._get("/api/user/portfolios")
|
|
99
|
+
|
|
100
|
+
async def get_portfolio(self, portfolio_id: str) -> Dict:
|
|
101
|
+
return await self._get(f"/api/user/portfolios/{portfolio_id}")
|
|
102
|
+
|
|
103
|
+
async def get_positions(self, portfolio_id: str) -> Any:
|
|
104
|
+
return await self._get(f"/api/user/portfolios/{portfolio_id}/positions")
|
|
105
|
+
|
|
106
|
+
async def add_trade(self, portfolio_id: str, body: Dict) -> Dict:
|
|
107
|
+
return await self._post(f"/api/user/portfolios/{portfolio_id}/trades", json=body)
|
|
108
|
+
|
|
109
|
+
async def close_trade(self, portfolio_id: str, trade_id: str, body: Dict) -> Dict:
|
|
110
|
+
return await self._post(f"/api/user/portfolios/{portfolio_id}/trades/{trade_id}/close", json=body)
|
|
111
|
+
|
|
112
|
+
async def add_spread(self, portfolio_id: str, body: Dict) -> Dict:
|
|
113
|
+
return await self._post(f"/api/user/portfolios/{portfolio_id}/spreads", json=body)
|
|
114
|
+
|
|
115
|
+
async def close_spread(self, portfolio_id: str, spread_id: str, body: Dict) -> Dict:
|
|
116
|
+
return await self._post(f"/api/user/portfolios/{portfolio_id}/spreads/{spread_id}/close", json=body)
|
|
117
|
+
|
|
118
|
+
async def add_position(self, portfolio_id: str, body: Dict) -> Dict:
|
|
119
|
+
return await self._post(f"/api/user/portfolios/{portfolio_id}/positions", json=body)
|
|
120
|
+
|
|
121
|
+
async def refresh_prices(self, portfolio_id: str) -> Dict:
|
|
122
|
+
return await self._get(f"/api/user/portfolios/{portfolio_id}/trades/refresh-prices")
|
|
123
|
+
|
|
124
|
+
async def get_user_analytics(self, period: str = "all", portfolio_id: Optional[str] = None) -> Dict:
|
|
125
|
+
params: Dict[str, Any] = {"period": period}
|
|
126
|
+
if portfolio_id:
|
|
127
|
+
params["portfolio_id"] = portfolio_id
|
|
128
|
+
return await self._get("/api/user/portfolios/analytics/closed-trades", params=params)
|
|
129
|
+
|
|
130
|
+
# ── IV & Earnings ──
|
|
131
|
+
|
|
132
|
+
async def get_iv_rank(self, ticker: str) -> Dict:
|
|
133
|
+
return await self._get(f"/api/iv/{ticker}")
|
|
134
|
+
|
|
135
|
+
async def get_earnings_ticker(self, ticker: str) -> Dict:
|
|
136
|
+
return await self._get(f"/api/earnings/{ticker}")
|
|
137
|
+
|
|
138
|
+
async def get_earnings_calendar(self, within_days: int = 14) -> Any:
|
|
139
|
+
return await self._get("/api/earnings/calendar", params={"within_days": within_days})
|
|
140
|
+
|
|
141
|
+
# ── Technicals ──
|
|
142
|
+
|
|
143
|
+
async def get_technicals(self, ticker: str, force_refresh: bool = False) -> Dict:
|
|
144
|
+
params: Dict[str, Any] = {}
|
|
145
|
+
if force_refresh:
|
|
146
|
+
params["force_refresh"] = True
|
|
147
|
+
return await self._get(f"/api/technicals/{ticker}", params=params or None)
|
|
148
|
+
|
|
149
|
+
# ── Unusual options activity (flow) ──
|
|
150
|
+
|
|
151
|
+
async def get_options_flow(self, limit: int = 20) -> Dict:
|
|
152
|
+
return await self._get("/api/unusual-activity", params={"limit": limit})
|
|
153
|
+
|
|
154
|
+
async def get_ticker_flow(self, ticker: str) -> Dict:
|
|
155
|
+
return await self._get(f"/api/unusual-activity/ticker/{ticker}")
|
|
156
|
+
|
|
157
|
+
# ── Scanner ──
|
|
158
|
+
|
|
159
|
+
async def get_scanner_stocks(self, params: Optional[Dict] = None) -> Dict:
|
|
160
|
+
return await self._get("/api/scanner/stocks", params=params or None)
|
|
161
|
+
|
|
162
|
+
# ── Saved screeners & morning briefing ──
|
|
163
|
+
|
|
164
|
+
async def get_screener_configs(self) -> Any:
|
|
165
|
+
return await self._get("/api/screener-configs")
|
|
166
|
+
|
|
167
|
+
async def get_briefing(self, portfolio_id: str) -> Dict:
|
|
168
|
+
return await self._get(f"/api/notifications/briefing/preview/{portfolio_id}")
|
|
169
|
+
|
|
170
|
+
# ── Chain ──
|
|
171
|
+
|
|
172
|
+
async def get_chain(
|
|
173
|
+
self, ticker: str, expiration: str = None, option_type: str = None,
|
|
174
|
+
min_volume: int = 0, min_open_interest: int = 0, top: int = 200,
|
|
175
|
+
sort: str = "strike",
|
|
176
|
+
) -> Dict:
|
|
177
|
+
params: Dict[str, Any] = {"top": top, "sort": sort}
|
|
178
|
+
if expiration:
|
|
179
|
+
params["expiration"] = expiration
|
|
180
|
+
if option_type:
|
|
181
|
+
params["type"] = option_type
|
|
182
|
+
if min_volume:
|
|
183
|
+
params["min_volume"] = min_volume
|
|
184
|
+
if min_open_interest:
|
|
185
|
+
params["min_open_interest"] = min_open_interest
|
|
186
|
+
return await self._get(f"/api/chain/{ticker}", params=params)
|
|
187
|
+
|
|
188
|
+
async def get_option_quote(
|
|
189
|
+
self, ticker: str, expiration: str, strike: float, option_type: str,
|
|
190
|
+
) -> Dict:
|
|
191
|
+
return await self._get(f"/api/chain/{ticker}/quote", params={
|
|
192
|
+
"expiration": expiration,
|
|
193
|
+
"strike": strike,
|
|
194
|
+
"type": option_type,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
# ── Watchlists ──
|
|
198
|
+
|
|
199
|
+
async def get_watchlists(self) -> Any:
|
|
200
|
+
return await self._get("/api/watchlists/")
|
|
201
|
+
|
|
202
|
+
async def get_watchlist(self, watchlist_id: str) -> Dict:
|
|
203
|
+
return await self._get(f"/api/watchlists/{watchlist_id}")
|
|
204
|
+
|
|
205
|
+
async def analyze_strategy(self, body: Dict) -> Dict:
|
|
206
|
+
return await self._post("/api/strategy/analyze", json=body)
|
|
207
|
+
|
|
208
|
+
async def save_screener_config(self, body: Dict) -> Dict:
|
|
209
|
+
return await self._post("/api/screener-configs", json=body)
|
|
210
|
+
|
|
211
|
+
async def get_trade_dna(self) -> Dict:
|
|
212
|
+
return await self._get("/api/user/portfolios/analytics/trade-dna")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Configuration — reads REMORA_API_KEY and REMORA_API_URL from environment.
|
|
2
|
+
|
|
3
|
+
The legacy OTX_API_KEY / OTX_API_URL names are still accepted as a fallback so
|
|
4
|
+
existing setups keep working after the rebrand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_api_key() -> str:
|
|
12
|
+
key = os.environ.get("REMORA_API_KEY") or os.environ.get("OTX_API_KEY", "")
|
|
13
|
+
if not key:
|
|
14
|
+
print(
|
|
15
|
+
"ERROR: REMORA_API_KEY environment variable is required.\n"
|
|
16
|
+
"Generate one at https://heyremora.com/account (Pro subscription required).\n"
|
|
17
|
+
"Then set it: export REMORA_API_KEY=<your-key>",
|
|
18
|
+
file=sys.stderr,
|
|
19
|
+
)
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
return key
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_base_url() -> str:
|
|
25
|
+
url = os.environ.get("REMORA_API_URL") or os.environ.get("OTX_API_URL", "")
|
|
26
|
+
if not url:
|
|
27
|
+
print(
|
|
28
|
+
"ERROR: REMORA_API_URL environment variable is required.\n"
|
|
29
|
+
"Set it to the Remora API URL, e.g.:\n"
|
|
30
|
+
" export REMORA_API_URL=https://heyremora.com\n"
|
|
31
|
+
"For local development:\n"
|
|
32
|
+
" export REMORA_API_URL=http://localhost:8000",
|
|
33
|
+
file=sys.stderr,
|
|
34
|
+
)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
return url.rstrip("/")
|