@claudinho/core 0.2.0 → 0.3.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.
- package/README.md +5 -3
- package/dist/index.d.ts +307 -1
- package/dist/index.js +461 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @claudinho/core ⚽
|
|
2
2
|
|
|
3
|
-
Shared domain model, data-provider adapters,
|
|
4
|
-
the 2026 men's football tournament in your dev
|
|
5
|
-
|
|
3
|
+
Shared domain model, data-provider adapters, a read-only market-signal sidecar,
|
|
4
|
+
and helpers for **Claudinho** — the 2026 men's football tournament in your dev
|
|
5
|
+
environment. This is the engine behind
|
|
6
|
+
[`@claudinho/cli`](https://www.npmjs.com/package/@claudinho/cli) and
|
|
6
7
|
[`@claudinho/mcp`](https://www.npmjs.com/package/@claudinho/mcp).
|
|
7
8
|
|
|
8
9
|
> ⚠️ **Not affiliated with, endorsed by, or connected to FIFA or Anthropic.**
|
|
@@ -22,6 +23,7 @@ npm i @claudinho/core
|
|
|
22
23
|
- **Live overlay** — `makeAdapter`, `getMatchesForDate`, `getLiveMatches`, `mergeLive` (static base + live state, with graceful degradation)
|
|
23
24
|
- **Standings** — `computeStandings` (points / GD / GF tiebreak)
|
|
24
25
|
- **Helpers** — emoji flags (`nationToFlag`), TZ-aware time (`formatKickoff`, `countdown`, `localDate`), location strings (`matchLocation`), localized commentary flair (`matchFlavor` / `FlavorLevel`), validators (`isValidDate`, `isValidTimeZone`)
|
|
26
|
+
- **Prediction-market signals (sidecar)** — read-only market odds kept *separate* from `Match`: the `MarketSignal` / `MarketProvider` model, the `PolymarketProvider` (public Gamma data only — no auth/trading/links; event slugs auto-derived per fixture, validation fails closed), a `FakeMarketProvider`, `makeMarketProvider`, `getMarketSignal` / `getMarketSignals`, the `isReliableMarketSignal` gate, and approved-copy formatters (`marketFavoriteText`, `marketProbabilityText`, `marketBlock`). Informational only — never betting advice.
|
|
25
27
|
|
|
26
28
|
## Example
|
|
27
29
|
|
package/dist/index.d.ts
CHANGED
|
@@ -394,7 +394,15 @@ interface LiveResult {
|
|
|
394
394
|
matches: Match[];
|
|
395
395
|
/** True when the provider call failed and we fell back to static data. */
|
|
396
396
|
degraded: boolean;
|
|
397
|
+
/**
|
|
398
|
+
* The live-data provider that served this result (e.g. "espn"), for
|
|
399
|
+
* attribution. Absent when `degraded` — the bundled static schedule, served
|
|
400
|
+
* by no live provider, must not be attributed to one.
|
|
401
|
+
*/
|
|
402
|
+
source?: string;
|
|
397
403
|
}
|
|
404
|
+
/** Human label for a live-data provider name (attribution). Text only. */
|
|
405
|
+
declare function liveSourceLabel(source: string): string;
|
|
398
406
|
/**
|
|
399
407
|
* Matches for a date, preferring live provider data, falling back to the static
|
|
400
408
|
* schedule on any provider/network error (graceful degradation).
|
|
@@ -403,4 +411,302 @@ declare function getMatchesForDate(adapter: ProviderAdapter, dateISO: string): P
|
|
|
403
411
|
/** Currently-live matches; empty + degraded on error. */
|
|
404
412
|
declare function getLiveMatches(adapter: ProviderAdapter): Promise<LiveResult>;
|
|
405
413
|
|
|
406
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Prediction-market "signal" model — a *sidecar* to Match, deliberately never
|
|
416
|
+
* embedded in it. Market data has different freshness, reliability, failure,
|
|
417
|
+
* and legal semantics than tournament facts, so it lives in its own folder and
|
|
418
|
+
* is keyed back to a match by id. This keeps prediction-market context off the
|
|
419
|
+
* hot paths (statusline, hook) by construction rather than by remembering a flag.
|
|
420
|
+
*
|
|
421
|
+
* Read-only by design: providers fetch public market data only — no wallet,
|
|
422
|
+
* no auth, no order placement. Nothing here models trading.
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
/** Which match result a priced line refers to (home win / draw / away win). */
|
|
426
|
+
type MarketOutcomeKind = 'home' | 'draw' | 'away' | 'other';
|
|
427
|
+
/** One priced outcome of a match market. */
|
|
428
|
+
interface MarketOutcome {
|
|
429
|
+
kind: MarketOutcomeKind;
|
|
430
|
+
/** Team code for 'home'/'away' (e.g. "MEX"); absent for draw/other. */
|
|
431
|
+
teamCode?: string;
|
|
432
|
+
/** Label as the source displays it (e.g. "Mexico", "Draw"). */
|
|
433
|
+
label: string;
|
|
434
|
+
/** Market-implied probability, normalized to [0,1] (sums to ~1 across a market). */
|
|
435
|
+
probability: number;
|
|
436
|
+
}
|
|
437
|
+
/** How strongly the market leans toward its top outcome. */
|
|
438
|
+
type FavoriteStrength = 'close' | 'slight' | 'clear';
|
|
439
|
+
/** The top outcome plus its strength bucket. */
|
|
440
|
+
interface MarketFavorite {
|
|
441
|
+
kind: 'home' | 'draw' | 'away';
|
|
442
|
+
teamCode?: string;
|
|
443
|
+
probability: number;
|
|
444
|
+
strength: FavoriteStrength;
|
|
445
|
+
}
|
|
446
|
+
/** A normalized prediction-market reading for a single match. */
|
|
447
|
+
interface MarketSignal {
|
|
448
|
+
/** Claudinho match id this signal maps to. */
|
|
449
|
+
matchId: string;
|
|
450
|
+
/** Provider name (e.g. "polymarket"). */
|
|
451
|
+
source: string;
|
|
452
|
+
/**
|
|
453
|
+
* Opaque source market id, for debugging and mapping only. Deliberately never
|
|
454
|
+
* surfaced as a clickable link in v1 (see the no-outbound-links guardrail).
|
|
455
|
+
*/
|
|
456
|
+
sourceMarketId?: string;
|
|
457
|
+
/** When the source last priced the market (ISO 8601 UTC). */
|
|
458
|
+
asOf: string;
|
|
459
|
+
/** When Claudinho fetched it (ISO 8601 UTC). */
|
|
460
|
+
fetchedAt: string;
|
|
461
|
+
/** Priced outcomes, normalized so positive probabilities sum to ~1. */
|
|
462
|
+
outcomes: MarketOutcome[];
|
|
463
|
+
/** Top outcome, when one can be determined from a clean mapping. */
|
|
464
|
+
favorite?: MarketFavorite;
|
|
465
|
+
/** Source liquidity, when available (provider units; used only for gating). */
|
|
466
|
+
liquidity?: number;
|
|
467
|
+
/** Source 24h volume, when available (provider units). */
|
|
468
|
+
volume24h?: number;
|
|
469
|
+
/** True when the snapshot is older than the freshness window. */
|
|
470
|
+
stale: boolean;
|
|
471
|
+
/** True when the market does not map cleanly to the displayed home/draw/away result. */
|
|
472
|
+
ambiguous: boolean;
|
|
473
|
+
}
|
|
474
|
+
/** Tunables for reliability gating and provider fetch behavior. */
|
|
475
|
+
interface MarketSignalOptions {
|
|
476
|
+
/** "Now" for staleness math; defaults to the current time. */
|
|
477
|
+
now?: Date;
|
|
478
|
+
/** Minimum source liquidity required to treat a signal as reliable. */
|
|
479
|
+
minLiquidity?: number;
|
|
480
|
+
/** Max age (ms) before a signal is considered stale. */
|
|
481
|
+
maxAgeMs?: number;
|
|
482
|
+
/**
|
|
483
|
+
* Bypass reliability gates — used by the dedicated `markets` surface, which
|
|
484
|
+
* may show a thin/stale market *with a caveat*. Default surfaces never set this.
|
|
485
|
+
*/
|
|
486
|
+
includeUnreliable?: boolean;
|
|
487
|
+
/**
|
|
488
|
+
* Max total wall-clock (ms) for a batch `findSignals` before it stops early.
|
|
489
|
+
* Keeps optional market enrichment from blocking core fixture output.
|
|
490
|
+
*/
|
|
491
|
+
deadlineMs?: number;
|
|
492
|
+
/** Per-request fetch timeout (ms) override for the provider. */
|
|
493
|
+
timeoutMs?: number;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Result of a batch lookup. `checked` is the set of match ids the provider
|
|
497
|
+
* DEFINITIVELY resolved (reached the source and found no usable market, or the
|
|
498
|
+
* fixture is unmappable) — distinct from matches that errored or were skipped by
|
|
499
|
+
* the deadline. Callers negative-cache only `checked` ids, so a transient
|
|
500
|
+
* provider/network failure never suppresses a valid signal.
|
|
501
|
+
*/
|
|
502
|
+
interface MarketSignalsResult {
|
|
503
|
+
signals: Map<string, MarketSignal>;
|
|
504
|
+
checked: Set<string>;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* A prediction-market provider. A *separate* swap-point from ProviderAdapter
|
|
508
|
+
* (which supplies match data): different cadence, reliability, and legal
|
|
509
|
+
* posture. Implementations fetch public market data only.
|
|
510
|
+
*/
|
|
511
|
+
interface MarketProvider {
|
|
512
|
+
readonly name: string;
|
|
513
|
+
/** Signal for one match, or undefined when nothing maps cleanly. */
|
|
514
|
+
findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
|
|
515
|
+
/** Batch form; signals plus the set of definitively-checked ids. */
|
|
516
|
+
findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Probability normalization + the reliability gate. `isReliableMarketSignal` is
|
|
521
|
+
* the single primitive every default-on surface keys off: "show only when
|
|
522
|
+
* reliable, otherwise omit silently."
|
|
523
|
+
*/
|
|
524
|
+
|
|
525
|
+
/** Default freshness window: a signal older than this is stale (15 minutes). */
|
|
526
|
+
declare const DEFAULT_MAX_AGE_MS: number;
|
|
527
|
+
/**
|
|
528
|
+
* Re-scale outcome probabilities so the positive ones sum to 1. This removes
|
|
529
|
+
* market "vig" (raw prices sum to >1) and is scale-agnostic: inputs may be
|
|
530
|
+
* 0..1 or 0..100 and the result is a clean [0,1] distribution. Non-finite or
|
|
531
|
+
* non-positive inputs collapse to 0.
|
|
532
|
+
*/
|
|
533
|
+
declare function normalizeOutcomes(outcomes: MarketOutcome[]): MarketOutcome[];
|
|
534
|
+
/** Bucket a favorite's probability into a strength label. */
|
|
535
|
+
declare function favoriteStrength(probability: number): FavoriteStrength;
|
|
536
|
+
/** Pick the top home/draw/away outcome as the favorite, if one exists. */
|
|
537
|
+
declare function deriveFavorite(outcomes: MarketOutcome[]): MarketFavorite | undefined;
|
|
538
|
+
/**
|
|
539
|
+
* Does this market cleanly price the displayed home/draw/away result? Rejects
|
|
540
|
+
* "to advance"/"to win tournament"-style markets (an 'other' outcome), markets
|
|
541
|
+
* whose team codes don't match the fixture, and group-stage markets missing a
|
|
542
|
+
* draw line (a 90' group result must price the draw).
|
|
543
|
+
*/
|
|
544
|
+
declare function mapsCleanly(match: Match, outcomes: MarketOutcome[]): boolean;
|
|
545
|
+
/** Sanity check: ≥2 priced outcomes whose probabilities sum to ~1. */
|
|
546
|
+
declare function hasSaneDistribution(outcomes: MarketOutcome[]): boolean;
|
|
547
|
+
/** Is the signal older than the freshness window? Unparseable timestamps are stale. */
|
|
548
|
+
declare function isStaleSignal(signal: MarketSignal, options?: MarketSignalOptions): boolean;
|
|
549
|
+
/**
|
|
550
|
+
* The load-bearing gate. A signal is reliable only when it is unambiguous, has
|
|
551
|
+
* a determinable favorite, is a sane distribution, is fresh, and (when a floor
|
|
552
|
+
* is configured) liquid enough. `includeUnreliable` bypasses all of it.
|
|
553
|
+
*/
|
|
554
|
+
declare function isReliableMarketSignal(signal: MarketSignal, options?: MarketSignalOptions): boolean;
|
|
555
|
+
/** Inputs a provider supplies to build a normalized, gated signal. */
|
|
556
|
+
interface BuildSignalInput {
|
|
557
|
+
match: Match;
|
|
558
|
+
source: string;
|
|
559
|
+
sourceMarketId?: string;
|
|
560
|
+
asOf: string;
|
|
561
|
+
fetchedAt?: string;
|
|
562
|
+
/** Raw outcomes (any positive scale); normalized internally. */
|
|
563
|
+
outcomes: MarketOutcome[];
|
|
564
|
+
liquidity?: number;
|
|
565
|
+
volume24h?: number;
|
|
566
|
+
/** Force-flag as ambiguous (e.g. the source title didn't parse to a clean result). */
|
|
567
|
+
ambiguous?: boolean;
|
|
568
|
+
now?: Date;
|
|
569
|
+
maxAgeMs?: number;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Construct a normalized MarketSignal from a provider's raw parts. Centralizes
|
|
573
|
+
* normalization, clean-mapping detection, favorite derivation, and staleness so
|
|
574
|
+
* every provider produces identically-shaped, gate-ready signals.
|
|
575
|
+
*/
|
|
576
|
+
declare function buildMarketSignal(input: BuildSignalInput): MarketSignal;
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Provider-neutral text snippets for market signals — the single home for the
|
|
580
|
+
* approved copy bank, shared verbatim by CLI and MCP. Keeping copy here (not in
|
|
581
|
+
* each client) is a legal control: the language stays factual and never says
|
|
582
|
+
* "bet", "value", "edge", "lock", etc. Display precision is whole-number
|
|
583
|
+
* percent on purpose, to avoid implying false precision.
|
|
584
|
+
*/
|
|
585
|
+
|
|
586
|
+
/** Human label for the data source (text only — never a logo). */
|
|
587
|
+
declare function marketSourceLabel(source: string): string;
|
|
588
|
+
/** One-line favorite read, drawn only from the approved copy bank. */
|
|
589
|
+
declare function marketFavoriteText(signal: MarketSignal, match: Match): string;
|
|
590
|
+
/** "Mexico 56% · Draw 25% · South Africa 19%" in home·draw·away reading order. */
|
|
591
|
+
declare function marketProbabilityText(signal: MarketSignal, match: Match): string;
|
|
592
|
+
/** "Source: Polymarket · updated 14:32 UTC". */
|
|
593
|
+
declare function marketAttributionText(signal: MarketSignal): string;
|
|
594
|
+
/** Compact one-liner for an inline annotation under a match row. */
|
|
595
|
+
declare function marketLine(signal: MarketSignal, match: Match): string;
|
|
596
|
+
/**
|
|
597
|
+
* Multi-line detail block (the caller indents/colorizes). Used by `match <id>`
|
|
598
|
+
* and the dedicated `markets` command. Always carries attribution and the
|
|
599
|
+
* informational-only caveat; prepends a stale warning when applicable.
|
|
600
|
+
*/
|
|
601
|
+
declare function marketBlock(signal: MarketSignal, match: Match): string[];
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Provider factory + graceful-degradation wrappers, mirroring `live.ts`'s
|
|
605
|
+
* getMatchesForDate contract: a market signal is optional enrichment, so any
|
|
606
|
+
* provider/network/parse error degrades to "no signal" and never throws.
|
|
607
|
+
*/
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Resolve the market-data source: explicit arg > CLAUDINHO_MARKETS_SOURCE env >
|
|
611
|
+
* 'polymarket' (mirrors resolveCompetition). Set CLAUDINHO_MARKETS_SOURCE=fake
|
|
612
|
+
* to preview the UX with synthetic, clearly-labeled "demo data" odds.
|
|
613
|
+
*/
|
|
614
|
+
declare function resolveMarketSource(explicit?: string): string;
|
|
615
|
+
/**
|
|
616
|
+
* Construct a market-signal provider. Defaults to the Polymarket public-data
|
|
617
|
+
* adapter; honors CLAUDINHO_MARKETS_SOURCE ('fake' = network-free synthetic
|
|
618
|
+
* demo data; 'none'/'off' = network-free no-op). Tests usually inject directly.
|
|
619
|
+
*/
|
|
620
|
+
declare function makeMarketProvider(source?: string): MarketProvider;
|
|
621
|
+
/** Fetch one match's signal; never throws — undefined on any error. */
|
|
622
|
+
declare function getMarketSignal(provider: MarketProvider, match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
|
|
623
|
+
/** Batch fetch; never throws — empty result (nothing checked) on any error. */
|
|
624
|
+
declare function getMarketSignals(provider: MarketProvider, matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* A network-free MarketProvider for tests and local UX validation. Returns
|
|
628
|
+
* explicitly-provided signals and (optionally) deterministically synthesizes
|
|
629
|
+
* plausible-but-fake odds so every surface can be exercised without any live
|
|
630
|
+
* API. Always labeled `source: 'fake'` so it can never masquerade as real data.
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
interface FakeMarketProviderOptions {
|
|
634
|
+
/** Pre-built signals keyed by matchId; returned verbatim when present. */
|
|
635
|
+
signals?: Record<string, MarketSignal>;
|
|
636
|
+
/** When true, synthesize a deterministic signal for any unmapped match. */
|
|
637
|
+
synthesize?: boolean;
|
|
638
|
+
/** "Now" used when synthesizing timestamps (keeps tests deterministic). */
|
|
639
|
+
now?: Date;
|
|
640
|
+
}
|
|
641
|
+
declare class FakeMarketProvider implements MarketProvider {
|
|
642
|
+
private readonly opts;
|
|
643
|
+
readonly name = "fake";
|
|
644
|
+
constructor(opts?: FakeMarketProviderOptions);
|
|
645
|
+
findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
|
|
646
|
+
findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
|
|
647
|
+
private synthesize;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Polymarket public-data adapter — read-only prediction-market signals.
|
|
652
|
+
*
|
|
653
|
+
* STRICT read-only by design: it touches only the public Gamma events/markets
|
|
654
|
+
* data endpoint. No auth, no wallet, no CLOB/order endpoints, no trading, and no
|
|
655
|
+
* outbound links (sourceMarketId is opaque, never a URL). Any network/parse/host
|
|
656
|
+
* error degrades to "no signal" — it never throws.
|
|
657
|
+
*
|
|
658
|
+
* Payload model (verified against the live Gamma API, see
|
|
659
|
+
* docs/POLYMARKET_MARKET_PREDICTIONS.md): a World Cup match is a Gamma EVENT
|
|
660
|
+
* (`fifwc-{home}-{away}-{date}`) whose payload carries the three moneyline
|
|
661
|
+
* BINARY markets — home win / draw / away win. Each is `outcomes: ["Yes","No"]`
|
|
662
|
+
* and the outcome's probability is its "Yes" price.
|
|
663
|
+
*
|
|
664
|
+
* Mapping is by event slug, which is deterministic — so by default the slug is
|
|
665
|
+
* DERIVED from the fixture (team codes + UTC date) and every match attempts
|
|
666
|
+
* enrichment. A hand-curated entry in mapping.2026.json overrides the derived
|
|
667
|
+
* slug for the rare fixture whose slug doesn't follow the pattern. Because the
|
|
668
|
+
* slug is a guess, validation FAILS CLOSED: the returned event must match the
|
|
669
|
+
* requested slug, line up on kickoff, expose the right moneyline markets, and
|
|
670
|
+
* resolve in regular time — otherwise no signal is produced.
|
|
671
|
+
*/
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Optional override of the derived event slug for a fixture whose Polymarket
|
|
675
|
+
* slug doesn't follow `fifwc-{home}-{away}-{date}` (e.g. an abbreviation that
|
|
676
|
+
* differs from the FIFA code). Most matches need no entry — the slug is derived.
|
|
677
|
+
*/
|
|
678
|
+
interface MarketMapping {
|
|
679
|
+
/** Gamma event slug, e.g. "fifwc-mex-rsa-2026-06-11". */
|
|
680
|
+
eventSlug: string;
|
|
681
|
+
/** Optional Gamma event id (diagnostics; the slug is the lookup key). */
|
|
682
|
+
eventId?: string;
|
|
683
|
+
}
|
|
684
|
+
type MarketMappingTable = Record<string, MarketMapping>;
|
|
685
|
+
interface PolymarketProviderOptions {
|
|
686
|
+
fetchImpl?: typeof fetch;
|
|
687
|
+
timeoutMs?: number;
|
|
688
|
+
/** Base URL; must resolve to an allow-listed host. */
|
|
689
|
+
baseUrl?: string;
|
|
690
|
+
/** Override the bundled mapping table (tests / future gateway). */
|
|
691
|
+
mapping?: MarketMappingTable;
|
|
692
|
+
now?: Date;
|
|
693
|
+
maxAgeMs?: number;
|
|
694
|
+
}
|
|
695
|
+
declare class PolymarketProvider implements MarketProvider {
|
|
696
|
+
private readonly opts;
|
|
697
|
+
readonly name = "polymarket";
|
|
698
|
+
constructor(opts?: PolymarketProviderOptions);
|
|
699
|
+
findSignal(match: Match, options?: MarketSignalOptions): Promise<MarketSignal | undefined>;
|
|
700
|
+
findSignals(matches: Match[], options?: MarketSignalOptions): Promise<MarketSignalsResult>;
|
|
701
|
+
/**
|
|
702
|
+
* Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
|
|
703
|
+
* source and found no usable market, or the fixture is unmappable) from a
|
|
704
|
+
* provider/network error — so transient failures are retried, not
|
|
705
|
+
* negative-cached.
|
|
706
|
+
*/
|
|
707
|
+
private resolveOne;
|
|
708
|
+
private fetchEvent;
|
|
709
|
+
private toSignal;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export { type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchEvent, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type Stage, type StandingRow, type Status, type Team, allFixtures, asFlavorLevel, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, deriveFavorite, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, flagEmoji, formatKickoff, getLiveMatches, getMarketSignal, getMarketSignals, getMatchesForDate, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, localDate, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketLine, marketProbabilityText, marketSourceLabel, matchFlavor, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeOutcomes, outcomeFromScore, resolveCompetition, resolveMarketSource, resolveTz, scoreline };
|
package/dist/index.js
CHANGED
|
@@ -2848,12 +2848,16 @@ function mergeLive(base, live) {
|
|
|
2848
2848
|
for (const m of live) byId.set(m.id, m);
|
|
2849
2849
|
return [...byId.values()];
|
|
2850
2850
|
}
|
|
2851
|
+
function liveSourceLabel(source) {
|
|
2852
|
+
const known = { espn: "ESPN" };
|
|
2853
|
+
return known[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
|
|
2854
|
+
}
|
|
2851
2855
|
async function getMatchesForDate(adapter, dateISO) {
|
|
2852
2856
|
const base = allFixtures();
|
|
2853
2857
|
const day = dateISO.slice(0, 10);
|
|
2854
2858
|
try {
|
|
2855
2859
|
const live = adapter.fetchWindow ? await adapter.fetchWindow(shiftUtcDate(day, -1), shiftUtcDate(day, 1)) : await adapter.fetchByDate(day);
|
|
2856
|
-
return { matches: mergeLive(base, live), degraded: false };
|
|
2860
|
+
return { matches: mergeLive(base, live), degraded: false, source: adapter.name };
|
|
2857
2861
|
} catch {
|
|
2858
2862
|
return { matches: base, degraded: true };
|
|
2859
2863
|
}
|
|
@@ -2864,46 +2868,501 @@ function shiftUtcDate(dateISO, days) {
|
|
|
2864
2868
|
}
|
|
2865
2869
|
async function getLiveMatches(adapter) {
|
|
2866
2870
|
try {
|
|
2867
|
-
return { matches: await adapter.fetchLive(), degraded: false };
|
|
2871
|
+
return { matches: await adapter.fetchLive(), degraded: false, source: adapter.name };
|
|
2868
2872
|
} catch {
|
|
2869
2873
|
return { matches: [], degraded: true };
|
|
2870
2874
|
}
|
|
2871
2875
|
}
|
|
2876
|
+
|
|
2877
|
+
// src/markets/normalize.ts
|
|
2878
|
+
var DEFAULT_MAX_AGE_MS = 15 * 6e4;
|
|
2879
|
+
function normalizeOutcomes(outcomes) {
|
|
2880
|
+
const sum = outcomes.reduce(
|
|
2881
|
+
(s, o) => s + (Number.isFinite(o.probability) && o.probability > 0 ? o.probability : 0),
|
|
2882
|
+
0
|
|
2883
|
+
);
|
|
2884
|
+
if (sum <= 0) return outcomes.map((o) => ({ ...o, probability: 0 }));
|
|
2885
|
+
return outcomes.map((o) => ({
|
|
2886
|
+
...o,
|
|
2887
|
+
probability: Number.isFinite(o.probability) && o.probability > 0 ? o.probability / sum : 0
|
|
2888
|
+
}));
|
|
2889
|
+
}
|
|
2890
|
+
function favoriteStrength(probability) {
|
|
2891
|
+
if (probability >= 0.65) return "clear";
|
|
2892
|
+
if (probability >= 0.52) return "slight";
|
|
2893
|
+
return "close";
|
|
2894
|
+
}
|
|
2895
|
+
function deriveFavorite(outcomes) {
|
|
2896
|
+
let top;
|
|
2897
|
+
for (const o of outcomes) {
|
|
2898
|
+
if (o.kind === "other") continue;
|
|
2899
|
+
if (!top || o.probability > top.probability) top = o;
|
|
2900
|
+
}
|
|
2901
|
+
if (!top || top.probability <= 0 || top.kind === "other") return void 0;
|
|
2902
|
+
return {
|
|
2903
|
+
kind: top.kind,
|
|
2904
|
+
teamCode: top.teamCode,
|
|
2905
|
+
probability: top.probability,
|
|
2906
|
+
strength: favoriteStrength(top.probability)
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
function mapsCleanly(match, outcomes) {
|
|
2910
|
+
if (outcomes.some((o) => o.kind === "other")) return false;
|
|
2911
|
+
const home = outcomes.find((o) => o.kind === "home");
|
|
2912
|
+
const away = outcomes.find((o) => o.kind === "away");
|
|
2913
|
+
const draw = outcomes.find((o) => o.kind === "draw");
|
|
2914
|
+
if (!home || !away) return false;
|
|
2915
|
+
if (home.teamCode && home.teamCode.toUpperCase() !== match.home.code.toUpperCase()) {
|
|
2916
|
+
return false;
|
|
2917
|
+
}
|
|
2918
|
+
if (away.teamCode && away.teamCode.toUpperCase() !== match.away.code.toUpperCase()) {
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
if (match.stage === "GROUP" && !draw) return false;
|
|
2922
|
+
return true;
|
|
2923
|
+
}
|
|
2924
|
+
function hasSaneDistribution(outcomes) {
|
|
2925
|
+
const priced = outcomes.filter((o) => Number.isFinite(o.probability) && o.probability > 0);
|
|
2926
|
+
if (priced.length < 2) return false;
|
|
2927
|
+
const sum = priced.reduce((s, o) => s + o.probability, 0);
|
|
2928
|
+
return sum > 0.97 && sum < 1.03;
|
|
2929
|
+
}
|
|
2930
|
+
function isStaleSignal(signal, options = {}) {
|
|
2931
|
+
const maxAge = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2932
|
+
const asOf = Date.parse(signal.asOf);
|
|
2933
|
+
if (!Number.isFinite(asOf)) return true;
|
|
2934
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
2935
|
+
return now - asOf > maxAge;
|
|
2936
|
+
}
|
|
2937
|
+
function isReliableMarketSignal(signal, options = {}) {
|
|
2938
|
+
if (options.includeUnreliable) return true;
|
|
2939
|
+
if (signal.ambiguous) return false;
|
|
2940
|
+
if (!signal.favorite) return false;
|
|
2941
|
+
if (!hasSaneDistribution(signal.outcomes)) return false;
|
|
2942
|
+
if (signal.stale || isStaleSignal(signal, options)) return false;
|
|
2943
|
+
if (options.minLiquidity != null) {
|
|
2944
|
+
if (signal.liquidity == null || signal.liquidity < options.minLiquidity) return false;
|
|
2945
|
+
}
|
|
2946
|
+
return true;
|
|
2947
|
+
}
|
|
2948
|
+
function buildMarketSignal(input) {
|
|
2949
|
+
const outcomes = normalizeOutcomes(input.outcomes);
|
|
2950
|
+
const ambiguous = input.ambiguous === true || !mapsCleanly(input.match, outcomes);
|
|
2951
|
+
const favorite = ambiguous ? void 0 : deriveFavorite(outcomes);
|
|
2952
|
+
const signal = {
|
|
2953
|
+
matchId: input.match.id,
|
|
2954
|
+
source: input.source,
|
|
2955
|
+
sourceMarketId: input.sourceMarketId,
|
|
2956
|
+
asOf: input.asOf,
|
|
2957
|
+
fetchedAt: input.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2958
|
+
outcomes,
|
|
2959
|
+
favorite,
|
|
2960
|
+
liquidity: input.liquidity,
|
|
2961
|
+
volume24h: input.volume24h,
|
|
2962
|
+
stale: false,
|
|
2963
|
+
ambiguous
|
|
2964
|
+
};
|
|
2965
|
+
signal.stale = isStaleSignal(signal, { now: input.now, maxAgeMs: input.maxAgeMs });
|
|
2966
|
+
return signal;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/markets/format.ts
|
|
2970
|
+
function pct(p) {
|
|
2971
|
+
return Math.round(p * 100);
|
|
2972
|
+
}
|
|
2973
|
+
function marketSourceLabel(source) {
|
|
2974
|
+
if (source === "polymarket") return "Polymarket";
|
|
2975
|
+
if (source === "fake") return "demo data";
|
|
2976
|
+
return source.charAt(0).toUpperCase() + source.slice(1);
|
|
2977
|
+
}
|
|
2978
|
+
function outcomeLabel(o, match) {
|
|
2979
|
+
if (o.kind === "home") return match.home.name;
|
|
2980
|
+
if (o.kind === "away") return match.away.name;
|
|
2981
|
+
if (o.kind === "draw") return "Draw";
|
|
2982
|
+
return o.label;
|
|
2983
|
+
}
|
|
2984
|
+
function utcHhmm(iso) {
|
|
2985
|
+
const t = Date.parse(iso);
|
|
2986
|
+
if (!Number.isFinite(t)) return "";
|
|
2987
|
+
return `${new Date(t).toISOString().slice(11, 16)} UTC`;
|
|
2988
|
+
}
|
|
2989
|
+
function marketFavoriteText(signal, match) {
|
|
2990
|
+
const fav = signal.favorite;
|
|
2991
|
+
if (!fav || fav.strength === "close") return "Prediction markets see this match as close.";
|
|
2992
|
+
if (fav.kind === "draw") return "Prediction markets see a draw as the top outcome.";
|
|
2993
|
+
const name = fav.kind === "home" ? match.home.name : match.away.name;
|
|
2994
|
+
return fav.strength === "clear" ? `Prediction markets favor ${name}.` : `Prediction markets slightly favor ${name}.`;
|
|
2995
|
+
}
|
|
2996
|
+
function marketProbabilityText(signal, match) {
|
|
2997
|
+
const order = ["home", "draw", "away"];
|
|
2998
|
+
const parts = [];
|
|
2999
|
+
for (const kind of order) {
|
|
3000
|
+
const o = signal.outcomes.find((x) => x.kind === kind);
|
|
3001
|
+
if (o) parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
3002
|
+
}
|
|
3003
|
+
for (const o of signal.outcomes) {
|
|
3004
|
+
if (o.kind === "other") parts.push(`${outcomeLabel(o, match)} ${pct(o.probability)}%`);
|
|
3005
|
+
}
|
|
3006
|
+
return parts.join(" \xB7 ");
|
|
3007
|
+
}
|
|
3008
|
+
function marketAttributionText(signal) {
|
|
3009
|
+
const time = utcHhmm(signal.asOf);
|
|
3010
|
+
const src = `Source: ${marketSourceLabel(signal.source)}`;
|
|
3011
|
+
return time ? `${src} \xB7 updated ${time}` : src;
|
|
3012
|
+
}
|
|
3013
|
+
function marketLine(signal, match) {
|
|
3014
|
+
return `Market: ${marketProbabilityText(signal, match)} \xB7 ${marketSourceLabel(
|
|
3015
|
+
signal.source
|
|
3016
|
+
)} \xB7 informational only`;
|
|
3017
|
+
}
|
|
3018
|
+
function marketBlock(signal, match) {
|
|
3019
|
+
const lines = [];
|
|
3020
|
+
if (signal.stale) lines.push("Market signal is stale; the reading may be out of date.");
|
|
3021
|
+
lines.push(marketFavoriteText(signal, match));
|
|
3022
|
+
lines.push(marketProbabilityText(signal, match));
|
|
3023
|
+
lines.push(`${marketAttributionText(signal)} \xB7 informational only`);
|
|
3024
|
+
return lines;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/markets/fake.ts
|
|
3028
|
+
var FakeMarketProvider = class {
|
|
3029
|
+
constructor(opts = {}) {
|
|
3030
|
+
this.opts = opts;
|
|
3031
|
+
}
|
|
3032
|
+
opts;
|
|
3033
|
+
name = "fake";
|
|
3034
|
+
async findSignal(match, options) {
|
|
3035
|
+
const preset = this.opts.signals?.[match.id];
|
|
3036
|
+
if (preset) return preset;
|
|
3037
|
+
if (this.opts.synthesize) return this.synthesize(match, options);
|
|
3038
|
+
return void 0;
|
|
3039
|
+
}
|
|
3040
|
+
async findSignals(matches, options) {
|
|
3041
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3042
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3043
|
+
for (const m of matches) {
|
|
3044
|
+
checked.add(m.id);
|
|
3045
|
+
const s = await this.findSignal(m, options);
|
|
3046
|
+
if (s) signals.set(m.id, s);
|
|
3047
|
+
}
|
|
3048
|
+
return { signals, checked };
|
|
3049
|
+
}
|
|
3050
|
+
synthesize(match, options) {
|
|
3051
|
+
const seed = hash(`${match.home.code}-${match.away.code}`);
|
|
3052
|
+
const home = 0.3 + seed % 33 / 100;
|
|
3053
|
+
const away = 0.18 + (seed >> 3) % 23 / 100;
|
|
3054
|
+
const draw = Math.max(0.05, 1 - home - away);
|
|
3055
|
+
const outcomes = [
|
|
3056
|
+
{ kind: "home", teamCode: match.home.code, label: match.home.name, probability: home },
|
|
3057
|
+
{ kind: "draw", label: "Draw", probability: draw },
|
|
3058
|
+
{ kind: "away", teamCode: match.away.code, label: match.away.name, probability: away }
|
|
3059
|
+
];
|
|
3060
|
+
const now = this.opts.now ?? options?.now ?? /* @__PURE__ */ new Date();
|
|
3061
|
+
const asOf = new Date(now.getTime() - 6e4).toISOString();
|
|
3062
|
+
return buildMarketSignal({
|
|
3063
|
+
match,
|
|
3064
|
+
source: "fake",
|
|
3065
|
+
sourceMarketId: `fake-${match.id}`,
|
|
3066
|
+
asOf,
|
|
3067
|
+
fetchedAt: now.toISOString(),
|
|
3068
|
+
outcomes,
|
|
3069
|
+
liquidity: 5e4,
|
|
3070
|
+
now,
|
|
3071
|
+
maxAgeMs: options?.maxAgeMs
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
function hash(s) {
|
|
3076
|
+
let h = 0;
|
|
3077
|
+
for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) >>> 0;
|
|
3078
|
+
return h % 1e5;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// src/markets/mapping.2026.json
|
|
3082
|
+
var mapping_2026_default = {
|
|
3083
|
+
version: 1,
|
|
3084
|
+
note: "Optional matchId -> Polymarket EVENT-slug OVERRIDES. By default the slug is DERIVED from each fixture (fifwc-{home}-{away}-{UTC-date}, e.g. fifwc-mex-rsa-2026-06-11), so most matches need NO entry here. Add an entry only for a fixture whose real Polymarket slug differs (e.g. a team abbreviation that isn't the FIFA code, or a tz-shifted date). The event payload carries the three moneyline binary markets; each outcome's probability is its 'Yes' price; validation fails closed. Entry: { eventSlug, eventId? }.",
|
|
3085
|
+
markets: {}
|
|
3086
|
+
};
|
|
3087
|
+
|
|
3088
|
+
// src/markets/polymarket.ts
|
|
3089
|
+
var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
3090
|
+
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
3091
|
+
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
3092
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
3093
|
+
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
3094
|
+
var WC_SPORT = "fifwc";
|
|
3095
|
+
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
3096
|
+
var NON_REGULAR_TIME = /extra time|penalt|to advance|to qualif|win the (group|tournament|cup|title)/i;
|
|
3097
|
+
var BUNDLED_MAPPING = mapping_2026_default.markets;
|
|
3098
|
+
var PolymarketProvider = class {
|
|
3099
|
+
constructor(opts = {}) {
|
|
3100
|
+
this.opts = opts;
|
|
3101
|
+
}
|
|
3102
|
+
opts;
|
|
3103
|
+
name = "polymarket";
|
|
3104
|
+
async findSignal(match, options) {
|
|
3105
|
+
return (await this.resolveOne(match, options)).signal;
|
|
3106
|
+
}
|
|
3107
|
+
async findSignals(matches, options) {
|
|
3108
|
+
const signals = /* @__PURE__ */ new Map();
|
|
3109
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3110
|
+
const deadline = options?.deadlineMs != null ? Date.now() + options.deadlineMs : Number.POSITIVE_INFINITY;
|
|
3111
|
+
for (const m of matches) {
|
|
3112
|
+
if (Date.now() >= deadline) break;
|
|
3113
|
+
const r = await this.resolveOne(m, options);
|
|
3114
|
+
if (r.checked) checked.add(m.id);
|
|
3115
|
+
if (r.signal) signals.set(m.id, r.signal);
|
|
3116
|
+
}
|
|
3117
|
+
return { signals, checked };
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Resolve one match. `checked` distinguishes a DEFINITIVE result (reached the
|
|
3121
|
+
* source and found no usable market, or the fixture is unmappable) from a
|
|
3122
|
+
* provider/network error — so transient failures are retried, not
|
|
3123
|
+
* negative-cached.
|
|
3124
|
+
*/
|
|
3125
|
+
async resolveOne(match, options) {
|
|
3126
|
+
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
3127
|
+
const eventSlug = entry?.eventSlug ?? deriveEventSlug(match);
|
|
3128
|
+
if (!eventSlug) return { checked: true };
|
|
3129
|
+
try {
|
|
3130
|
+
const event = await this.fetchEvent(eventSlug, options?.timeoutMs);
|
|
3131
|
+
const signal = event ? this.toSignal(match, eventSlug, event, options) : void 0;
|
|
3132
|
+
return { signal, checked: true };
|
|
3133
|
+
} catch {
|
|
3134
|
+
return { checked: false };
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
async fetchEvent(slug, timeoutMs) {
|
|
3138
|
+
const base = this.opts.baseUrl ?? DEFAULT_BASE2;
|
|
3139
|
+
assertAllowedHost(base);
|
|
3140
|
+
const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
|
|
3141
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
3142
|
+
const res = await doFetch(url, {
|
|
3143
|
+
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
3144
|
+
headers: { Accept: "application/json", "User-Agent": USER_AGENT2 }
|
|
3145
|
+
});
|
|
3146
|
+
if (res.status === 404) return void 0;
|
|
3147
|
+
if (!res.ok) {
|
|
3148
|
+
throw new Error(`Polymarket request failed: ${res.status} ${res.statusText}`);
|
|
3149
|
+
}
|
|
3150
|
+
const data = await res.json();
|
|
3151
|
+
const event = Array.isArray(data) ? data[0] : data;
|
|
3152
|
+
return event && typeof event === "object" ? event : void 0;
|
|
3153
|
+
}
|
|
3154
|
+
toSignal(match, eventSlug, event, options) {
|
|
3155
|
+
if (event.active === false || event.closed === true) return void 0;
|
|
3156
|
+
if (event.seriesSlug != null && event.seriesSlug !== WC_SERIES_SLUG && event.sport?.sport !== WC_SPORT) {
|
|
3157
|
+
return void 0;
|
|
3158
|
+
}
|
|
3159
|
+
if (event.slug != null && event.slug !== eventSlug) return void 0;
|
|
3160
|
+
const start = event.startTime ? Date.parse(event.startTime) : Number.NaN;
|
|
3161
|
+
const kick = Date.parse(match.kickoff);
|
|
3162
|
+
if (Number.isFinite(start) && Number.isFinite(kick) && Math.abs(start - kick) > KICKOFF_TOLERANCE_MS) {
|
|
3163
|
+
return void 0;
|
|
3164
|
+
}
|
|
3165
|
+
const moneyline = (event.markets ?? []).filter(
|
|
3166
|
+
(m) => (m.sportsMarketType ?? "moneyline") === "moneyline"
|
|
3167
|
+
);
|
|
3168
|
+
const homeMarket = pickMarket(moneyline, match.home.code, match.home.name);
|
|
3169
|
+
const awayMarket = pickMarket(moneyline, match.away.code, match.away.name);
|
|
3170
|
+
const drawMarket = pickDraw(moneyline);
|
|
3171
|
+
if (!homeMarket || !awayMarket) return void 0;
|
|
3172
|
+
const legIds = [homeMarket, awayMarket, drawMarket].filter((m) => m != null).map((m) => m.id ?? m.slug ?? "");
|
|
3173
|
+
if (new Set(legIds).size !== legIds.length) return void 0;
|
|
3174
|
+
const legs = [
|
|
3175
|
+
["home", homeMarket, match.home.code, match.home.name],
|
|
3176
|
+
["draw", drawMarket, void 0, "Draw"],
|
|
3177
|
+
["away", awayMarket, match.away.code, match.away.name]
|
|
3178
|
+
];
|
|
3179
|
+
const outcomes = [];
|
|
3180
|
+
let asOf = event.updatedAt;
|
|
3181
|
+
let liquidity;
|
|
3182
|
+
for (const [kind, market, teamCode, label] of legs) {
|
|
3183
|
+
if (!market) continue;
|
|
3184
|
+
if (market.closed === true || market.active === false) return void 0;
|
|
3185
|
+
if (market.description && NON_REGULAR_TIME.test(market.description)) return void 0;
|
|
3186
|
+
const yes = yesPrice(market);
|
|
3187
|
+
if (yes == null) return void 0;
|
|
3188
|
+
outcomes.push({ kind, teamCode, label, probability: yes });
|
|
3189
|
+
if (market.updatedAt && (!asOf || market.updatedAt < asOf)) asOf = market.updatedAt;
|
|
3190
|
+
const liq = numberish(market.liquidityNum ?? market.liquidity);
|
|
3191
|
+
if (liq != null) liquidity = liquidity == null ? liq : Math.min(liquidity, liq);
|
|
3192
|
+
}
|
|
3193
|
+
const rawSum = outcomes.reduce((s, o) => s + o.probability, 0);
|
|
3194
|
+
if (rawSum < 0.9 || rawSum > 1.15) return void 0;
|
|
3195
|
+
const signal = buildMarketSignal({
|
|
3196
|
+
match,
|
|
3197
|
+
source: "polymarket",
|
|
3198
|
+
sourceMarketId: event.id ?? eventSlug,
|
|
3199
|
+
asOf: asOf ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3200
|
+
outcomes,
|
|
3201
|
+
liquidity,
|
|
3202
|
+
now: options?.now ?? this.opts.now,
|
|
3203
|
+
maxAgeMs: options?.maxAgeMs ?? this.opts.maxAgeMs
|
|
3204
|
+
});
|
|
3205
|
+
return signal.ambiguous ? void 0 : signal;
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
function deriveEventSlug(match) {
|
|
3209
|
+
const home = match.home.code.toLowerCase();
|
|
3210
|
+
const away = match.away.code.toLowerCase();
|
|
3211
|
+
if (home === away || home === "tbd" || away === "tbd") return void 0;
|
|
3212
|
+
if (!/^[a-z]{3}$/.test(home) || !/^[a-z]{3}$/.test(away)) return void 0;
|
|
3213
|
+
const date = match.kickoff.slice(0, 10);
|
|
3214
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return void 0;
|
|
3215
|
+
return `fifwc-${home}-${away}-${date}`;
|
|
3216
|
+
}
|
|
3217
|
+
function slugToken(m) {
|
|
3218
|
+
return (m.slug ?? "").toLowerCase().split("-").pop() ?? "";
|
|
3219
|
+
}
|
|
3220
|
+
function isDrawMarket(m) {
|
|
3221
|
+
return slugToken(m) === "draw" || (m.groupItemTitle ?? "").trim().toLowerCase().startsWith("draw");
|
|
3222
|
+
}
|
|
3223
|
+
function pickMarket(markets, teamCode, teamName) {
|
|
3224
|
+
const code = teamCode.toLowerCase();
|
|
3225
|
+
const name = teamName.trim().toLowerCase();
|
|
3226
|
+
const teamMarkets = markets.filter((m) => !isDrawMarket(m));
|
|
3227
|
+
const bySlug = teamMarkets.find((m) => slugToken(m) === code);
|
|
3228
|
+
if (bySlug) return bySlug;
|
|
3229
|
+
return teamMarkets.find((m) => (m.groupItemTitle ?? "").trim().toLowerCase() === name);
|
|
3230
|
+
}
|
|
3231
|
+
function pickDraw(markets) {
|
|
3232
|
+
return markets.find(isDrawMarket);
|
|
3233
|
+
}
|
|
3234
|
+
function assertAllowedHost(base) {
|
|
3235
|
+
let host;
|
|
3236
|
+
try {
|
|
3237
|
+
host = new URL(base).host;
|
|
3238
|
+
} catch {
|
|
3239
|
+
throw new Error(`Invalid Polymarket base URL: ${base}`);
|
|
3240
|
+
}
|
|
3241
|
+
if (!ALLOWED_HOSTS.has(host)) {
|
|
3242
|
+
throw new Error(`Polymarket host not allow-listed: ${host}`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function yesPrice(market) {
|
|
3246
|
+
const labels = parseJsonArray(market.outcomes);
|
|
3247
|
+
const prices = parseJsonArray(market.outcomePrices).map((p2) => Number(p2));
|
|
3248
|
+
if (labels.length === 0 || labels.length !== prices.length) return void 0;
|
|
3249
|
+
const i = labels.findIndex((l) => l.trim().toLowerCase() === "yes");
|
|
3250
|
+
if (i < 0) return void 0;
|
|
3251
|
+
const p = prices[i];
|
|
3252
|
+
return typeof p === "number" && Number.isFinite(p) && p > 0 && p <= 1 ? p : void 0;
|
|
3253
|
+
}
|
|
3254
|
+
function parseJsonArray(v) {
|
|
3255
|
+
if (Array.isArray(v)) return v.map((x) => String(x));
|
|
3256
|
+
if (typeof v === "string") {
|
|
3257
|
+
try {
|
|
3258
|
+
const parsed = JSON.parse(v);
|
|
3259
|
+
return Array.isArray(parsed) ? parsed.map((x) => String(x)) : [];
|
|
3260
|
+
} catch {
|
|
3261
|
+
return [];
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
return [];
|
|
3265
|
+
}
|
|
3266
|
+
function numberish(v) {
|
|
3267
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : void 0;
|
|
3268
|
+
if (typeof v === "string") {
|
|
3269
|
+
const n = Number(v);
|
|
3270
|
+
return Number.isFinite(n) ? n : void 0;
|
|
3271
|
+
}
|
|
3272
|
+
return void 0;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// src/markets/provider.ts
|
|
3276
|
+
function resolveMarketSource(explicit) {
|
|
3277
|
+
if (explicit) return explicit;
|
|
3278
|
+
if (typeof process !== "undefined" && process.env?.CLAUDINHO_MARKETS_SOURCE) {
|
|
3279
|
+
return process.env.CLAUDINHO_MARKETS_SOURCE;
|
|
3280
|
+
}
|
|
3281
|
+
return "polymarket";
|
|
3282
|
+
}
|
|
3283
|
+
function makeMarketProvider(source) {
|
|
3284
|
+
switch (resolveMarketSource(source)) {
|
|
3285
|
+
case "fake":
|
|
3286
|
+
return new FakeMarketProvider({ synthesize: true });
|
|
3287
|
+
case "none":
|
|
3288
|
+
case "off":
|
|
3289
|
+
return new FakeMarketProvider();
|
|
3290
|
+
// no synth → yields no signals, no network
|
|
3291
|
+
default:
|
|
3292
|
+
return new PolymarketProvider();
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
async function getMarketSignal(provider, match, options) {
|
|
3296
|
+
try {
|
|
3297
|
+
return await provider.findSignal(match, options);
|
|
3298
|
+
} catch {
|
|
3299
|
+
return void 0;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
async function getMarketSignals(provider, matches, options) {
|
|
3303
|
+
try {
|
|
3304
|
+
return await provider.findSignals(matches, options);
|
|
3305
|
+
} catch {
|
|
3306
|
+
return { signals: /* @__PURE__ */ new Map(), checked: /* @__PURE__ */ new Set() };
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
2872
3309
|
export {
|
|
2873
3310
|
DEFAULT_COMPETITION,
|
|
2874
3311
|
DEFAULT_FLAVOR,
|
|
3312
|
+
DEFAULT_MAX_AGE_MS,
|
|
2875
3313
|
EspnAdapter,
|
|
2876
3314
|
FLAVOR_LEVELS,
|
|
3315
|
+
FakeMarketProvider,
|
|
3316
|
+
PolymarketProvider,
|
|
2877
3317
|
allFixtures,
|
|
2878
3318
|
asFlavorLevel,
|
|
3319
|
+
buildMarketSignal,
|
|
2879
3320
|
byKickoff,
|
|
2880
3321
|
competitionBase,
|
|
2881
3322
|
computeStandings,
|
|
2882
3323
|
countdown,
|
|
3324
|
+
deriveFavorite,
|
|
3325
|
+
favoriteStrength,
|
|
2883
3326
|
fixturesByDate,
|
|
2884
3327
|
fixturesByGroup,
|
|
2885
3328
|
fixturesByTeam,
|
|
2886
3329
|
flagEmoji,
|
|
2887
3330
|
formatKickoff,
|
|
2888
3331
|
getLiveMatches,
|
|
3332
|
+
getMarketSignal,
|
|
3333
|
+
getMarketSignals,
|
|
2889
3334
|
getMatchesForDate,
|
|
2890
3335
|
groups,
|
|
3336
|
+
hasSaneDistribution,
|
|
2891
3337
|
isFinished,
|
|
2892
3338
|
isFlavorLevel,
|
|
2893
3339
|
isLive,
|
|
3340
|
+
isReliableMarketSignal,
|
|
3341
|
+
isStaleSignal,
|
|
2894
3342
|
isValidDate,
|
|
2895
3343
|
isValidTimeZone,
|
|
3344
|
+
liveSourceLabel,
|
|
2896
3345
|
localDate,
|
|
2897
3346
|
makeAdapter,
|
|
3347
|
+
makeMarketProvider,
|
|
2898
3348
|
mapEspnEvent,
|
|
3349
|
+
mapsCleanly,
|
|
3350
|
+
marketAttributionText,
|
|
3351
|
+
marketBlock,
|
|
3352
|
+
marketFavoriteText,
|
|
3353
|
+
marketLine,
|
|
3354
|
+
marketProbabilityText,
|
|
3355
|
+
marketSourceLabel,
|
|
2899
3356
|
matchFlavor,
|
|
2900
3357
|
matchLocation,
|
|
2901
3358
|
mergeLive,
|
|
2902
3359
|
nationToFlag,
|
|
2903
3360
|
nationToRegion,
|
|
2904
3361
|
nextFixtureForTeam,
|
|
3362
|
+
normalizeOutcomes,
|
|
2905
3363
|
outcomeFromScore,
|
|
2906
3364
|
resolveCompetition,
|
|
3365
|
+
resolveMarketSource,
|
|
2907
3366
|
resolveTz,
|
|
2908
3367
|
scoreline
|
|
2909
3368
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Domain model,
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Domain model, provider adapters, standings, and a read-only market-signal sidecar for Claudinho — the 2026 men's football tournament. Not affiliated with FIFA or Anthropic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Arturo Garrido",
|