Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/backtest-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Backtest report artifact

on:
workflow_dispatch:

jobs:
backtest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install
run: pnpm install --frozen-lockfile

- name: Run backtest
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
run: mkdir -p reports && pnpm ci:backtest

- name: Upload report
uses: actions/upload-artifact@v4
with:
name: backtest-report
path: reports/BACKTEST_REPORT.md
if-no-files-found: warn
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install
run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm typecheck

- name: Test (CI ladder)
run: pnpm test:ci
29 changes: 29 additions & 0 deletions .github/workflows/collect-resolutions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Collect resolutions

on:
workflow_dispatch:

jobs:
collect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'

- name: Install
run: pnpm install --frozen-lockfile

- name: Collect resolutions
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
COLLECT_RESOLUTIONS_FAIL_ON_ERROR: '1'
run: pnpm collect:resolutions
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,16 @@ node_modules
dist
.tmp
coverage
reports/
BACKTEST_REPORT.md
src/ml/models/*.json
!src/ml/models/.gitkeep
!src/ml/models/README.md

# No Office binaries in git
*.docx
*.xlsx
*.pptx
*.doc
*.xls
*.ppt
54 changes: 41 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,53 @@

It keeps the shared prediction-market intelligence stack that used to live inside the monolithic `Musashi/` project:

- REST API handlers in [`api/`](../musashi-api/api)
- analysis pipeline in [`src/analysis/`](../musashi-api/src/analysis)
- market/Twitter clients in [`src/api/`](../musashi-api/src/api)
- SDK client in [`src/sdk/`](../musashi-api/src/sdk)
- Supabase schema and the auxiliary backend server in [`server/`](../musashi-api/server)
- REST API handlers in [`api/`](./api)
- Analysis pipeline in [`src/analysis/`](./src/analysis)
- Market/Twitter clients in [`src/api/`](./src/api)
- SDK client in [`src/sdk/`](./src/sdk)
- Supabase schema and the auxiliary backend server in [`server/`](./server)

## Goal

This repo is the new source of truth for shared functionality. Both `musashi-extension` and `musashi-mcp` should consume this API instead of importing code from the old `Musashi/` directory.
This repo is the source of truth for shared functionality. Consumers should call this API instead of importing code from legacy monolith paths.

## Interview narrative (keep it honest)

No project **guarantees** an internship—recruiters also weigh timing, referrals, and how you communicate. This repo **does** give you concrete talking points many candidates lack: production-style API wiring, explicit feature flags, a **closed-loop ML story** (log → resolve → measure → backtest), and honest limits (mid-price vs executable edge, serverless constraints).

Before a call, run **`pnpm interview:check`** (same checks as CI plus pitch prompts). In production, **`GET /api/health`** includes **`operational_readiness`** booleans derived from env (Supabase, KV, internal routes) so you can show configuration discipline without opening the dashboard.

## Scripts

- `pnpm dev`: run the local API shim on `http://127.0.0.1:3000`
- `pnpm backend:dev`: run the Supabase-backed auxiliary backend from [`server/api-server.mjs`](../musashi-api/server/api-server.mjs)
- `pnpm test:agent`: run the API/SDK smoke and contract tests against URL `https://musashi-api.vercel.app`
- `pnpm test:agent:local`: run the same agent test suite against the local API at `http://127.0.0.1:3000`
- `pnpm typecheck`: type-check core sources plus Vercel API handlers
| Command | Description |
|---------|-------------|
| `pnpm dev` | Local API shim on `http://127.0.0.1:3000` |
| `pnpm backend:dev` | Supabase-backed auxiliary backend from [`server/api-server.mjs`](./server/api-server.mjs) |
| `pnpm test:agent` | Contract/smoke tests against production URL (`MUSASHI_API_BASE_URL` overrides — preview or local) |
| `pnpm test:agent:local` | Same suite against `http://127.0.0.1:3000` |
| `pnpm test:ci` | **Required ladder:** typecheck + smoke imports + wallet tests |
| `pnpm typecheck` | Core sources + Vercel API handlers |
| `pnpm collect:resolutions` | Batch-update `signal_outcomes` from venue resolutions ([`scripts/ml/collect-resolutions.ts`](./scripts/ml/collect-resolutions.ts)) |
| `pnpm ci:backtest` | Writes `reports/BACKTEST_REPORT.md` (needs Supabase env; see [`scripts/backtest/run-backtest.ts`](./scripts/backtest/run-backtest.ts)) |
| `pnpm interview:check` | Runs `test:ci` then prints interview talking points ([`scripts/interview-ready.ts`](./scripts/interview-ready.ts)) |

## Environment & deployment

- **Full flag matrix:** [`docs/ENVIRONMENT.md`](./docs/ENVIRONMENT.md)
- **Deploy checklist (Supabase migrations, Vercel secrets):** [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md)
- **Testing ladder & preview URLs:** [`docs/TESTING.md`](./docs/TESTING.md)
- **`sharp` / transformers troubleshooting:** [`docs/NATIVE_DEPS.md`](./docs/NATIVE_DEPS.md)
- **Polymarket WS operations (top-N, backpressure):** [`docs/WS_STRATEGY.md`](./docs/WS_STRATEGY.md)
- **Portfolio / correlation risk (beyond session API):** [`docs/PORTFOLIO_RISK.md`](./docs/PORTFOLIO_RISK.md)

Key toggles: `MUSASHI_POLYMARKET_WS`, cache TTLs (`MARKET_CACHE_TTL_SECONDS`, `ARBITRAGE_CACHE_TTL_SECONDS`), risk thresholds (`RISK_CAUTION_THRESHOLD`, `RISK_HALT_THRESHOLD`), `MUSASHI_DISABLE_SEMANTIC_MATCHING`, `MUSASHI_ML_SHADOW`.

## Notes

- The original reference docs were copied in `*.upstream.md` files so functionality and historical guidance remain available in this split repo.
- `vercel.json` now includes the `ground-probability` route so local and deployed API behavior stay aligned.
- Reference case study materials are stored externally — contact the team for access.
- Historical reference docs remain in `*.upstream.md` files where present.
- `vercel.json` routes must stay aligned with handlers under [`api/`](./api); [`api/health.ts`](./api/health.ts) summarizes supported endpoints.

## Submitting / shipping

- **PR vs email, applications, course hand-in:** [`docs/SUBMISSION.md`](./docs/SUBMISSION.md)
88 changes: 57 additions & 31 deletions api/analyze-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@ import type { VercelRequest, VercelResponse } from '@vercel/node';
import { KeywordMatcher } from '../src/analysis/keyword-matcher';
import { generateSignal, TradingSignal } from '../src/analysis/signal-generator';
import { getMarkets, getArbitrage, getMarketMetadata } from './lib/market-cache';
import { VolatilityRegime } from '../src/analysis/kelly-sizing';
import {
getClientIp,
isRateLimited,
parsePositiveIntEnv,
} from './lib/rate-limit';

function isMalformedJsonError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}

if (error instanceof SyntaxError) {
return true;
}

const message = error.message.toLowerCase();
return (
message.includes('json') ||
message.includes('unexpected token') ||
message.includes('request body')
);
if (!(error instanceof Error)) return false;
if (error instanceof SyntaxError) return true;
const msg = error.message.toLowerCase();
return msg.includes('json') || msg.includes('unexpected token') || msg.includes('request body');
}

export default async function handler(
Expand All @@ -29,13 +25,11 @@ export default async function handler(
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

// Handle preflight
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}

// Only accept POST
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST, OPTIONS');
res.status(405).json({
Expand All @@ -48,13 +42,27 @@ export default async function handler(
return;
}

const analyzeLimit = parsePositiveIntEnv('MUSASHI_ANALYZE_TEXT_RATE_LIMIT_PER_MIN', 120);
if (isRateLimited(`analyze:${getClientIp(req)}`, analyzeLimit)) {
res.status(429).json({
event_id: 'evt_error',
signal_type: 'user_interest',
urgency: 'low',
success: false,
error: 'Too many requests. Retry later.',
});
return;
}

const startTime = Date.now();

try {
const body = req.body as {
text: string;
minConfidence?: number;
maxResults?: number;
vol_regime?: VolatilityRegime;
use_ml_scorer?: boolean;
} | null;

if (!body || typeof body !== 'object' || Array.isArray(body)) {
Expand All @@ -68,7 +76,6 @@ export default async function handler(
return;
}

// Validate request
if (!body.text || typeof body.text !== 'string') {
res.status(400).json({
event_id: 'evt_error',
Expand All @@ -80,8 +87,7 @@ export default async function handler(
return;
}

// Validate text length (prevent abuse)
if (body.text.length > 10000) {
if (body.text.length > 10_000) {
res.status(400).json({
event_id: 'evt_error',
signal_type: 'user_interest',
Expand All @@ -93,8 +99,12 @@ export default async function handler(
}

const { text, minConfidence = 0.3, maxResults = 5 } = body;
const useMlScorer = body.use_ml_scorer === true;

// Optional volatility regime hint from caller (e.g. from their own regime detector)
const volRegime: VolatilityRegime =
body.vol_regime === 'low' || body.vol_regime === 'high' ? body.vol_regime : 'normal';

// Validate numeric parameters
if (
typeof minConfidence !== 'number' ||
!Number.isFinite(minConfidence) ||
Expand Down Expand Up @@ -127,7 +137,17 @@ export default async function handler(
return;
}

// Get markets
if (body.use_ml_scorer !== undefined && typeof body.use_ml_scorer !== 'boolean') {
res.status(400).json({
event_id: 'evt_error',
signal_type: 'user_interest',
urgency: 'low',
success: false,
error: 'use_ml_scorer must be a boolean when provided.',
});
return;
}

const markets = await getMarkets();

if (markets.length === 0) {
Expand All @@ -141,28 +161,28 @@ export default async function handler(
return;
}

// Match markets
const matcher = new KeywordMatcher(markets, minConfidence, maxResults);
const matches = matcher.match(text);

// Get cached arbitrage opportunities
// Filter out anomalous markets from arbitrage consideration
const arbitrageOpportunities = await getArbitrage(0.03);
Comment on lines +167 to 168
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says “Filter out anomalous markets from arbitrage consideration”, but the code only filters is_directionally_opposed. Either update the comment to reflect what’s actually being filtered, or implement the anomalous-market filter here (e.g., based on market.is_anomalous).

Copilot uses AI. Check for mistakes.
let arbitrageForSignal = undefined;

if (matches.length > 0 && arbitrageOpportunities.length > 0) {
const topMatchId = matches[0].market.id;
arbitrageForSignal = arbitrageOpportunities.find(
arb => arb.polymarket.id === topMatchId || arb.kalshi.id === topMatchId
arb =>
(arb.polymarket.id === topMatchId || arb.kalshi.id === topMatchId) &&
!arb.is_directionally_opposed // Skip false positives
);
}

// Generate trading signal
const signal: TradingSignal = generateSignal(text, matches, arbitrageForSignal);
const signal: TradingSignal = generateSignal(text, matches, arbitrageForSignal, volRegime, {
use_ml_scorer: useMlScorer,
});

// Stage 0: Get freshness metadata
const freshnessMetadata = getMarketMetadata();

// Build response
const response = {
event_id: signal.event_id,
signal_type: signal.signal_type,
Expand All @@ -175,12 +195,18 @@ export default async function handler(
suggested_action: signal.suggested_action,
sentiment: signal.sentiment,
arbitrage: signal.arbitrage,
// ── New fields ──────────────────────────────────────────────────
valid_until_seconds: signal.valid_until_seconds,
is_near_resolution: signal.is_near_resolution,
vol_regime: volRegime,
use_ml_scorer: useMlScorer,
ml_score: signal.ml_score,
ml_score_shadow: signal.ml_score_shadow,
metadata: {
processing_time_ms: Date.now() - startTime,
sources_checked: 2, // Polymarket + Kalshi
sources_checked: 2,
markets_analyzed: markets.length,
model_version: 'v2.0.0',
// Stage 0: Freshness metadata
model_version: 'v3.0.0',
data_age_seconds: freshnessMetadata.data_age_seconds,
fetched_at: freshnessMetadata.fetched_at,
sources: freshnessMetadata.sources,
Expand Down
Loading