phase-8g: rate limiting and update readme doc

This commit is contained in:
Kazuma
2026-06-05 23:02:21 -04:00
parent 9fb3808eb5
commit cea0ef4396
7 changed files with 320 additions and 184 deletions
+3 -3
View File
@@ -163,7 +163,7 @@ ui/ ← SvelteKit dashboard (lives inside this repo, not a
market-calls.json ← persisted market thesis calls (written by MarketCallRepository)
portfolio.json ← user's holdings: ticker, shares, costBasis, source, type
.env ← SIMPLEFIN_ACCESS_URL or SIMPLEFIN_SETUP_TOKEN, ANTHROPIC_API_KEY
.env ← SIMPLEFIN_ACCESS_URL or SIMPLEFIN_SETUP_TOKEN, ANTHROPIC_API_KEY, API_KEY (optional — enables Bearer auth on all routes)
```
---
@@ -626,9 +626,9 @@ Add one Fastify `inject()` smoke test per route using a fixture for `ScreenerEng
`BenchmarkProvider`'s 1-hour cache is in-memory only — cold start after every restart adds 24s latency to the first request. Write the cached `MarketContext` to `.benchmark-cache.json` (or a single-row SQLite table). Read it on boot; only re-fetch if stale.
#### 8g — Rate limiting + API key auth
#### 8g — Rate limiting + API key auth
Add `@fastify/rate-limit` on `/api/screen` and `/api/analyze` (e.g. 10 req/min per IP). Add a simple `Authorization: Bearer <key>` check against an `API_KEY` env var as middleware in `server/app.ts`. Both are single-digit line additions.
`@fastify/rate-limit` registered globally in `server/app.ts` (`global: false`, opt-in per route). `/api/screen`, `/api/screen/catalysts`, and `/api/analyze` each carry `config: { rateLimit: { max: 10, timeWindow: '1 minute' } }`. API key enforced via `onRequest` hook when `API_KEY` env var is set (`Authorization: Bearer <key>`); `/health` and OPTIONS are exempt. **Requires `npm install` after adding `@fastify/rate-limit` to dependencies (done in package.json).**
#### 8h — Extract `CalendarService`
+248 -177
View File
@@ -1,143 +1,141 @@
# Market Screener
A Node.js stock screener and personal finance assistant. Screens stocks, ETFs, and bonds using live Yahoo Finance data and scores each asset under two lenses — **Market-Adjusted** (what's acceptable in today's market) and **Fundamental** (strict Graham value-investing) — then compares both to give you an honest signal.
Comes with a **Fastify API server** and a companion **SvelteKit dashboard** (`../market-screener-ui`) for an interactive UI, or use it as a CLI to generate HTML reports.
A personal stock screener and portfolio tracker. Scores stocks, ETFs, and bonds under two lenses — **Market-Adjusted** (what's acceptable in today's market) and **Fundamental** (strict Graham value-investing) — then compares them to produce an actionable signal. Comes with a live SvelteKit dashboard.
---
## Quick Start
## Table of Contents
- [Developer Setup](#developer-setup)
- [Environment Variables](#environment-variables)
- [Commands](#commands)
- [Running Tests](#running-tests)
- [Project Structure](#project-structure)
- [User Guide](#user-guide)
---
## Developer Setup
### Prerequisites
- Node.js 20+
- npm 10+
### Install
```bash
# API + Dashboard (recommended)
# Install server dependencies
npm install
cd ../market-screener-ui && npm install && cd ../market_screener
npm run dev # starts API on :3000 + UI on :5173
# open http://localhost:5173
# CLI only
npm start # screen today's news catalyst tickers → screener-report.html
# Install UI dependencies (first time only)
npm run ui:install
```
### Start
```bash
npm run dev
```
This starts both the API server on **port 3000** and the SvelteKit UI on **port 5173** concurrently. Open [http://localhost:5173](http://localhost:5173).
To run the API server alone:
```bash
npm run server
```
---
## Environment Variables
Create a `.env` file in the project root. None are required to run the app — it works with Yahoo Finance data out of the box. Optional keys unlock additional features.
### `ANTHROPIC_API_KEY` — LLM news analysis *(optional)*
Powers the **Analyze** button on each screener section. Without this key the button is disabled.
1. Go to [console.anthropic.com](https://console.anthropic.com)
2. Create an API key under **API Keys**
3. Add to `.env`:
```env
ANTHROPIC_API_KEY=sk-ant-...
```
### `SIMPLEFIN_SETUP_TOKEN` — Live bank/brokerage balances *(optional)*
Powers the personal finance section of the Portfolio page (net worth, account balances, spending breakdown).
1. Go to [beta-bridge.simplefin.org](https://beta-bridge.simplefin.org) and create a Setup Token
2. Add to `.env`:
```env
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
```
On first request the token is claimed automatically and the resulting Access URL is saved back to `.env` as `SIMPLEFIN_ACCESS_URL`. Subsequent restarts use the Access URL directly.
### `API_KEY` — Bearer token auth *(optional)*
When set, every API route requires `Authorization: Bearer <key>`. Useful when the server is exposed to a network. `/health` and OPTIONS preflight are exempt.
```env
API_KEY=your-secret-key
```
### `CLIENT_ORIGIN` — CORS allowed origin *(optional)*
Defaults to `http://localhost:5173`. Change if the UI is served from a different origin.
```env
CLIENT_ORIGIN=https://yourdomain.com
```
### Complete `.env` example
```env
ANTHROPIC_API_KEY=sk-ant-...
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
API_KEY=optional-secret
CLIENT_ORIGIN=http://localhost:5173
```
---
## Commands
| Command | What it does |
| Command | Description |
|---|---|
| `npm run dev` | Start API server (port 3000) + SvelteKit UI (port 5173) together |
| `npm run dev` | Start API (port 3000) + UI (port 5173) together |
| `npm run server` | Start API server only |
| `npm start` | CLI: fetch today's market news, extract tickers, screen them |
| `npm start -- watch` | CLI: screen the default watchlist |
| `npm start -- AAPL MSFT VOO` | CLI: screen specific tickers |
| `npm run finance` | CLI: portfolio advice + SimpleFIN → `finance-report.html` |
| `npm run import-portfolio -- file.csv` | Import Robinhood/Vanguard/Fidelity CSV into `portfolio.json` |
| `npm test` | Run all 61 unit tests |
| `npm run test:watch` | Re-run tests on file changes |
| `npm run ui:install` | Install UI dependencies (first time / after `git pull`) |
| `npm test` | Run all unit + integration tests |
| `npm run test:watch` | Watch mode — re-run on file changes |
| `npm run typecheck` | TypeScript type check without emitting |
| `npm run format` | Format all source files with Prettier |
| `npm run format:check` | Check formatting without writing (used in CI) |
---
## How the Screener Works
Every asset is scored **twice** under different rule sets:
### Market-Adjusted mode
Gates derived from live Yahoo Finance benchmarks — reflects what is acceptable in today's market:
| Gate | Formula |
|---|---|
| Stock P/E | S&P 500 P/E (via SPY) × 1.5× (or × 1.2× in HIGH rate regime) |
| Tech P/E | XLK sector P/E × 1.3× |
| REIT min yield | XLRE dividend yield × 0.85× |
| Bond min spread | LQD TNX spread × 0.80× |
### Fundamental mode
Strict Graham/value-investing gates — reflects genuine value regardless of market conditions:
| Gate | Value | Rationale |
|---|---|---|
| Stock P/E | < 15× | Graham's actual rule (trailing earnings) |
| Stock PEG | < 1.0 | Lynch standard: PEG > 1.0 = paying full price |
| D/E ratio | < 1.5× | Distress typically starts above 2× |
| Quick ratio | > 0.8 | Below 0.8 = real liquidity stress |
| Bond spread | > 1.5% | Must clear risk-free rate meaningfully |
| Bond duration | < 7 years | Rate sensitivity management |
### Signals
| Signal | Meaning |
|---|---|
| ✅ Strong Buy | Passes both lenses — genuinely good value |
| ⚡ Momentum | Passes market-adjusted, holds fundamentally |
| ⚠️ Speculation | Passes market-adjusted, fails fundamental — priced for perfection |
| 🔄 Neutral | Hold territory in one or both lenses |
| ❌ Avoid | Fails both |
---
## Sector Overrides
Sector-specific rules apply in both modes (not just inflated):
| Sector | Key adjustments |
|---|---|
| **Technology** | P/E up to 35×, D/E up to 2.0 (buybacks), FCF weight raised |
| **REIT** | P/E/PEG disabled, scored on dividend yield + P/FFO proxy |
| **Financial** | D/E disabled, scored on ROE (≥12%) + P/B (< 1.5×) |
| **Energy** | FCF primary signal (weight 4), dividend yield scored |
| **Healthcare** | Revenue growth primary, P/E up to 25× (R&D burn) |
| **Communication** | FCF primary (META/GOOGL platforms), P/E up to 25× |
| **Consumer Staples** | Margin/ROE focus, low revenue growth expectations (25%) |
| **Consumer Discretionary** | Revenue growth primary, P/E up to 25× |
---
## API Server
```
GET /health → { status: "ok" }
POST /api/screen → screen tickers
body: { tickers: string[] }
GET /api/screen/catalysts → Yahoo news → { tickers, stories }
GET /api/finance/portfolio → portfolio advice + net worth
GET /api/finance/market-context → live benchmark data
```
Set `CLIENT_ORIGIN` env var to allow a different CORS origin (default: `http://localhost:5173`).
---
## Personal Finance
Edit `portfolio.json` with your holdings (or import from a broker CSV):
```json
{
"holdings": [
{ "ticker": "AAPL", "shares": 10, "costBasis": 150.00, "source": "Robinhood", "type": "stock" },
{ "ticker": "VOO", "shares": 8, "costBasis": 380.00, "source": "Vanguard", "type": "etf" },
{ "ticker": "BTC-USD", "shares": 0.25, "costBasis": 45000, "source": "Coinbase", "type": "crypto" }
]
}
```
`npm run finance` (CLI) or `GET /api/finance/portfolio` (API) screens your holdings and cross-references the screener signal with your gain/loss position to give hold/sell/add advice.
### SimpleFIN (optional — live bank/brokerage balances)
1. Get your setup token from [beta-bridge.simplefin.org](https://beta-bridge.simplefin.org)
2. Add to `.env`: `SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...`
3. On first run the Access URL is claimed and saved to `.env` automatically
### Importing broker holdings
## Running Tests
```bash
npm run import-portfolio -- ~/Downloads/robinhood_holdings.csv
npm run import-portfolio -- ~/Downloads/vanguard_holdings.csv
npm test
```
Broker is auto-detected from CSV headers. Multiple imports merge into `portfolio.json`.
Uses Node's built-in `node:test` runner — no external framework. Tests cover:
- Scoring rules and gate values (`ScoringConfig`, `RuleMerger`, `MarketRegime`)
- Asset scorers (`StockScorer`, `EtfScorer`, `BondScorer`)
- Data mapping (`DataMapper`)
- Portfolio advice logic (`PortfolioAdvisor`)
- LLM response parsing (`LLMAnalyst`)
- Repository CRUD (`MarketCallRepository`)
- Controller integration tests for all API routes (Fastify `inject()`, zero live network calls)
Pre-commit hook runs Prettier then tests. Pre-push hook runs tests.
---
@@ -145,81 +143,154 @@ Broker is auto-detected from CSV headers. Multiple imports merge into `portfolio
```
bin/
screen.js CLI screener entry point
finance.js CLI personal finance entry point
import-portfolio.js Broker CSV importer
server.js Fastify API server entry point
server.ts API server entry point
scripts/
summary-reporter.js Custom node:test reporter (silent pass, shows failures + summary)
server/
app.ts Fastify app factory — wires DI, rate limiting, auth hook
controllers/ HTTP layer: parse request → call service → return response
services/ Business logic (ScreenerEngine, BenchmarkProvider, PortfolioAdvisor…)
repositories/ JSON file persistence (MarketCallRepository, PortfolioRepository)
clients/ External API connectors (YahooFinanceClient, SimpleFINClient, AnthropicClient)
models/ Domain entities: Stock, Etf, Bond
scorers/ Stateless scoring functions: StockScorer, EtfScorer, BondScorer
config/ ScoringConfig (all gates/weights), constants
types/ TypeScript interfaces, one file per domain
src/
config/
ScoringConfig.js All gates, weights, thresholds (single source of truth)
constants.js Shared enums: SIGNAL, ASSET_TYPE, SECTOR, SCORE_MODE, REGIME
ui/
src/
routes/ SvelteKit pages: /, /portfolio, /calls, /safe-buys
lib/
stores/ Svelte 5 reactive stores (screener.store, portfolio.store)
api/ Fetch wrappers for each API domain
portfolio/ Portfolio-specific components
calls/ Market calls components
styles/ Global SCSS design tokens and partials
market/
YahooClient.js Wraps yahoo-finance2 v3 with retry + backoff
BenchmarkProvider.js Fetches live benchmarks → marketContext (1-hour cache)
MarketRegime.js Derives inflated gate overrides from live data + rate regime
tests/ Unit + integration tests
screener/
ScreenerEngine.js Orchestrates fetch → score × 2.
screenTickers() → pure data (server/CLI)
screenWithProgress() → with stdout progress (CLI only)
DataMapper.js Normalises Yahoo payload → flat asset objects
Uses trailingPE (not forwardPE). Preserves negative FCF.
RuleMerger.js Merges base rules + sector overrides + MarketRegime
assets/ Stock, Etf, Bond data containers
scorers/ StockScorer, EtfScorer, BondScorer (stateless)
analyst/
CatalystAnalyst.js Extracts tickers from Yahoo Finance news headlines
finance/
clients/
SimpleFINClient.js Auth + account fetching via Basic Auth header
PersonalFinanceAnalyzer.js Net worth, cash vs investments, spending
PortfolioAdvisor.js Hold/sell/add advice per holding
PortfolioImporter.js Parses Robinhood/Vanguard/Fidelity CSV
reporters/
HtmlReporter.js render() → string | generate() → file (CLI)
FinanceReporter.js render() → string | generate() → file (CLI)
server/
app.js Fastify app factory (buildApp)
routes/
screener.js POST /api/screen, GET /api/screen/catalysts
finance.js GET /api/finance/portfolio, GET /api/finance/market-context
portfolio.json Your holdings (gitignored — create manually or via the UI)
market-calls.json Persisted market thesis calls (gitignored)
.benchmark-cache.json Benchmark data cache — survives server restart (gitignored)
```
---
## Environment Variables
## User Guide
```bash
# .env
SIMPLEFIN_ACCESS_URL=https://user:pass@beta-bridge.simplefin.org/simplefin
# or on first run:
SIMPLEFIN_SETUP_TOKEN=aHR0cHM6Ly...
### Screener tab
# Optional server config
PORT=3000
HOST=0.0.0.0
CLIENT_ORIGIN=http://localhost:5173 # CORS allowed origin for SvelteKit UI
```
The main view. On load it automatically fetches today's financial news, extracts the most-mentioned tickers, and screens them.
#### Market context strip
The row of chips at the top shows live benchmark data fetched from Yahoo Finance:
| Chip | Meaning |
|---|---|
| 10Y | 10-year Treasury yield — the risk-free rate |
| VIX | Volatility index — market fear gauge |
| S&P | S&P 500 index price |
| S&P P/E | Trailing P/E of the S&P 500 (via SPY) |
| Tech P/E | Trailing P/E of the tech sector (via XLK) |
| REIT Yld | REIT dividend yield (via XLRE) |
| IG Sprd | Investment-grade bond spread above risk-free (LQD TNX) |
| Rates | Rate regime: LOW / NORMAL / HIGH (based on 10Y yield) |
| Vol | Volatility regime: LOW / NORMAL / HIGH (based on VIX) |
The rate regime affects how strict the Market-Adjusted gates are — in a HIGH rate environment the P/E multiplier compresses and bond spreads tighten.
#### Signal Summary table
Quick overview of all screened tickers with their signal, market-adjusted verdict, fundamental verdict, market cap tier, and growth style.
#### Per-asset detail tables
Expand each section (STOCK / ETF / BOND) for full metrics: P/E, PEG, ROE, margins, FCF yield, D/E, analyst consensus, DCF intrinsic value, 52-week movement, and more.
#### Analyze button
Runs an Anthropic LLM over the latest Yahoo Finance news for assets in that section. Returns a sentiment summary, affected industries, and related tickers to watch. Requires `ANTHROPIC_API_KEY`.
#### Search tickers
Click **Search tickers** to screen any custom list — type tickers comma or space separated and press Enter or click Screen.
#### Signals explained
| Signal | What it means |
|---|---|
| ✅ Strong Buy | Passes both Market-Adjusted AND Fundamental gates — genuine value at current prices |
| ⚡ Momentum | Passes Market-Adjusted, holds fundamentally — good in the current market but not a bargain |
| ⚠️ Speculation | Passes Market-Adjusted, fails Fundamental — priced for perfection, high risk |
| 🔄 Neutral | Borderline in one or both lenses — hold, no clear edge |
| ❌ Avoid | Fails both lenses |
#### How scoring works
Every asset is scored twice:
**Market-Adjusted** gates move with the market. The stock P/E gate = SPY trailing P/E × 1.5 (compresses to × 1.2 in a HIGH rate regime). Tech P/E = XLK P/E × 1.3. This reflects what the market is currently willing to pay.
**Fundamental** gates are fixed Graham/value-investing standards that never change:
| Gate | Threshold | Rationale |
|---|---|---|
| Stock P/E | < 15× | Graham's actual rule |
| Stock PEG | < 1.0 | Lynch: PEG > 1.0 = paying full price |
| D/E ratio | < 1.5× | Distress typically starts above 2× |
| Quick ratio | > 0.8 | Below 0.8 = real liquidity stress |
Sector overrides apply in both modes — e.g. tech stocks allow P/E up to 35× and D/E up to 2.0, REITs are scored on yield rather than P/E.
---
## Testing
### Portfolio tab
61 unit tests, no external test framework:
Track your holdings and get hold/sell/add advice cross-referenced with screener signals.
```bash
npm test # summary output: "✅ 61 tests: 61 passed (0.02s)"
npm run test:watch # verbose spec output for development
```
**Adding holdings** — click **+ Add Holding** and fill in ticker, shares, cost basis, asset type, and source broker. Holdings are saved to `portfolio.json` on disk.
Pre-commit: Prettier (auto-format staged files) + full test suite.
Pre-push: full test suite.
**Inline editing** — click the ✎ pencil icon on any row to edit shares, cost basis, type, or source directly in the table.
**Advice column** — each holding is screened live and the signal is combined with your gain/loss position:
| Situation | Advice |
|---|---|
| ✅ Strong Buy signal | Hold & Add |
| ⚡ Momentum + > 30% gain | Consider partial profit-taking |
| ⚠️ Speculation + > 20% gain | Reduce position |
| ❌ Avoid signal + in profit | Sell (Take Profits) |
| ❌ Avoid signal + at a loss | Sell (Cut Loss) |
| Crypto | Hold / Review position (no fundamental scoring) |
**Personal finance section** *(requires SimpleFIN)* — when configured, the page also shows net worth, total assets vs liabilities, cash vs investments ratio, monthly income/spend, account balances, and a spending breakdown by category for the last 30 days.
---
### Market Calls tab
Record and track quarterly investment theses from the day you make the call.
**Creating a call** — click ** New Call** and fill in:
- **Title** — e.g. "Q3 2025 — Rate pivot & tech rotation"
- **Quarter** — the quarter this thesis applies to
- **Thesis** — the macro reasoning behind the call (min 10 characters)
- **Tickers** — the assets you're watching for this thesis
When saved, the current price and signal for each ticker are snapshotted automatically.
**Viewing performance** — click any call card to see the current price and signal for each ticker alongside the original snapshot, so you can measure how the thesis played out.
**Calendar** — shows upcoming earnings dates, ex-dividend dates, and dividend payment dates for all tickers across your active calls.
---
### Safe Buys tab
A filtered view showing only tickers with a **✅ Strong Buy** signal across both lenses. A quick watchlist of assets passing the strictest criteria in the current market.
---
### API rate limits
`/api/screen`, `/api/screen/catalysts`, and `/api/analyze` are capped at **10 requests per minute** per IP. All other routes allow 60 per minute.
+31
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.100.1",
"@fastify/cors": "^11.2.0",
"@fastify/rate-limit": "^10.2.1",
"dotenv": "^16.0.0",
"fastify": "^5.8.5",
"yahoo-finance2": "^3.15.2"
@@ -733,6 +734,27 @@
"node": ">= 10"
}
},
"node_modules/@fastify/rate-limit": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-10.3.0.tgz",
"integrity": "sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^5.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.14",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
@@ -783,6 +805,15 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@lukeed/ms": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+1
View File
@@ -24,6 +24,7 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.100.1",
"@fastify/cors": "^11.2.0",
"@fastify/rate-limit": "^10.2.1",
"dotenv": "^16.0.0",
"fastify": "^5.8.5",
"yahoo-finance2": "^3.15.2"
+22 -1
View File
@@ -1,5 +1,6 @@
import Fastify from 'fastify';
import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
import { ScreenerController } from './controllers/screener.controller';
import { FinanceController } from './controllers/finance.controller';
import { CallsController } from './controllers/calls.controller';
@@ -31,6 +32,26 @@ export async function buildApp({ logger = true }: BuildAppOptions = {}) {
origin: process.env.CLIENT_ORIGIN ?? 'http://localhost:5173',
});
// ── Rate limiting — applied globally, tightest on expensive routes ───────
await app.register(rateLimit, {
global: false, // opt-in per route via config.rateLimit
max: 60,
timeWindow: '1 minute',
});
// ── API key auth — only enforced when API_KEY env var is set ─────────────
const API_KEY = process.env.API_KEY;
if (API_KEY) {
app.addHook('onRequest', async (req: FastifyRequest, reply: FastifyReply) => {
// Skip auth for health check and OPTIONS preflight
if (req.url === '/health' || req.method === 'OPTIONS') return;
const header = req.headers['authorization'] ?? '';
if (header !== `Bearer ${API_KEY}`) {
return reply.code(401).send({ error: 'Unauthorized' });
}
});
}
const yahoo = new YahooFinanceClient();
const benchmark = new BenchmarkProvider(yahoo, { logger: noopLogger });
const engine = new ScreenerEngine(yahoo, benchmark, { logger: noopLogger });
+5 -1
View File
@@ -10,7 +10,11 @@ export class AnalyzeController {
) {}
register(app: FastifyInstance): void {
app.post('/api/analyze', { schema: analyzeSchema }, this.analyze.bind(this));
app.post(
'/api/analyze',
{ schema: analyzeSchema, config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
this.analyze.bind(this),
);
}
private async analyze(req: FastifyRequest, reply: FastifyReply) {
+10 -2
View File
@@ -8,8 +8,16 @@ export class ScreenerController {
constructor(private readonly engine: ScreenerEngine) {}
register(app: FastifyInstance): void {
app.post('/api/screen', { schema: screenSchema }, this.screen.bind(this));
app.get('/api/screen/catalysts', this.catalysts.bind(this));
app.post(
'/api/screen',
{ schema: screenSchema, config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
this.screen.bind(this),
);
app.get(
'/api/screen/catalysts',
{ config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
this.catalysts.bind(this),
);
}
private static serializeAssets(arr: LiveAssetResult[]) {