@scriptmasterlabs/mcp-x402 2.0.2 → 2.1.0

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.
Files changed (93) hide show
  1. package/.well-known/agentcard.json +34 -34
  2. package/.well-known/ai.txt +32 -0
  3. package/CONTRIBUTING.md +76 -76
  4. package/LICENSE +21 -21
  5. package/README.md +304 -304
  6. package/agents.json +81 -67
  7. package/ai/faq.json +74 -0
  8. package/ai/summary.json +157 -0
  9. package/dist/lib/chains/base.d.ts.map +1 -1
  10. package/dist/lib/chains/base.js +2 -0
  11. package/dist/lib/chains/base.js.map +1 -1
  12. package/dist/lib/credit/bureau.d.ts +7 -1
  13. package/dist/lib/credit/bureau.d.ts.map +1 -1
  14. package/dist/lib/credit/bureau.js +40 -10
  15. package/dist/lib/credit/bureau.js.map +1 -1
  16. package/dist/server/index.js +128 -5
  17. package/dist/server/index.js.map +1 -1
  18. package/llms.txt +170 -70
  19. package/package.json +78 -78
  20. package/server.json +52 -48
  21. package/.env.example +0 -35
  22. package/.github/workflows/ci.yml +0 -59
  23. package/.github/workflows/keepalive.yml +0 -31
  24. package/Dockerfile +0 -19
  25. package/docker-compose.yml +0 -50
  26. package/mcp-publisher.exe +0 -0
  27. package/render.yaml +0 -39
  28. package/sdk/mcp-x402-sdk/package.json +0 -18
  29. package/sdk/mcp-x402-sdk/src/index.ts +0 -118
  30. package/sdk/mcp-x402-sdk/tsconfig.json +0 -14
  31. package/services/backtest_service.py +0 -176
  32. package/src/lib/chains/base.ts +0 -77
  33. package/src/lib/chains/solana.ts +0 -59
  34. package/src/lib/chains/xrpl.ts +0 -63
  35. package/src/lib/credit/bureau.ts +0 -65
  36. package/src/lib/sml-api/agentcard.ts +0 -40
  37. package/src/lib/sml-api/backtest.ts +0 -47
  38. package/src/lib/sml-api/brokers.ts +0 -160
  39. package/src/lib/sml-api/copytrader.ts +0 -33
  40. package/src/lib/sml-api/crawl.ts +0 -44
  41. package/src/lib/sml-api/echo.ts +0 -28
  42. package/src/lib/sml-api/forge.ts +0 -33
  43. package/src/lib/sml-api/ftd.ts +0 -53
  44. package/src/lib/sml-api/ghost.ts +0 -35
  45. package/src/lib/sml-api/launchpad.ts +0 -43
  46. package/src/lib/sml-api/leviathan.ts +0 -49
  47. package/src/lib/sml-api/nexus.ts +0 -50
  48. package/src/lib/sml-api/proof402.ts +0 -27
  49. package/src/lib/sml-api/rails.ts +0 -34
  50. package/src/lib/sml-api/shadow.ts +0 -35
  51. package/src/lib/sml-api/squeezeos.ts +0 -95
  52. package/src/lib/sml-api/xdeo.ts +0 -40
  53. package/src/lib/sml-api/xmit.ts +0 -40
  54. package/src/server/health.ts +0 -52
  55. package/src/server/index.ts +0 -213
  56. package/src/server/payments/ap2.ts +0 -101
  57. package/src/server/payments/receipt.ts +0 -85
  58. package/src/server/payments/router.ts +0 -110
  59. package/src/server/payments/wallet.ts +0 -123
  60. package/src/server/payments/x402.ts +0 -177
  61. package/src/server/registry/catalog.ts +0 -61
  62. package/src/server/registry/discovery.ts +0 -39
  63. package/src/server/registry/pricing.ts +0 -133
  64. package/src/server/security/acl.ts +0 -42
  65. package/src/server/security/audit.ts +0 -94
  66. package/src/server/security/rate-limit.ts +0 -84
  67. package/src/server/security/sandbox.ts +0 -40
  68. package/src/server/tools/agentcard.ts +0 -134
  69. package/src/server/tools/backtest.ts +0 -119
  70. package/src/server/tools/brokers.ts +0 -250
  71. package/src/server/tools/copytrader.ts +0 -104
  72. package/src/server/tools/crawl.ts +0 -70
  73. package/src/server/tools/discovery.ts +0 -202
  74. package/src/server/tools/echo.ts +0 -58
  75. package/src/server/tools/forge.ts +0 -87
  76. package/src/server/tools/ftd.ts +0 -88
  77. package/src/server/tools/ghost.ts +0 -93
  78. package/src/server/tools/index.ts +0 -42
  79. package/src/server/tools/launchpad.ts +0 -173
  80. package/src/server/tools/leviathan.ts +0 -81
  81. package/src/server/tools/nexus.ts +0 -76
  82. package/src/server/tools/proof402.ts +0 -87
  83. package/src/server/tools/rails.ts +0 -92
  84. package/src/server/tools/shadow.ts +0 -128
  85. package/src/server/tools/squeezeos.ts +0 -312
  86. package/src/server/tools/xdeo.ts +0 -67
  87. package/src/server/tools/xmit.ts +0 -68
  88. package/tests/integration/e2e.test.ts +0 -51
  89. package/tests/unit/payments.test.ts +0 -49
  90. package/tests/unit/security.test.ts +0 -92
  91. package/tests/unit/tools.test.ts +0 -42
  92. package/tsconfig.json +0 -21
  93. package/vitest.config.ts +0 -20
package/server.json CHANGED
@@ -1,48 +1,52 @@
1
- {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
- "name": "io.github.Timwal78/mcp-x402",
4
- "description": "The x402 Amazon: 43+ MCP tools, pay-per-call. SEC, squeeze, options, FTD, XRPL/Base payments.",
5
- "repository": {
6
- "url": "https://github.com/timwal78/SML_Portfolio",
7
- "source": "github"
8
- },
9
- "version": "2.0.2",
10
- "packages": [
11
- {
12
- "registryType": "npm",
13
- "identifier": "@scriptmasterlabs/mcp-x402",
14
- "version": "2.0.2",
15
- "runtimeHint": "node",
16
- "transport": {
17
- "type": "stdio"
18
- },
19
- "environmentVariables": [
20
- {
21
- "name": "MCP_TRANSPORT",
22
- "description": "Transport mode. Use 'sse' for HTTP server mode.",
23
- "isRequired": false
24
- },
25
- {
26
- "name": "SML_PAYMENT_RECEIVER",
27
- "description": "USDC Base address to receive payments from agent tool calls.",
28
- "isRequired": false
29
- },
30
- {
31
- "name": "WALLET_SEED",
32
- "description": "BIP-39 mnemonic for the server wallet. Auto-generated if not set.",
33
- "isRequired": false
34
- }
35
- ]
36
- }
37
- ],
38
- "remotes": [
39
- {
40
- "type": "sse",
41
- "url": "https://mcp-x402.onrender.com/sse"
42
- },
43
- {
44
- "type": "streamable-http",
45
- "url": "https://mcp-x402.onrender.com/mcp"
46
- }
47
- ]
48
- }
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.Timwal78/mcp-x402",
4
+ "title": "mcp-x402 — The x402 Amazon for AI Agents",
5
+ "description": "51 pay-per-call MCP tools: market intel, SEC, XRPL payments, copy-trading. USDC/RLUSD. No keys.",
6
+ "version": "2.1.0",
7
+ "repository": {
8
+ "url": "https://github.com/timwal78/SML_Portfolio",
9
+ "source": "github"
10
+ },
11
+ "homepage": "https://scriptmasterlabs.com",
12
+ "license": "MIT",
13
+ "packages": [
14
+ {
15
+ "registryType": "npm",
16
+ "identifier": "@scriptmasterlabs/mcp-x402",
17
+ "version": "2.1.0",
18
+ "runtimeHint": "node",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "MCP_TRANSPORT",
25
+ "description": "Transport mode. Omit for stdio (default). Set to 'sse' for HTTP server mode.",
26
+ "isRequired": false
27
+ },
28
+ {
29
+ "name": "SML_PAYMENT_RECEIVER",
30
+ "description": "Optional USDC Base address to receive payments from agent tool calls.",
31
+ "isRequired": false
32
+ },
33
+ {
34
+ "name": "WALLET_SEED",
35
+ "description": "BIP-39 mnemonic for the server wallet. Auto-generated if not set.",
36
+ "isRequired": false
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "remotes": [
42
+ {
43
+ "type": "streamable-http",
44
+ "url": "https://mcp-x402.onrender.com/mcp"
45
+ },
46
+ {
47
+ "type": "sse",
48
+ "url": "https://mcp-x402.onrender.com/sse"
49
+ }
50
+ ],
51
+ "keywords": ["x402", "micropayments", "usdc", "rlusd", "xrpl", "solana", "base", "squeeze", "options", "sec-filings", "ftd", "market-intelligence", "pay-per-call", "autonomous-agents"]
52
+ }
package/.env.example DELETED
@@ -1,35 +0,0 @@
1
- # === TRANSPORT ===
2
- # stdio (default, for Claude Code) or sse (remote/Cursor)
3
- MCP_TRANSPORT=stdio
4
- MCP_SSE_PORT=3402
5
-
6
- # === SML API ===
7
- # Base URL for ScriptMasterLabs APIs
8
- SML_API_BASE=https://api.scriptmasterlabs.com
9
- SML_MTLS_CERT_PATH=./certs/client.crt
10
- SML_MTLS_KEY_PATH=./certs/client.key
11
- SML_MTLS_CA_PATH=./certs/sml-ca.crt
12
-
13
- # === WALLET (stored in OS keychain — env only for CI/testnet) ===
14
- # NEVER use in production — use OS keychain instead
15
- # CI_WALLET_SEED=your-bip39-mnemonic-here
16
-
17
- # === CHAINS ===
18
- BASE_RPC_URL=https://mainnet.base.org
19
- BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
20
- XRPL_RPC_URL=wss://xrplcluster.com
21
- SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
22
-
23
- # === SPEND LIMITS ===
24
- DAILY_SPEND_CAP_USD=50
25
- AUTO_APPROVE_THRESHOLD_USD=1
26
- PRICE_CACHE_TTL_MS=60000
27
-
28
- # === AUDIT ===
29
- AUDIT_LOG_PATH=./audit.log
30
-
31
- # === CREDIT BUREAU ===
32
- MIN_CREDIT_SCORE=300
33
-
34
- # === TESTNET ===
35
- TESTNET=false
@@ -1,59 +0,0 @@
1
- name: mcp-x402 CI
2
-
3
- on:
4
- push:
5
- paths:
6
- - 'mcp-x402/**'
7
- pull_request:
8
- paths:
9
- - 'mcp-x402/**'
10
-
11
- defaults:
12
- run:
13
- working-directory: mcp-x402
14
-
15
- jobs:
16
- test:
17
- runs-on: ubuntu-latest
18
- steps:
19
- - uses: actions/checkout@v4
20
-
21
- - uses: actions/setup-node@v4
22
- with:
23
- node-version: '22'
24
- cache: 'npm'
25
- cache-dependency-path: mcp-x402/package-lock.json
26
-
27
- - name: Install dependencies
28
- run: npm ci --ignore-scripts
29
-
30
- - name: Type check
31
- run: npm run typecheck
32
-
33
- - name: Unit tests
34
- run: npm run test:unit
35
-
36
- - name: Upload coverage
37
- uses: codecov/codecov-action@v4
38
- with:
39
- directory: mcp-x402/coverage
40
- flags: mcp-x402
41
- continue-on-error: true
42
-
43
- build:
44
- runs-on: ubuntu-latest
45
- steps:
46
- - uses: actions/checkout@v4
47
-
48
- - uses: actions/setup-node@v4
49
- with:
50
- node-version: '22'
51
-
52
- - name: Install dependencies
53
- run: npm ci --ignore-scripts
54
-
55
- - name: Build
56
- run: npm run build
57
-
58
- - name: Docker build
59
- run: docker build -t mcp-x402:ci .
@@ -1,31 +0,0 @@
1
- name: mcp-x402 Keepalive
2
-
3
- # Pings the SSE health endpoint every 14 minutes to prevent cold starts
4
- # on free-tier Render instances (which sleep after 15 minutes of inactivity).
5
- on:
6
- schedule:
7
- - cron: '*/14 * * * *' # every 14 minutes, 24/7
8
- workflow_dispatch: # allow manual trigger
9
-
10
- jobs:
11
- ping:
12
- runs-on: ubuntu-latest
13
- timeout-minutes: 2
14
- steps:
15
- - name: Ping mcp-x402 health endpoint
16
- env:
17
- MCP_X402_URL: ${{ secrets.MCP_X402_URL }}
18
- run: |
19
- URL="${MCP_X402_URL:-https://mcp-x402.scriptmasterlabs.com}"
20
- STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "${URL}/health")
21
- echo "Health check HTTP ${STATUS} — ${URL}/health"
22
- if [ "$STATUS" != "200" ]; then
23
- echo "::warning::Health check returned HTTP ${STATUS}. Service may be degraded."
24
- fi
25
-
26
- - name: Ping SqueezeOS keepalive (ecosystem dependency)
27
- run: |
28
- curl -s --max-time 30 https://squeezeos-api.onrender.com/api/status > /dev/null || true
29
- curl -s --max-time 30 https://four02proof.onrender.com/health > /dev/null || true
30
- curl -s --max-time 30 https://ghost-layer.onrender.com/health > /dev/null || true
31
- echo "Ecosystem keepalives sent."
package/Dockerfile DELETED
@@ -1,19 +0,0 @@
1
- FROM node:22-alpine AS builder
2
- WORKDIR /app
3
- COPY package*.json ./
4
- RUN npm ci --ignore-scripts
5
- COPY tsconfig.json ./
6
- COPY src/ ./src/
7
- RUN npm run build
8
-
9
- FROM node:22-alpine AS runner
10
- WORKDIR /app
11
- RUN apk add --no-cache dbus libsecret
12
- COPY package*.json ./
13
- RUN npm ci --omit=dev --ignore-scripts
14
- COPY --from=builder /app/dist ./dist
15
- COPY agents.json llms.txt .well-known/ ./
16
- EXPOSE 3402
17
- ENV NODE_ENV=production
18
- USER node
19
- CMD ["node", "dist/server/index.js"]
@@ -1,50 +0,0 @@
1
- version: '3.9'
2
-
3
- services:
4
- mcp-x402:
5
- build:
6
- context: .
7
- dockerfile: Dockerfile
8
- image: mcp-x402:latest
9
- container_name: mcp-x402
10
- restart: always # Auto-restart on crash, reboot, or OOM
11
- environment:
12
- NODE_ENV: production
13
- MCP_TRANSPORT: sse
14
- MCP_SSE_PORT: "3402"
15
- DAILY_SPEND_CAP_USD: "50"
16
- AUTO_APPROVE_THRESHOLD_USD: "1"
17
- PRICE_CACHE_TTL_MS: "60000"
18
- PROOF402_URL: https://four02proof.onrender.com
19
- TESTNET: "false"
20
- # Secrets — supply via .env file or secrets manager; NEVER hardcode
21
- # SML_API_BASE:
22
- # BASE_RPC_URL:
23
- # XRPL_RPC_URL:
24
- # SOLANA_RPC_URL:
25
- ports:
26
- - "3402:3402"
27
- volumes:
28
- - ./certs:/app/certs:ro # mTLS certs (read-only)
29
- - audit_logs:/app/audit_logs # Persistent audit log volume
30
- healthcheck:
31
- test: ["CMD", "wget", "-qO-", "http://localhost:3402/health"]
32
- interval: 30s
33
- timeout: 10s
34
- retries: 3
35
- start_period: 20s
36
- logging:
37
- driver: json-file
38
- options:
39
- max-size: "50m"
40
- max-file: "7"
41
- deploy:
42
- resources:
43
- limits:
44
- memory: 512m
45
- reservations:
46
- memory: 128m
47
-
48
- volumes:
49
- audit_logs:
50
- driver: local
package/mcp-publisher.exe DELETED
Binary file
package/render.yaml DELETED
@@ -1,39 +0,0 @@
1
- services:
2
- - type: web
3
- name: mcp-x402
4
- runtime: docker
5
- dockerfilePath: ./mcp-x402/Dockerfile
6
- dockerContext: ./mcp-x402
7
- plan: starter
8
- region: oregon
9
- healthCheckPath: /health
10
- envVars:
11
- - key: NODE_ENV
12
- value: production
13
- - key: MCP_TRANSPORT
14
- value: sse
15
- - key: MCP_SSE_PORT
16
- value: 3402
17
- - key: SML_API_BASE
18
- sync: false
19
- - key: WALLET_SEED
20
- sync: false
21
- - key: BASE_RPC_URL
22
- sync: false
23
- - key: XRPL_RPC_URL
24
- sync: false
25
- - key: SOLANA_RPC_URL
26
- sync: false
27
- - key: DAILY_SPEND_CAP_USD
28
- value: "50"
29
- - key: AUTO_APPROVE_THRESHOLD_USD
30
- value: "1"
31
- - key: PRICE_CACHE_TTL_MS
32
- value: "60000"
33
- - key: PROOF402_URL
34
- value: https://four02proof.onrender.com
35
- - key: TESTNET
36
- value: "false"
37
- autoDeploy: true
38
- # Render restarts the service automatically on crash
39
- # healthCheckPath triggers restart if /health returns non-2xx
@@ -1,18 +0,0 @@
1
- {
2
- "name": "@scriptmasterlabs/mcp-x402-sdk",
3
- "version": "1.0.0",
4
- "description": "5-line drop-in x402 payment wrapper for any MCP server author.",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "scripts": {
8
- "build": "tsc",
9
- "prepublishOnly": "npm run build"
10
- },
11
- "keywords": ["mcp", "x402", "autonomous-payments", "ai-agents"],
12
- "author": "ScriptMasterLabs",
13
- "license": "MIT",
14
- "peerDependencies": {
15
- "@modelcontextprotocol/sdk": ">=1.0.0",
16
- "zod": ">=3.0.0"
17
- }
18
- }
@@ -1,118 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- export type SupportedCurrency = 'USDC' | 'RLUSD';
4
- export type SupportedChain = 'base' | 'xrpl' | 'solana';
5
-
6
- export interface X402PaymentConfig<TInput extends z.ZodTypeAny> {
7
- price: string;
8
- currency?: SupportedCurrency;
9
- chain?: SupportedChain;
10
- inputSchema: TInput;
11
- handler: (input: z.infer<TInput>, receipt: PaymentReceipt) => Promise<ToolResult>;
12
- }
13
-
14
- export interface PaymentReceipt {
15
- receipt_id: string;
16
- tx_hash: string;
17
- chain: string;
18
- amount_paid: string;
19
- currency: string;
20
- timestamp: number;
21
- }
22
-
23
- export interface ToolResult {
24
- content: Array<{ type: 'text'; text: string }>;
25
- isError?: boolean;
26
- }
27
-
28
- const PROOF402_URL = process.env['PROOF402_URL'] ?? 'https://four02proof.onrender.com';
29
-
30
- /**
31
- * x402Payment — 5-line drop-in for any MCP server author.
32
- *
33
- * @example
34
- * server.tool('my_tool', schema, x402Payment({
35
- * price: '0.01',
36
- * currency: 'USDC',
37
- * inputSchema: MyInputSchema,
38
- * handler: async (input, receipt) => {
39
- * return { content: [{ type: 'text', text: JSON.stringify({ result: 'data', receipt }) }] };
40
- * },
41
- * }));
42
- */
43
- export function x402Payment<TInput extends z.ZodTypeAny>(
44
- config: X402PaymentConfig<TInput>,
45
- ): (rawArgs: unknown) => Promise<ToolResult> {
46
- return async (rawArgs: unknown): Promise<ToolResult> => {
47
- const parsed = config.inputSchema.safeParse(rawArgs);
48
- if (!parsed.success) {
49
- return {
50
- content: [{ type: 'text', text: JSON.stringify({ error: 'validation_error', issues: parsed.error.issues }) }],
51
- isError: true,
52
- };
53
- }
54
-
55
- const args = parsed.data as z.infer<TInput>;
56
- const walletAddress = (args as Record<string, unknown>)['wallet_address'] as string | undefined;
57
-
58
- let receipt: PaymentReceipt;
59
- try {
60
- receipt = await processPayment({
61
- price: config.price,
62
- currency: config.currency ?? 'USDC',
63
- chain: config.chain,
64
- walletAddress,
65
- });
66
- } catch (err) {
67
- return {
68
- content: [{ type: 'text', text: JSON.stringify({ error: 'payment_failed', message: String(err) }) }],
69
- isError: true,
70
- };
71
- }
72
-
73
- return config.handler(args, receipt);
74
- };
75
- }
76
-
77
- async function processPayment(params: {
78
- price: string;
79
- currency: SupportedCurrency;
80
- chain?: SupportedChain;
81
- walletAddress?: string;
82
- }): Promise<PaymentReceipt> {
83
- // Delegates to the 402Proof payment endpoint
84
- const res = await fetch(`${PROOF402_URL}/v1/pay`, {
85
- method: 'POST',
86
- headers: { 'Content-Type': 'application/json' },
87
- body: JSON.stringify({
88
- amount: params.price,
89
- currency: params.currency,
90
- chain: params.chain ?? 'base',
91
- wallet: params.walletAddress,
92
- }),
93
- signal: AbortSignal.timeout(15_000),
94
- });
95
-
96
- if (!res.ok) {
97
- throw new Error(`Payment failed: HTTP ${res.status}`);
98
- }
99
-
100
- const body = (await res.json()) as {
101
- receipt_id: string;
102
- tx_hash: string;
103
- chain: string;
104
- amount: string;
105
- currency: string;
106
- };
107
-
108
- return {
109
- receipt_id: body.receipt_id,
110
- tx_hash: body.tx_hash,
111
- chain: body.chain,
112
- amount_paid: body.amount,
113
- currency: body.currency,
114
- timestamp: Date.now(),
115
- };
116
- }
117
-
118
- export { x402Payment as default };
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true
12
- },
13
- "include": ["src/**/*"]
14
- }
@@ -1,176 +0,0 @@
1
- """
2
- Backtest microservice — wraps backtester-mcp + yfinance
3
- POST /backtest { ticker, strategy_signals, lookback_days, fees, slippage }
4
- POST /validate { ticker, lookback_days, train_ratio } — walk-forward split
5
- GET /health
6
- """
7
-
8
- from __future__ import annotations
9
- import os
10
- import json
11
- import numpy as np
12
- from datetime import datetime, timedelta
13
- from typing import Any
14
-
15
- from flask import Flask, request, jsonify
16
- import backtester_mcp as bt
17
-
18
- # yfinance is the free price source — no API key required
19
- try:
20
- import yfinance as yf
21
- YF_AVAILABLE = True
22
- except ImportError:
23
- YF_AVAILABLE = False
24
-
25
- app = Flask(__name__)
26
-
27
- # ── helpers ──────────────────────────────────────────────────────────────────
28
-
29
- def _fetch_prices(ticker: str, days: int) -> np.ndarray:
30
- if not YF_AVAILABLE:
31
- raise RuntimeError("yfinance not installed")
32
- end = datetime.utcnow()
33
- start = end - timedelta(days=days + 30) # buffer for weekends/holidays
34
- df = yf.download(ticker, start=start.strftime("%Y-%m-%d"),
35
- end=end.strftime("%Y-%m-%d"), progress=False, auto_adjust=True)
36
- if df.empty:
37
- raise ValueError(f"No price data for {ticker}")
38
- return df["Close"].dropna().values[-days:]
39
-
40
-
41
- def _momentum_signals(prices: np.ndarray, window: int = 10, threshold: float = 0.001) -> np.ndarray:
42
- """Default long-only momentum signal for validation."""
43
- returns = np.diff(np.log(prices))
44
- mom = np.convolve(returns, np.ones(window) / window, mode="same")
45
- signals = np.zeros(len(prices))
46
- signals[1:] = np.where(mom[:-1] > threshold, 1, 0)
47
- return signals
48
-
49
-
50
- def _run_backtest(prices: np.ndarray, signals: np.ndarray,
51
- fees: float, slippage: float) -> dict[str, Any]:
52
- result = bt.backtest(prices, signals, fees=fees, slippage=slippage)
53
- m = result.metrics
54
- return {
55
- "sharpe": round(m["sharpe"], 3),
56
- "sortino": round(m["sortino"], 3),
57
- "cagr": round(m["cagr"], 4),
58
- "total_return": round(m["total_return"], 4),
59
- "max_drawdown": round(m["max_drawdown"], 4),
60
- "max_drawdown_duration_days": int(m["max_drawdown_duration"]),
61
- "win_rate": round(m["win_rate"], 4),
62
- "profit_factor": round(m["profit_factor"], 3),
63
- "calmar": round(m["calmar"], 3),
64
- "volatility": round(m["volatility"], 4),
65
- "num_trades": int(m["num_trades"]),
66
- }
67
-
68
-
69
- # ── routes ───────────────────────────────────────────────────────────────────
70
-
71
- @app.get("/health")
72
- def health():
73
- return jsonify({
74
- "status": "ok",
75
- "engine": f"backtester-mcp v{bt.__version__}",
76
- "yfinance": YF_AVAILABLE,
77
- "timestamp": datetime.utcnow().isoformat() + "Z",
78
- })
79
-
80
-
81
- @app.post("/backtest")
82
- def backtest():
83
- body = request.get_json(force=True)
84
- ticker: str = body.get("ticker", "").upper()
85
- custom_signals: list | None = body.get("signals") # optional float array
86
- lookback: int = int(body.get("lookback_days", 252))
87
- fees: float = float(body.get("fees", 0.001))
88
- slippage: float = float(body.get("slippage", 0.0005))
89
- window: int = int(body.get("momentum_window", 10))
90
- threshold: float = float(body.get("momentum_threshold", 0.001))
91
-
92
- if not ticker:
93
- return jsonify({"error": "ticker required"}), 400
94
-
95
- try:
96
- prices = _fetch_prices(ticker, lookback)
97
- except Exception as e:
98
- return jsonify({"error": str(e)}), 422
99
-
100
- if custom_signals:
101
- signals = np.array(custom_signals, dtype=float)
102
- if len(signals) != len(prices):
103
- return jsonify({"error": f"signals length {len(signals)} != prices length {len(prices)}"}), 400
104
- else:
105
- signals = _momentum_signals(prices, window=window, threshold=threshold)
106
-
107
- try:
108
- metrics = _run_backtest(prices, signals, fees, slippage)
109
- except Exception as e:
110
- return jsonify({"error": str(e)}), 500
111
-
112
- verdict = "ROBUST" if metrics["sharpe"] > 1.5 and metrics["max_drawdown"] > -0.25 else \
113
- "MODERATE" if metrics["sharpe"] > 0.8 else \
114
- "WEAK" if metrics["sharpe"] > 0 else "OVERFITTED"
115
-
116
- return jsonify({
117
- "ticker": ticker,
118
- "lookback_days": len(prices),
119
- "fees": fees,
120
- "slippage": slippage,
121
- "metrics": metrics,
122
- "verdict": verdict,
123
- "engine": f"backtester-mcp v{bt.__version__}",
124
- })
125
-
126
-
127
- @app.post("/validate")
128
- def walk_forward():
129
- """Walk-forward OOS validation — splits data into train/test."""
130
- body = request.get_json(force=True)
131
- ticker: str = body.get("ticker", "").upper()
132
- lookback: int = int(body.get("lookback_days", 504))
133
- train_ratio: float = float(body.get("train_ratio", 0.7))
134
- fees: float = float(body.get("fees", 0.001))
135
- slippage: float = float(body.get("slippage", 0.0005))
136
-
137
- if not ticker:
138
- return jsonify({"error": "ticker required"}), 400
139
-
140
- try:
141
- prices = _fetch_prices(ticker, lookback)
142
- except Exception as e:
143
- return jsonify({"error": str(e)}), 422
144
-
145
- split = int(len(prices) * train_ratio)
146
- train_prices = prices[:split]
147
- oos_prices = prices[split:]
148
-
149
- train_signals = _momentum_signals(train_prices)
150
- oos_signals = _momentum_signals(oos_prices)
151
-
152
- try:
153
- train_metrics = _run_backtest(train_prices, train_signals, fees, slippage)
154
- oos_metrics = _run_backtest(oos_prices, oos_signals, fees, slippage)
155
- except Exception as e:
156
- return jsonify({"error": str(e)}), 500
157
-
158
- # Deflated Sharpe — penalize if OOS degrades significantly
159
- sharpe_degradation = train_metrics["sharpe"] - oos_metrics["sharpe"]
160
- oos_verdict = "PASS" if oos_metrics["sharpe"] > 0.5 and sharpe_degradation < 1.5 else "FAIL"
161
-
162
- return jsonify({
163
- "ticker": ticker,
164
- "train_days": len(train_prices),
165
- "oos_days": len(oos_prices),
166
- "train_metrics": train_metrics,
167
- "oos_metrics": oos_metrics,
168
- "sharpe_degradation": round(sharpe_degradation, 3),
169
- "oos_verdict": oos_verdict,
170
- "engine": f"backtester-mcp v{bt.__version__}",
171
- })
172
-
173
-
174
- if __name__ == "__main__":
175
- port = int(os.environ.get("BACKTEST_PORT", 8300))
176
- app.run(host="0.0.0.0", port=port, debug=False)