feat: raise market_api rate to 20/min, add global Polygon cap at 45/min, add rate-limit API + watchlist warning

This commit is contained in:
Celes Renata
2026-04-16 07:26:10 +00:00
parent 0b3ab4ed90
commit 0ee7f26633
5 changed files with 148 additions and 9 deletions
+18
View File
@@ -717,6 +717,24 @@ export function useTrendProjection(trendId: string | undefined) {
return useGet<TrendProjection>(['trend-projection', trendId], 'query', `/api/trends/${trendId}/projection`, !!trendId);
}
// ---------------------------------------------------------------------------
// System: Rate Limits
// ---------------------------------------------------------------------------
export interface RateLimitInfo {
polygon_global_limit: number;
market_api: {
rate_per_minute: number;
cadence_seconds: number;
max_tickers_per_cycle: number;
active_sources: number;
};
}
export function useRateLimits() {
return useGet<RateLimitInfo>(['rate-limits'], 'query', '/api/system/rate-limits');
}
// ---------------------------------------------------------------------------
// Macro: Admin Toggle (Task 17.6)
// ---------------------------------------------------------------------------
+45 -4
View File
@@ -1,9 +1,12 @@
import { useState } from 'react';
import { useWatchlists, useWatchlistMembers, useCreateWatchlist } from '../api/hooks';
import { useWatchlists, useWatchlistMembers, useCreateWatchlist, useRateLimits } from '../api/hooks';
import type { Company } from '../api/hooks';
import { LoadingSpinner, Card } from '../components/ui';
import { AlertTriangle } from 'lucide-react';
export function WatchlistsPage() {
const { data: watchlists, isLoading } = useWatchlists();
const { data: rateLimits } = useRateLimits();
const [selected, setSelected] = useState<string | null>(null);
const [showCreate, setShowCreate] = useState(false);
@@ -34,21 +37,44 @@ export function WatchlistsPage() {
))}
</div>
{selected && <WatchlistMembers watchlistId={selected} />}
{selected && <WatchlistMembers watchlistId={selected} rateLimits={rateLimits} />}
</div>
);
}
function WatchlistMembers({ watchlistId }: { watchlistId: string }) {
interface RateLimitInfo {
polygon_global_limit: number;
market_api: {
rate_per_minute: number;
cadence_seconds: number;
max_tickers_per_cycle: number;
active_sources: number;
};
}
function WatchlistMembers({ watchlistId, rateLimits }: { watchlistId: string; rateLimits?: RateLimitInfo }) {
const { data: members, isLoading } = useWatchlistMembers(watchlistId);
if (isLoading) return <LoadingSpinner />;
if (!members?.length) return <p className="text-sm text-gray-500">No members in this watchlist</p>;
const maxTickers = rateLimits?.market_api.max_tickers_per_cycle;
const activeSources = rateLimits?.market_api.active_sources ?? 0;
const cadenceSec = rateLimits?.market_api.cadence_seconds ?? 300;
const cadenceMin = Math.round(cadenceSec / 60);
const overCapacity = maxTickers != null && activeSources > maxTickers;
return (
<Card>
<h2 className="mb-2 text-sm font-medium text-gray-400">Members</h2>
{overCapacity && (
<RateLimitWarning
activeSources={activeSources}
maxTickers={maxTickers!}
cadenceMin={cadenceMin}
/>
)}
<div className="flex flex-wrap gap-2">
{members.map((m) => (
{members.map((m: Company) => (
<span key={m.id} className="rounded-full border border-surface-700 bg-surface-800 px-3 py-1 text-xs text-gray-300">
{m.ticker} {m.legal_name}
</span>
@@ -58,6 +84,21 @@ function WatchlistMembers({ watchlistId }: { watchlistId: string }) {
);
}
function RateLimitWarning({ activeSources, maxTickers, cadenceMin }: { activeSources: number; maxTickers: number; cadenceMin: number }) {
const cycleMinutes = Math.ceil(activeSources / (maxTickers / cadenceMin));
return (
<div className="mb-3 flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-400" aria-hidden="true" />
<div className="text-xs text-amber-300">
<span className="font-medium">Rate limit warning:</span>{' '}
{activeSources} active tickers exceed the {maxTickers}-ticker refresh capacity
per {cadenceMin}-minute cycle. Full refresh will take ~{cycleMinutes} minutes
instead of {cadenceMin}. Remove tickers or increase the rate limit to improve freshness.
</div>
</div>
);
}
function CreateWatchlistForm({ onClose }: { onClose: () => void }) {
const [name, setName] = useState('');
const [desc, setDesc] = useState('');