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.
@@ -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`.
@@ -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,3 @@
1
+ """Remora MCP Server — options screening via Claude Desktop."""
2
+
3
+ __version__ = "0.3.0"
@@ -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("/")