Skip to content

Commit 2af1b01

Browse files
bearmugclaude
andauthored
feat: Add multi-asset screening tools (forex, crypto, ETF) (#18)
Implement three new MCP tools for screening different asset classes: - screen_forex: Screen 1,759+ forex pairs with technical indicators - screen_crypto: Screen 9,247+ cryptocurrencies with market data - screen_etf: Screen ETFs/funds with automatic type filtering Features: - Dedicated screening methods for each asset class - Proper caching and rate limiting per tool - Asset-specific default columns and sorting - Comprehensive test coverage (69 tests passing) Updates: - Added tool definitions and handlers to MCP server - Updated README with tool documentation and examples - Added 2 new tests for ETF screening validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5d60702 commit 2af1b01

4 files changed

Lines changed: 362 additions & 1 deletion

File tree

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,14 @@ Unlike hedge funds competing on speed and information access, you can compete on
6767

6868
**Built for research-driven investment discovery:**
6969

70-
- 🔍 **Systematic screening** - Find opportunities across stocks, forex, and crypto with advanced filters
70+
- 🔍 **Multi-asset screening** - Dedicated tools for stocks, forex, crypto, and ETFs with advanced filters
7171
- 📊 **75+ investment metrics** - Fundamental (valuation, margins, returns), technical (RSI, moving averages), and performance fields with TTM/FQ/FY variants
7272
- 🎯 **6 proven investment strategies** - Pre-configured screens for quality, value, dividend, momentum, and growth investing
7373
- 💡 **AI-powered exploration** - Natural language queries through Claude ("Find undervalued companies with strong balance sheets")
7474
- 💰 **Deep financial analysis** - EV, EV/EBIT, EV/EBITDA, PEG, gross/operating margins, ROIC, ROA, ROE
7575
-**Research-optimized** - Minimal (7 fields) for quick scans vs extended (35 fields) for comprehensive analysis
7676
- 🏦 **Exchange filtering** - Focus on NASDAQ, NYSE, CBOE, or primary listings only
77+
- 🌍 **Global coverage** - Screen stocks across multiple markets (America, Europe, Asia) and 9,000+ cryptocurrencies
7778

7879
## Installation
7980

@@ -245,6 +246,44 @@ See **[Preset Strategies Guide](docs/presets.md)** for detailed criteria and usa
245246

246247
List all available preset strategies.
247248

249+
### `screen_forex`
250+
251+
Screen forex pairs based on technical criteria.
252+
253+
**Parameters:**
254+
- `filters` - Array of filter conditions (field, operator, value)
255+
- `sort_by` - Field to sort by (default: `"volume"`)
256+
- `sort_order` - `"asc"` or `"desc"` (default: `"desc"`)
257+
- `limit` - Number of results (1-200, default: 20)
258+
259+
**Example fields:** `close`, `volume`, `change`, `RSI`, `SMA50`, `SMA200`, `Volatility.M`
260+
261+
### `screen_crypto`
262+
263+
Screen cryptocurrencies based on technical and market criteria.
264+
265+
**Parameters:**
266+
- `filters` - Array of filter conditions (field, operator, value)
267+
- `sort_by` - Field to sort by (default: `"market_cap_basic"`)
268+
- `sort_order` - `"asc"` or `"desc"` (default: `"desc"`)
269+
- `limit` - Number of results (1-200, default: 20)
270+
271+
**Example fields:** `close`, `market_cap_basic`, `volume`, `change`, `Perf.1M`, `Perf.3M`, `Perf.Y`
272+
273+
### `screen_etf`
274+
275+
Screen ETFs (Exchange-Traded Funds) based on performance and technical criteria.
276+
277+
**Parameters:**
278+
- `filters` - Array of filter conditions (field, operator, value)
279+
- `markets` - Markets to scan (default: `["america"]`)
280+
- `sort_by` - Field to sort by (default: `"market_cap_basic"`)
281+
- `sort_order` - `"asc"` or `"desc"` (default: `"desc"`)
282+
- `limit` - Number of results (1-200, default: 20)
283+
- `columns` - Optional array of columns to return
284+
285+
**Example fields:** `close`, `volume`, `change`, `Perf.1M`, `Perf.Y`, `RSI`, `beta_1_year`
286+
248287
## Key Features at a Glance
249288

250289
### 75+ Available Fields

src/index.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,166 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
158158
properties: {},
159159
},
160160
},
161+
{
162+
name: "screen_forex",
163+
description:
164+
"Screen forex pairs based on technical criteria. Returns forex pairs matching the specified filters.",
165+
inputSchema: {
166+
type: "object",
167+
properties: {
168+
filters: {
169+
type: "array",
170+
description: "Array of filter conditions to apply",
171+
items: {
172+
type: "object",
173+
properties: {
174+
field: {
175+
type: "string",
176+
description:
177+
"Field name to filter (e.g., 'close', 'volume', 'RSI', 'change')",
178+
},
179+
operator: {
180+
type: "string",
181+
description:
182+
"Comparison operator: greater, less, greater_or_equal, less_or_equal, equal, in_range, etc.",
183+
},
184+
value: {
185+
description:
186+
"Value to compare against (number, string for field comparison, or array [min, max] for in_range)",
187+
},
188+
},
189+
required: ["field", "operator", "value"],
190+
},
191+
},
192+
sort_by: {
193+
type: "string",
194+
description: "Field to sort results by. Default: 'volume'",
195+
},
196+
sort_order: {
197+
type: "string",
198+
enum: ["asc", "desc"],
199+
description: "Sort order. Default: 'desc'",
200+
},
201+
limit: {
202+
type: "number",
203+
description: "Number of results to return (1-200). Default: 20",
204+
minimum: 1,
205+
maximum: 200,
206+
},
207+
},
208+
required: ["filters"],
209+
},
210+
},
211+
{
212+
name: "screen_crypto",
213+
description:
214+
"Screen cryptocurrencies based on technical and market criteria. Returns cryptocurrencies matching the specified filters.",
215+
inputSchema: {
216+
type: "object",
217+
properties: {
218+
filters: {
219+
type: "array",
220+
description: "Array of filter conditions to apply",
221+
items: {
222+
type: "object",
223+
properties: {
224+
field: {
225+
type: "string",
226+
description:
227+
"Field name to filter (e.g., 'close', 'market_cap_basic', 'volume', 'change')",
228+
},
229+
operator: {
230+
type: "string",
231+
description:
232+
"Comparison operator: greater, less, greater_or_equal, less_or_equal, equal, in_range, etc.",
233+
},
234+
value: {
235+
description:
236+
"Value to compare against (number, string for field comparison, or array [min, max] for in_range)",
237+
},
238+
},
239+
required: ["field", "operator", "value"],
240+
},
241+
},
242+
sort_by: {
243+
type: "string",
244+
description: "Field to sort results by. Default: 'market_cap_basic'",
245+
},
246+
sort_order: {
247+
type: "string",
248+
enum: ["asc", "desc"],
249+
description: "Sort order. Default: 'desc'",
250+
},
251+
limit: {
252+
type: "number",
253+
description: "Number of results to return (1-200). Default: 20",
254+
minimum: 1,
255+
maximum: 200,
256+
},
257+
},
258+
required: ["filters"],
259+
},
260+
},
261+
{
262+
name: "screen_etf",
263+
description:
264+
"Screen ETFs (Exchange-Traded Funds) based on performance and technical criteria. Returns ETFs matching the specified filters.",
265+
inputSchema: {
266+
type: "object",
267+
properties: {
268+
filters: {
269+
type: "array",
270+
description: "Array of filter conditions to apply",
271+
items: {
272+
type: "object",
273+
properties: {
274+
field: {
275+
type: "string",
276+
description:
277+
"Field name to filter (e.g., 'close', 'volume', 'change', 'Perf.1M')",
278+
},
279+
operator: {
280+
type: "string",
281+
description:
282+
"Comparison operator: greater, less, greater_or_equal, less_or_equal, equal, in_range, etc.",
283+
},
284+
value: {
285+
description:
286+
"Value to compare against (number, string for field comparison, or array [min, max] for in_range)",
287+
},
288+
},
289+
required: ["field", "operator", "value"],
290+
},
291+
},
292+
markets: {
293+
type: "array",
294+
items: { type: "string" },
295+
description: "Markets to scan (e.g., ['america']). Default: ['america']",
296+
},
297+
sort_by: {
298+
type: "string",
299+
description: "Field to sort results by. Default: 'market_cap_basic'",
300+
},
301+
sort_order: {
302+
type: "string",
303+
enum: ["asc", "desc"],
304+
description: "Sort order. Default: 'desc'",
305+
},
306+
limit: {
307+
type: "number",
308+
description: "Number of results to return (1-200). Default: 20",
309+
minimum: 1,
310+
maximum: 200,
311+
},
312+
columns: {
313+
type: "array",
314+
items: { type: "string" },
315+
description: "Optional: specific columns to include in results. If not provided, uses minimal default columns.",
316+
},
317+
},
318+
required: ["filters"],
319+
},
320+
},
161321
],
162322
};
163323
});
@@ -227,6 +387,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
227387
};
228388
}
229389

390+
case "screen_forex": {
391+
const result = await screenTool.screenForex(args as any);
392+
return {
393+
content: [
394+
{
395+
type: "text",
396+
text: JSON.stringify(result, null, 2),
397+
},
398+
],
399+
};
400+
}
401+
402+
case "screen_crypto": {
403+
const result = await screenTool.screenCrypto(args as any);
404+
return {
405+
content: [
406+
{
407+
type: "text",
408+
text: JSON.stringify(result, null, 2),
409+
},
410+
],
411+
};
412+
}
413+
414+
case "screen_etf": {
415+
const result = await screenTool.screenETF(args as any);
416+
return {
417+
content: [
418+
{
419+
type: "text",
420+
text: JSON.stringify(result, null, 2),
421+
},
422+
],
423+
};
424+
}
425+
230426
default:
231427
return {
232428
content: [

src/tests/screen.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,53 @@ describe("ScreenTool - Filter Validation", () => {
329329
}
330330
);
331331
});
332+
333+
it("should validate filters in screenETF", async () => {
334+
await assert.rejects(
335+
async () => {
336+
await screenTool.screenETF({
337+
filters: [
338+
{
339+
field: "volume",
340+
operator: "invalid_operator",
341+
value: 1000000,
342+
},
343+
],
344+
});
345+
},
346+
{
347+
message: /Unknown operator: invalid_operator/,
348+
}
349+
);
350+
});
351+
352+
it("should accept valid filters in screenETF", async () => {
353+
// Mock successful response
354+
(mockClient.scanStocks as any).mock.mockImplementation(async (request: any) => {
355+
// Verify ETF-specific type filter is added
356+
const typeFilter = request.filter.find((f: any) => f.left === "type");
357+
assert.ok(typeFilter, "Should have type filter");
358+
assert.strictEqual(typeFilter.operation, "equal");
359+
assert.strictEqual(typeFilter.right, "fund");
360+
361+
return {
362+
totalCount: 0,
363+
data: [],
364+
};
365+
});
366+
367+
await assert.doesNotReject(async () => {
368+
await screenTool.screenETF({
369+
filters: [
370+
{
371+
field: "volume",
372+
operator: "greater",
373+
value: 1000000,
374+
},
375+
],
376+
});
377+
});
378+
});
332379
});
333380

334381
describe("Valid filter conversion", () => {

0 commit comments

Comments
 (0)