Compare commits
44 Commits
6205d5192f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aadbef308 | |||
| 6cb4c93a0e | |||
| 65907a9b8d | |||
| bf2a85b5c4 | |||
| f0c794f0c0 | |||
| 5655cde6bf | |||
| fbadd7fb6e | |||
| 7bc242911e | |||
| ad1c3fe3c9 | |||
| a289cda704 | |||
| 0dac8128bd | |||
| 09f2444157 | |||
| ca449b4300 | |||
| 8bdcb422aa | |||
| 7f2108129a | |||
| 7cb1b03fd5 | |||
| 5b32bd7a04 | |||
| 2b785aa861 | |||
| de8427d578 | |||
| e4ab9ce45b | |||
| f5a338fc4e | |||
| 96e48aebe5 | |||
| 640004a921 | |||
| 6878bd1f2b | |||
| 6356415789 | |||
| b9c5dbb7c7 | |||
| 76c2a671f4 | |||
| 2e7860637e | |||
| fbd166b1b7 | |||
| cea0ef4396 | |||
| 9fb3808eb5 | |||
| bd373ab69b | |||
| 617703e91d | |||
| 73db0fe7a8 | |||
| 69d13c3dbe | |||
| 16bd95aa85 | |||
| 104ed81b9f | |||
| 0a0a368b87 | |||
| b75e8bda72 | |||
| 19fc052d14 | |||
| 70643a1fd9 | |||
| 8ff7e5d235 | |||
| d9879d5c6c | |||
| 74e6797dcc |
@@ -1,606 +0,0 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: market-screener.bruno
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/analyze:
|
||||
post:
|
||||
summary: 'Analyze — Validation: empty tickers (expect 400)'
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_llm_analysis_analyze_validation-_empty_tickers_expect_400_bru
|
||||
description: 'Schema validation: minItems: 1. Expect 400.'
|
||||
tags:
|
||||
- LLM Analysis
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
parameters:
|
||||
- name: Content-Type
|
||||
in: header
|
||||
description: ''
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/analyze_validation_empty_tickers_expect_400'
|
||||
/health:
|
||||
get:
|
||||
summary: Health Check
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_health_health_check_bru
|
||||
description: 'Confirms the server is running. Expects { status: ''ok'' }.'
|
||||
tags:
|
||||
- Health
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/finance/market-context:
|
||||
get:
|
||||
summary: Get Market Context
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_context_get_market_context_bru
|
||||
description: >-
|
||||
Returns live benchmark data: S&P500 price, 10Y rate, VIX, SPY P/E, XLK
|
||||
P/E, XLRE yield, LQD spread. Served from 1-hour in-memory cache.
|
||||
tags:
|
||||
- Market Context
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/calls:
|
||||
post:
|
||||
summary: Create Market Call
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_create_market_call_bru
|
||||
description: >-
|
||||
Creates a market thesis call. Snapshots current prices + screener
|
||||
signals at creation time for future comparison.
|
||||
|
||||
|
||||
The test script saves the returned ID to the {{callId}} collection
|
||||
variable for use in subsequent requests.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
parameters:
|
||||
- name: Content-Type
|
||||
in: header
|
||||
description: ''
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/create_market_call'
|
||||
get:
|
||||
summary: List Calls (empty or existing)
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_list_calls_empty_or_existing_bru
|
||||
description: >-
|
||||
Returns all market calls sorted newest first. Returns { calls: [] } if
|
||||
none exist yet.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/calls/{{callId}}:
|
||||
delete:
|
||||
summary: Delete Call
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_delete_call_bru
|
||||
description: >-
|
||||
Deletes the call created earlier. Returns { ok: true }. Requires
|
||||
{{callId}} to be set.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
get:
|
||||
summary: Get Call by ID (with current re-screen)
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_get_call_by_id_with_current_re-screen_bru
|
||||
description: >-
|
||||
Fetches the call and re-screens all tickers to show how signal/price has
|
||||
changed since creation.
|
||||
|
||||
|
||||
Returns: original call fields + `current` map of ticker → { price,
|
||||
signal, inflatedVerdict, fundamentalVerdict, pe, roe, fcf }.
|
||||
|
||||
|
||||
Depends on {{callId}} being set by the Create Market Call request.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/calls/00000000-0000-0000-0000-000000000000:
|
||||
get:
|
||||
summary: Get Call — Non-existent ID (expect 404)
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_get_call_non-existent_id_expect_404_bru
|
||||
description: A UUID that doesn't exist. Expect 404.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/calls/calendar:
|
||||
get:
|
||||
summary: Get Earnings Calendar (call tickers)
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_get_earnings_calendar_call_tickers_bru
|
||||
description: >-
|
||||
Returns upcoming earnings dates and dividend events for all tickers
|
||||
across all saved calls.
|
||||
|
||||
|
||||
Optional query param ?tickers=AAPL,MSFT to restrict to specific tickers.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/calls/calendar?tickers=AAPL,MSFT:
|
||||
get:
|
||||
summary: Get Earnings Calendar — Specific Tickers
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_market_calls_get_earnings_calendar_specific_tickers_bru
|
||||
description: Calendar for specific tickers regardless of saved calls.
|
||||
tags:
|
||||
- Market Calls
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
parameters:
|
||||
- name: tickers
|
||||
in: query
|
||||
description: ''
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: AAPL,MSFT
|
||||
/api/finance/holdings:
|
||||
post:
|
||||
summary: 'Add Holding — Validation: missing shares (expect 400)'
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_portfolio_add_holding_validation-_missing_shares_expect_400_bru
|
||||
description: 'Schema validation: shares is required. Expect 400.'
|
||||
tags:
|
||||
- Portfolio
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
parameters:
|
||||
- name: Content-Type
|
||||
in: header
|
||||
description: ''
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
requestBody:
|
||||
$ref: >-
|
||||
#/components/requestBodies/add_holding_validation_missing_shares_expect_400
|
||||
/api/finance/portfolio:
|
||||
get:
|
||||
summary: Get Portfolio
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_portfolio_get_portfolio_bru
|
||||
description: >-
|
||||
Screens all non-crypto holdings via Yahoo Finance, then cross-references
|
||||
with signals to produce buy/hold/sell advice.
|
||||
|
||||
|
||||
Each row has: ticker, signal, advice, reason, currentPrice, marketValue,
|
||||
gainLossPct.
|
||||
|
||||
Also returns marketContext.
|
||||
|
||||
|
||||
Note: first call after server start may be slow (benchmark cache cold).
|
||||
tags:
|
||||
- Portfolio
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/finance/holdings/AAPL:
|
||||
delete:
|
||||
summary: Remove Holding — AAPL
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_portfolio_remove_holding_aapl_bru
|
||||
description: 'Removes the AAPL holding from portfolio.json. Expect { ok: true }.'
|
||||
tags:
|
||||
- Portfolio
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/finance/holdings/ZZZZZZ:
|
||||
delete:
|
||||
summary: Remove Holding — Non-existent (expect 404)
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_portfolio_remove_holding_non-existent_expect_404_bru
|
||||
description: Ticker does not exist in portfolio. Expect 404.
|
||||
tags:
|
||||
- Portfolio
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/screen/catalysts:
|
||||
get:
|
||||
summary: Get Catalysts
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_screener_get_catalysts_bru
|
||||
description: >-
|
||||
Fetches today's Yahoo Finance news, extracts ticker symbols mentioned,
|
||||
and returns { tickers, stories }. May take 3-5s as it queries multiple
|
||||
news endpoints.
|
||||
tags:
|
||||
- Screener
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
/api/screen:
|
||||
post:
|
||||
summary: 'Screen — Validation: over 50 tickers (expect 400)'
|
||||
operationId: >-
|
||||
users_kanna_documents_bruno_market_screener_api_-_1_screener_screen_validation-_over_50_tickers_expect_400_bru
|
||||
description: 'Schema validation: maxItems: 50. 51 tickers should return 400.'
|
||||
tags:
|
||||
- Screener
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
parameters:
|
||||
- name: Content-Type
|
||||
in: header
|
||||
description: ''
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
requestBody:
|
||||
$ref: >-
|
||||
#/components/requestBodies/screen_validation_over_50_tickers_expect_400
|
||||
servers:
|
||||
- url: http://localhost:3000
|
||||
description: Base Server
|
||||
components:
|
||||
schemas:
|
||||
analyze_tickers:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers:
|
||||
- NVDA
|
||||
- AMD
|
||||
- INTC
|
||||
analyze_validation_empty_tickers_expect_400:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers: []
|
||||
create_call_validation_short_thesis_expect_400:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
quarter:
|
||||
type: string
|
||||
thesis:
|
||||
type: string
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
title: Test
|
||||
quarter: Q1
|
||||
thesis: short
|
||||
tickers:
|
||||
- AAPL
|
||||
create_market_call:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
quarter:
|
||||
type: string
|
||||
thesis:
|
||||
type: string
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
title: AI Infrastructure Supercycle
|
||||
quarter: Q3 2025
|
||||
thesis: >-
|
||||
Hyperscaler capex remains elevated through 2026 driven by LLM training
|
||||
demand. NVDA, MSFT and AMD are the primary beneficiaries. Entry here
|
||||
as NVDA pulled back 15% from high.
|
||||
tickers:
|
||||
- NVDA
|
||||
- MSFT
|
||||
- AMD
|
||||
add_holding_aapl:
|
||||
type: object
|
||||
properties:
|
||||
ticker:
|
||||
type: string
|
||||
shares:
|
||||
type: integer
|
||||
costBasis:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
example:
|
||||
ticker: AAPL
|
||||
shares: 10
|
||||
costBasis: 150
|
||||
type: stock
|
||||
source: Robinhood
|
||||
add_holding_btc-usd_crypto_no_scoring:
|
||||
type: object
|
||||
properties:
|
||||
ticker:
|
||||
type: string
|
||||
shares:
|
||||
type: number
|
||||
costBasis:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
example:
|
||||
ticker: BTC-USD
|
||||
shares: 0.1
|
||||
costBasis: 50000
|
||||
type: crypto
|
||||
source: Coinbase
|
||||
add_holding_voo_etf:
|
||||
type: object
|
||||
properties:
|
||||
ticker:
|
||||
type: string
|
||||
shares:
|
||||
type: integer
|
||||
costBasis:
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
example:
|
||||
ticker: VOO
|
||||
shares: 5
|
||||
costBasis: 420
|
||||
type: etf
|
||||
source: Vanguard
|
||||
add_holding_validation_missing_shares_expect_400:
|
||||
type: object
|
||||
properties:
|
||||
ticker:
|
||||
type: string
|
||||
example:
|
||||
ticker: MSFT
|
||||
screen_mixed_stock_etf_bond:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers:
|
||||
- AAPL
|
||||
- MSFT
|
||||
- GOOGL
|
||||
- VOO
|
||||
- AGG
|
||||
screen_reit_tests_p_ffo_scoring_path:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers:
|
||||
- O
|
||||
- VICI
|
||||
- PLD
|
||||
screen_tech_stocks_tests_technology_sector_override:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers:
|
||||
- NVDA
|
||||
- META
|
||||
- AMZN
|
||||
- TSLA
|
||||
screen_validation_empty_tickers_expect_400:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers: []
|
||||
screen_validation_over_50_tickers_expect_400:
|
||||
type: object
|
||||
properties:
|
||||
tickers:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
tickers:
|
||||
- A
|
||||
- B
|
||||
- C
|
||||
- D
|
||||
- E
|
||||
- F
|
||||
- G
|
||||
- H
|
||||
- I
|
||||
- J
|
||||
- K
|
||||
- L
|
||||
- M
|
||||
- 'N'
|
||||
- O
|
||||
- P
|
||||
- Q
|
||||
- R
|
||||
- S
|
||||
- T
|
||||
- U
|
||||
- V
|
||||
- W
|
||||
- X
|
||||
- 'Y'
|
||||
- Z
|
||||
- AA
|
||||
- BB
|
||||
- CC
|
||||
- DD
|
||||
- EE
|
||||
- FF
|
||||
- GG
|
||||
- HH
|
||||
- II
|
||||
- JJ
|
||||
- KK
|
||||
- LL
|
||||
- MM
|
||||
- NN
|
||||
- OO
|
||||
- PP
|
||||
- QQ
|
||||
- RR
|
||||
- SS
|
||||
- TT
|
||||
- UU
|
||||
- VV
|
||||
- WW
|
||||
- XX
|
||||
- YY
|
||||
requestBodies:
|
||||
analyze_tickers:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/analyze_tickers'
|
||||
description: ''
|
||||
required: true
|
||||
analyze_validation_empty_tickers_expect_400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/analyze_validation_empty_tickers_expect_400'
|
||||
description: ''
|
||||
required: true
|
||||
create_call_validation_short_thesis_expect_400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/create_call_validation_short_thesis_expect_400
|
||||
description: ''
|
||||
required: true
|
||||
create_market_call:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/create_market_call'
|
||||
description: ''
|
||||
required: true
|
||||
add_holding_aapl:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/add_holding_aapl'
|
||||
description: ''
|
||||
required: true
|
||||
add_holding_btc-usd_crypto_no_scoring:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/add_holding_btc-usd_crypto_no_scoring'
|
||||
description: ''
|
||||
required: true
|
||||
add_holding_voo_etf:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/add_holding_voo_etf'
|
||||
description: ''
|
||||
required: true
|
||||
add_holding_validation_missing_shares_expect_400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/add_holding_validation_missing_shares_expect_400
|
||||
description: ''
|
||||
required: true
|
||||
screen_mixed_stock_etf_bond:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/screen_mixed_stock_etf_bond'
|
||||
description: ''
|
||||
required: true
|
||||
screen_reit_tests_p_ffo_scoring_path:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/screen_reit_tests_p_ffo_scoring_path'
|
||||
description: ''
|
||||
required: true
|
||||
screen_tech_stocks_tests_technology_sector_override:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: >-
|
||||
#/components/schemas/screen_tech_stocks_tests_technology_sector_override
|
||||
description: ''
|
||||
required: true
|
||||
screen_validation_empty_tickers_expect_400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/screen_validation_empty_tickers_expect_400'
|
||||
description: ''
|
||||
required: true
|
||||
screen_validation_over_50_tickers_expect_400:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/screen_validation_over_50_tickers_expect_400'
|
||||
description: ''
|
||||
required: true
|
||||
securitySchemes: {}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Market Screener API",
|
||||
"description": "Full test suite for the market-screener Fastify server.\n\nBase URL is stored in the `baseUrl` collection variable (default: http://localhost:3000).\n\nWorkflow order for a clean session:\n1. Health Check\n2. Screen Tickers (creates results to inspect)\n3. Get Market Context\n4. Get Catalysts\n5. Add Holdings → Get Portfolio\n6. Create Market Call → Get Call → Calendar\n7. Analyze\n8. Cleanup (delete holding, delete call)",
|
||||
"description": "Full test suite for the market-screener Fastify server.\n\nBase URL is stored in the `baseUrl` collection variable (default: http://localhost:3000).\n\nWorkflow order for a clean session:\n1. Health Check\n2. Screen Tickers (creates results to inspect)\n3. Get Market Context\n4. Get Catalysts\n5. Add Holdings \u2192 Get Portfolio\n6. Create Market Call \u2192 Get Call \u2192 Calendar\n7. Analyze\n8. Cleanup (delete holding, delete call)",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
@@ -15,6 +15,11 @@
|
||||
"value": "",
|
||||
"type": "string",
|
||||
"description": "Set automatically by the Create Market Call test script"
|
||||
},
|
||||
{
|
||||
"key": "jwt",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
@@ -49,11 +54,16 @@
|
||||
"name": "Screener",
|
||||
"item": [
|
||||
{
|
||||
"name": "Screen — Mixed (STOCK + ETF + BOND)",
|
||||
"name": "Screen \u2014 Mixed (STOCK + ETF + BOND)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/screen",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": [\"AAPL\", \"MSFT\", \"GOOGL\", \"VOO\", \"AGG\"]\n}"
|
||||
@@ -90,7 +100,7 @@
|
||||
"pm.test('Each stock has a signal', () => {",
|
||||
" pm.response.json().STOCK.forEach(r => {",
|
||||
" pm.expect(r.signal).to.be.oneOf([",
|
||||
" '✅ Strong Buy', '⚡ Momentum', '⚠️ Speculation', '🔄 Neutral', '❌ Avoid'",
|
||||
" '\u2705 Strong Buy', '\u26a1 Momentum', '\u26a0\ufe0f Speculation', '\ud83d\udd04 Neutral', '\u274c Avoid'",
|
||||
" ]);",
|
||||
" });",
|
||||
"});"
|
||||
@@ -100,11 +110,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screen — Tech Stocks (tests TECHNOLOGY sector override)",
|
||||
"name": "Screen \u2014 Tech Stocks (tests TECHNOLOGY sector override)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/screen",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": [\"NVDA\", \"META\", \"AMZN\", \"TSLA\"]\n}"
|
||||
@@ -140,11 +155,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screen — REIT (tests P/FFO scoring path)",
|
||||
"name": "Screen \u2014 REIT (tests P/FFO scoring path)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/screen",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": [\"O\", \"VICI\", \"PLD\"]\n}"
|
||||
@@ -169,11 +189,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screen — Validation: empty tickers (expect 400)",
|
||||
"name": "Screen \u2014 Validation: empty tickers (expect 400)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/screen",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": []\n}"
|
||||
@@ -192,11 +217,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screen — Validation: over 50 tickers (expect 400)",
|
||||
"name": "Screen \u2014 Validation: over 50 tickers (expect 400)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/screen",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": [\"A\",\"B\",\"C\",\"D\",\"E\",\"F\",\"G\",\"H\",\"I\",\"J\",\"K\",\"L\",\"M\",\"N\",\"O\",\"P\",\"Q\",\"R\",\"S\",\"T\",\"U\",\"V\",\"W\",\"X\",\"Y\",\"Z\",\"AA\",\"BB\",\"CC\",\"DD\",\"EE\",\"FF\",\"GG\",\"HH\",\"II\",\"JJ\",\"KK\",\"LL\",\"MM\",\"NN\",\"OO\",\"PP\",\"QQ\",\"RR\",\"SS\",\"TT\",\"UU\",\"VV\",\"WW\",\"XX\",\"YY\"]\n}"
|
||||
@@ -277,11 +307,16 @@
|
||||
"name": "Portfolio",
|
||||
"item": [
|
||||
{
|
||||
"name": "Add Holding — AAPL",
|
||||
"name": "Add Holding \u2014 AAPL",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/finance/holdings",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"ticker\": \"AAPL\",\n \"shares\": 10,\n \"costBasis\": 150.00,\n \"type\": \"stock\",\n \"source\": \"Robinhood\"\n}"
|
||||
@@ -307,11 +342,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add Holding — VOO (ETF)",
|
||||
"name": "Add Holding \u2014 VOO (ETF)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/finance/holdings",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"ticker\": \"VOO\",\n \"shares\": 5,\n \"costBasis\": 420.00,\n \"type\": \"etf\",\n \"source\": \"Vanguard\"\n}"
|
||||
@@ -329,16 +369,21 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add Holding — BTC-USD (Crypto, no scoring)",
|
||||
"name": "Add Holding \u2014 BTC-USD (Crypto, no scoring)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/finance/holdings",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"ticker\": \"BTC-USD\",\n \"shares\": 0.1,\n \"costBasis\": 50000,\n \"type\": \"crypto\",\n \"source\": \"Coinbase\"\n}"
|
||||
},
|
||||
"description": "Crypto is priced via Yahoo but not fundamentally scored. Advice column shows '—' for signal."
|
||||
"description": "Crypto is priced via Yahoo but not fundamentally scored. Advice column shows '\u2014' for signal."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
@@ -352,11 +397,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Add Holding — Validation: missing shares (expect 400)",
|
||||
"name": "Add Holding \u2014 Validation: missing shares (expect 400)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/finance/holdings",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"ticker\": \"MSFT\"\n}"
|
||||
@@ -407,7 +457,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Remove Holding — AAPL",
|
||||
"name": "Remove Holding \u2014 AAPL",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"url": "{{baseUrl}}/api/finance/holdings/AAPL",
|
||||
@@ -426,7 +476,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Remove Holding — Non-existent (expect 404)",
|
||||
"name": "Remove Holding \u2014 Non-existent (expect 404)",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"url": "{{baseUrl}}/api/finance/holdings/ZZZZZZ",
|
||||
@@ -472,7 +522,12 @@
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/calls",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"title\": \"AI Infrastructure Supercycle\",\n \"quarter\": \"Q3 2025\",\n \"thesis\": \"Hyperscaler capex remains elevated through 2026 driven by LLM training demand. NVDA, MSFT and AMD are the primary beneficiaries. Entry here as NVDA pulled back 15% from high.\",\n \"tickers\": [\"NVDA\", \"MSFT\", \"AMD\"]\n}"
|
||||
@@ -504,7 +559,7 @@
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/api/calls/{{callId}}",
|
||||
"description": "Fetches the call and re-screens all tickers to show how signal/price has changed since creation.\n\nReturns: original call fields + `current` map of ticker → { price, signal, inflatedVerdict, fundamentalVerdict, pe, roe, fcf }.\n\nDepends on {{callId}} being set by the Create Market Call request."
|
||||
"description": "Fetches the call and re-screens all tickers to show how signal/price has changed since creation.\n\nReturns: original call fields + `current` map of ticker \u2192 { price, signal, inflatedVerdict, fundamentalVerdict, pe, roe, fcf }.\n\nDepends on {{callId}} being set by the Create Market Call request."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
@@ -524,7 +579,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get Call — Non-existent ID (expect 404)",
|
||||
"name": "Get Call \u2014 Non-existent ID (expect 404)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/api/calls/00000000-0000-0000-0000-000000000000",
|
||||
@@ -574,13 +629,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get Earnings Calendar — Specific Tickers",
|
||||
"name": "Get Earnings Calendar \u2014 Specific Tickers",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/calls/calendar?tickers=AAPL,MSFT",
|
||||
"query": [
|
||||
{ "key": "tickers", "value": "AAPL,MSFT" }
|
||||
{
|
||||
"key": "tickers",
|
||||
"value": "AAPL,MSFT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Calendar for specific tickers regardless of saved calls."
|
||||
@@ -602,11 +660,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create Call — Validation: short thesis (expect 400)",
|
||||
"name": "Create Call \u2014 Validation: short thesis (expect 400)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/calls",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"title\": \"Test\",\n \"quarter\": \"Q1\",\n \"thesis\": \"short\",\n \"tickers\": [\"AAPL\"]\n}"
|
||||
@@ -644,7 +707,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Delete Call — Already Deleted (expect 404)",
|
||||
"name": "Delete Call \u2014 Already Deleted (expect 404)",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"url": "{{baseUrl}}/api/calls/{{callId}}",
|
||||
@@ -671,7 +734,12 @@
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/analyze",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": [\"NVDA\", \"AMD\", \"INTC\"]\n}"
|
||||
@@ -709,11 +777,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Analyze — Validation: empty tickers (expect 400)",
|
||||
"name": "Analyze \u2014 Validation: empty tickers (expect 400)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "{{baseUrl}}/api/analyze",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tickers\": []\n}"
|
||||
@@ -732,6 +805,347 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Screener \u2014 Ticker Detail & Sectors (June 2026)",
|
||||
"item": [
|
||||
{
|
||||
"name": "Signal history (snapshot ledger)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/screen/history/AAPL",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"screen",
|
||||
"history",
|
||||
"AAPL"
|
||||
]
|
||||
},
|
||||
"description": "Per-day signal/tier/score history from the signal_snapshots ledger (P0.1)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Company profile",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/screen/profile/AAPL",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"screen",
|
||||
"profile",
|
||||
"AAPL"
|
||||
]
|
||||
},
|
||||
"description": "Name, description, sector, market cap + analyst targets (mean/high/low, upside). Cached 1h."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Price chart",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/screen/chart/AAPL?range=6mo",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"screen",
|
||||
"chart",
|
||||
"AAPL"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "range",
|
||||
"value": "6mo"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Closes for the ticker modal chart. range: 1d|5d|1mo|3mo|6mo|ytd|1y|5y (intraday bars for 1d/5d)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Sector pulse",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/screen/sectors",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"screen",
|
||||
"sectors"
|
||||
]
|
||||
},
|
||||
"description": "Today's % change per sector via SPDR ETFs, sorted best\u2192worst, with leader. Cached 15m."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Sector drill-down",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/screen/sector/TECHNOLOGY",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"screen",
|
||||
"sector",
|
||||
"TECHNOLOGY"
|
||||
]
|
||||
},
|
||||
"description": "Top-10 ETF holdings screened + 3-day sector news. Sectors: TECHNOLOGY, FINANCIAL, ENERGY, HEALTHCARE, COMMUNICATION, CONSUMER_STAPLES, CONSUMER_DISCRETIONARY, REIT. Cached 30m."
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "News & Digest (June 2026)",
|
||||
"item": [
|
||||
{
|
||||
"name": "News for ticker (stored + live merge)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/news/AAPL?days=7&live=1",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"news",
|
||||
"AAPL"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "days",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"key": "live",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Stored pipeline stories (EDGAR + PR wires, catalyst-tagged) merged with a live Yahoo search. live=0 for stored only."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Recent news (all tickers)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/news/recent?limit=50",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"news",
|
||||
"recent"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "50"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Daily digest (today)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/digest",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"digest"
|
||||
]
|
||||
},
|
||||
"description": "Signal flips vs previous snapshots + news catalysts + M&A stories."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Daily digest (specific day)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/digest?date=2026-06-12",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"digest"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "date",
|
||||
"value": "2026-06-12"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Auth & Watchlist",
|
||||
"item": [
|
||||
{
|
||||
"name": "Register",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/register",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"auth",
|
||||
"register"
|
||||
]
|
||||
},
|
||||
"description": "Invite code is printed in the server terminal on boot (hidden during tests).",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"you@example.com\",\n \"password\": \"min-8-chars\",\n \"inviteCode\": \"<printed on server boot>\"\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Login",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/login",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"auth",
|
||||
"login"
|
||||
]
|
||||
},
|
||||
"description": "Returns { token } \u2014 save as {{jwt}} collection variable.",
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"you@example.com\",\n \"password\": \"min-8-chars\"\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get watchlist",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{jwt}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/watchlist",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"watchlist"
|
||||
]
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Pin ticker",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{jwt}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/watchlist/AAPL",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"watchlist",
|
||||
"AAPL"
|
||||
]
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Unpin ticker",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{jwt}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/watchlist/AAPL",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"watchlist",
|
||||
"AAPL"
|
||||
]
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+20
-10
@@ -40,6 +40,8 @@ import {
|
||||
interface BuildAppOptions {
|
||||
logger?: boolean;
|
||||
db?: DatabaseConnection;
|
||||
/** Inject a stub in tests to avoid live Yahoo news fetches. */
|
||||
catalystCache?: CatalystCache;
|
||||
}
|
||||
|
||||
// ── JWT auth helpers ─────────────────────────────────────────────────────────
|
||||
@@ -78,7 +80,11 @@ function makeRoleGuard(required: 'trader' | 'admin') {
|
||||
// 3. Create barrel: server/domains/<domain>/index.ts
|
||||
// 4. Import from domain and register controller below
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
export async function buildApp({ logger = true, db: injectedDb }: BuildAppOptions = {}) {
|
||||
export async function buildApp({
|
||||
logger = true,
|
||||
db: injectedDb,
|
||||
catalystCache: injectedCache,
|
||||
}: BuildAppOptions = {}) {
|
||||
const app = Fastify({ logger });
|
||||
|
||||
await app.register(cors, {
|
||||
@@ -126,7 +132,7 @@ export async function buildApp({ logger = true, db: injectedDb }: BuildAppOption
|
||||
const advisor = new PortfolioAdvisor(yahoo);
|
||||
const calSvc = new CalendarService(yahoo);
|
||||
const llm = new LLMAnalyst({ logger: noopLogger });
|
||||
const catalystCache = new CatalystCache({ logger: noopLogger }); // Singleton, cached for 15m
|
||||
const catalystCache = injectedCache ?? new CatalystCache({ logger: noopLogger }); // Singleton, 15m cache
|
||||
|
||||
// Auth domain — generate a fresh invite code on every boot and print it
|
||||
const INVITE_CODE = randomBytes(12).toString('hex'); // 24-char hex string
|
||||
@@ -136,14 +142,18 @@ export async function buildApp({ logger = true, db: injectedDb }: BuildAppOption
|
||||
const innerWidth = Math.max(line1.length, line2.length) + 2;
|
||||
const hr = '─'.repeat(innerWidth);
|
||||
const pad = (s: string) => `│ ${s}${' '.repeat(innerWidth - 1 - s.length)}│`;
|
||||
/* eslint-disable no-console -- boot-time invite code must reach the operator's terminal */
|
||||
console.log(`\n┌${hr}┐`);
|
||||
console.log(pad(''));
|
||||
console.log(pad(line1));
|
||||
console.log(pad(line2));
|
||||
console.log(pad(''));
|
||||
console.log(`└${hr}┘\n`);
|
||||
/* eslint-enable no-console */
|
||||
// Never print the invite code when the logger is disabled (tests) — secrets
|
||||
// don't belong in test output.
|
||||
if (logger !== false) {
|
||||
/* eslint-disable no-console -- boot-time invite code must reach the operator's terminal */
|
||||
console.log(`\n┌${hr}┐`);
|
||||
console.log(pad(''));
|
||||
console.log(pad(line1));
|
||||
console.log(pad(line2));
|
||||
console.log(pad(''));
|
||||
console.log(`└${hr}┘\n`);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
const userStore = new UserStore(db);
|
||||
const authService = new AuthService(userStore, JWT_SECRET);
|
||||
|
||||
@@ -35,12 +35,14 @@ export class ScreenerEngine {
|
||||
private static readonly BATCH_DELAY_MS = 1000;
|
||||
|
||||
private logger: Logger;
|
||||
private readonly batchDelayMs: number;
|
||||
|
||||
constructor(
|
||||
private readonly client: YahooFinanceClient,
|
||||
private readonly benchmarkProvider: BenchmarkProvider,
|
||||
{ logger }: ScreenerEngineOptions = {},
|
||||
{ logger, batchDelayMs }: ScreenerEngineOptions = {},
|
||||
) {
|
||||
this.batchDelayMs = batchDelayMs ?? ScreenerEngine.BATCH_DELAY_MS;
|
||||
// eslint-disable-next-line no-console
|
||||
this.logger = logger ?? {
|
||||
write: (msg: string) => process.stdout.write(msg),
|
||||
@@ -65,11 +67,12 @@ export class ScreenerEngine {
|
||||
const chunks = chunkArray(tickers, ScreenerEngine.BATCH_SIZE);
|
||||
let processed = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await this.processBatch(chunk, marketContext, results);
|
||||
processed += chunk.length;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
await this.processBatch(chunks[i], marketContext, results);
|
||||
processed += chunks[i].length;
|
||||
this.logProgress(showProgress, processed, tickers.length);
|
||||
await this.rateLimitDelay();
|
||||
// Rate-limit pause between batches — never after the last one
|
||||
if (i < chunks.length - 1) await this.rateLimitDelay();
|
||||
}
|
||||
|
||||
if (showProgress) {
|
||||
@@ -110,7 +113,8 @@ export class ScreenerEngine {
|
||||
}
|
||||
|
||||
private async rateLimitDelay(): Promise<void> {
|
||||
await new Promise<void>((r) => setTimeout(r, ScreenerEngine.BATCH_DELAY_MS));
|
||||
if (this.batchDelayMs <= 0) return;
|
||||
await new Promise<void>((r) => setTimeout(r, this.batchDelayMs));
|
||||
}
|
||||
|
||||
private async fetch(ticker: string): Promise<MappedData | ErrorResult> {
|
||||
|
||||
@@ -90,4 +90,6 @@ export interface RuleSet {
|
||||
// ── ScreenerEngine ────────────────────────────────────────────────────────
|
||||
export interface ScreenerEngineOptions {
|
||||
logger?: Logger;
|
||||
/** Delay between Yahoo batches (ms). Default 1000; set 0 in tests. */
|
||||
batchDelayMs?: number;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ const MOCK_LLM_RESPONSE = JSON.stringify({
|
||||
|
||||
const mockDb = new MockDatabaseConnection() as never;
|
||||
|
||||
// Stub catalyst cache — no live Yahoo news fetches in tests (fast + offline)
|
||||
const stubCatalystCache = {
|
||||
get: async () => ({ tickers: [] as string[], stories: [] }),
|
||||
} as never;
|
||||
|
||||
test('POST /api/analyze', async (t) => {
|
||||
// Spy on AnthropicClient.prototype.complete before buildApp wires it up.
|
||||
// This prevents any real API calls during tests.
|
||||
@@ -26,7 +31,7 @@ test('POST /api/analyze', async (t) => {
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
||||
|
||||
await t.test('returns analysis when stories match tickers', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
@@ -47,7 +52,7 @@ test('POST /api/analyze', async (t) => {
|
||||
// Reset the isAvailable mock to simulate no API key
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => false, { getter: true });
|
||||
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
@@ -68,7 +73,7 @@ test('POST /api/analyze', async (t) => {
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
||||
|
||||
const callsBefore = completeSpy.mock.calls.length;
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
|
||||
+13
-8
@@ -6,15 +6,20 @@ import { MockDatabaseConnection } from './helpers/mockDb.js';
|
||||
// Inject mock DB so tests don't require the native better-sqlite3 binary
|
||||
const mockDb = new MockDatabaseConnection() as never;
|
||||
|
||||
// Stub catalyst cache — no live Yahoo news fetches in tests (fast + offline)
|
||||
const stubCatalystCache = {
|
||||
get: async () => ({ tickers: [] as string[], stories: [] }),
|
||||
} as never;
|
||||
|
||||
test('App Bootstrap', async (t) => {
|
||||
await t.test('builds successfully without logger', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
assert.ok(app);
|
||||
assert.ok(app.server);
|
||||
});
|
||||
|
||||
await t.test('health check endpoint returns 200', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/health',
|
||||
@@ -25,7 +30,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('POST /api/screen requires valid schema', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/screen',
|
||||
@@ -36,7 +41,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('POST /api/screen rejects invalid payload', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/screen',
|
||||
@@ -46,7 +51,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('GET /api/screen/catalysts returns results', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/screen/catalysts',
|
||||
@@ -58,7 +63,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('CORS is enabled for configured origin', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/health',
|
||||
@@ -70,7 +75,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('API key auth is optional (disabled by default)', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/health',
|
||||
@@ -80,7 +85,7 @@ test('App Bootstrap', async (t) => {
|
||||
});
|
||||
|
||||
await t.test('OPTIONS requests bypass auth check', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
const app = await buildApp({ logger: false, db: mockDb, catalystCache: stubCatalystCache });
|
||||
const response = await app.inject({
|
||||
method: 'OPTIONS',
|
||||
url: '/api/screen',
|
||||
|
||||
@@ -84,6 +84,28 @@ test('BondScorer', async (t) => {
|
||||
assert.ok(resultLong.audit?.passedGates);
|
||||
});
|
||||
|
||||
await t.test('returns structured tier (P0.3)', () => {
|
||||
const good: BondMetrics = {
|
||||
ytm: 7.5, // 7.5% vs ~4% risk-free → wide spread
|
||||
duration: 5,
|
||||
creditRating: 'A',
|
||||
creditRatingNumeric: 8,
|
||||
};
|
||||
const pass = BondScorer.score(good, DEFAULT_RULES);
|
||||
assert.equal(pass.tier, 'PASS');
|
||||
assert.equal(typeof pass.score, 'number');
|
||||
|
||||
const junk: BondMetrics = {
|
||||
ytm: 8,
|
||||
duration: 5,
|
||||
creditRating: 'CCC',
|
||||
creditRatingNumeric: 4, // below investment-grade gate
|
||||
};
|
||||
const reject = BondScorer.score(junk, DEFAULT_RULES);
|
||||
assert.equal(reject.tier, 'REJECT');
|
||||
assert.equal(reject.score, null);
|
||||
});
|
||||
|
||||
await t.test('handles null/undefined metrics gracefully', () => {
|
||||
const metrics = {
|
||||
ytm: null,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"sass": "^1.100.0",
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
export default {
|
||||
kit: {
|
||||
|
||||
Reference in New Issue
Block a user