@oracle-agent/oracle 0.14.1 → 0.16.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 +2 -2
- package/SETUP.md +4 -4
- package/dist/action-receipts.mjs +1 -0
- package/dist/action-semantics.mjs +1 -0
- package/dist/address-book.mjs +2 -0
- package/dist/bin/desk-server.mjs +60 -0
- package/dist/bin/oracle-data-mcp.mjs +9 -0
- package/dist/bin/oracle-equities.mjs +20 -0
- package/dist/bin/oracle-init.mjs +24 -0
- package/dist/bin/oracle-public-server.mjs +5 -0
- package/dist/bin/oracle-route.mjs +22 -0
- package/dist/bin/oracle-scan.mjs +18 -0
- package/dist/bin/oracle-upgrade.mjs +21 -0
- package/dist/bin/oracle.mjs +63 -0
- package/dist/chains.mjs +1 -0
- package/dist/cli/commands/auth.mjs +16 -0
- package/dist/cli/commands/bootstrap.mjs +18 -0
- package/dist/cli/commands/chain.mjs +21 -0
- package/dist/cli/commands/chat.mjs +45 -0
- package/dist/cli/commands/credential.mjs +1 -0
- package/dist/cli/commands/data-mcp.mjs +2 -0
- package/dist/cli/commands/data.mjs +12 -0
- package/dist/cli/commands/doctor.mjs +5 -0
- package/dist/cli/commands/equities.mjs +15 -0
- package/dist/cli/commands/farm.mjs +59 -0
- package/dist/cli/commands/fees.mjs +10 -0
- package/dist/cli/commands/follow.mjs +28 -0
- package/dist/cli/commands/harness.mjs +15 -0
- package/dist/cli/commands/help.mjs +1 -0
- package/dist/cli/commands/init.mjs +3 -0
- package/dist/cli/commands/mcp.mjs +36 -0
- package/dist/cli/commands/model.mjs +54 -0
- package/dist/cli/commands/plugins.mjs +43 -0
- package/dist/cli/commands/prepare.mjs +2 -0
- package/dist/cli/commands/public.mjs +3 -0
- package/dist/cli/commands/resolve.mjs +23 -0
- package/dist/cli/commands/route.mjs +2 -0
- package/dist/cli/commands/runner.mjs +1 -0
- package/dist/cli/commands/scan.mjs +2 -0
- package/dist/cli/commands/setup.mjs +34 -0
- package/dist/cli/commands/sign.mjs +4 -0
- package/dist/cli/commands/signer.mjs +1 -0
- package/dist/cli/commands/strategy.mjs +18 -0
- package/dist/cli/commands/swap.mjs +36 -0
- package/dist/cli/commands/upgrade.mjs +2 -0
- package/dist/cli/commands/vault.mjs +1 -0
- package/dist/cli/commands/version.mjs +2 -0
- package/dist/data/desk-data.mjs +55 -0
- package/dist/data/names.mjs +2 -0
- package/dist/data/providers/farming.mjs +2 -0
- package/dist/equities/index.mjs +1 -0
- package/dist/index.mjs +60 -0
- package/dist/nft-gas-war-guard.mjs +1 -0
- package/dist/onboarding/harness-configs.mjs +4 -0
- package/dist/portfolio-risk.mjs +1 -0
- package/dist/prepare-envelope.mjs +1 -0
- package/dist/public-api/agent-grants.mjs +3 -0
- package/dist/public-control/policy-schema.mjs +1 -0
- package/dist/router/index.mjs +2 -0
- package/dist/scanner/index.mjs +4 -0
- package/dist/signals/index.mjs +1 -0
- package/dist/strategy/index.mjs +3 -0
- package/dist/watch-preferences.mjs +1 -0
- package/package.json +33 -78
- package/public/oracle-splash/_variants/ice.html +5 -18
- package/public/oracle-splash/_variants/ivory.html +5 -18
- package/public/oracle-splash/downloads/index.html +4 -4
- package/public/oracle-splash/index.html +113 -34
- package/CONTRIBUTING.md +0 -99
- package/bin/desk-server.mjs +0 -473
- package/bin/oracle-data-mcp.mjs +0 -1252
- package/bin/oracle-equities.mjs +0 -168
- package/bin/oracle-init.mjs +0 -525
- package/bin/oracle-public-server.mjs +0 -36
- package/bin/oracle-route.mjs +0 -254
- package/bin/oracle-scan.mjs +0 -192
- package/bin/oracle-upgrade.mjs +0 -42
- package/bin/oracle.mjs +0 -34
- package/docs/adding-a-chain.md +0 -229
- package/docs/architecture.md +0 -139
- package/docs/buzz-integration.md +0 -115
- package/docs/cli.md +0 -143
- package/docs/connectors.md +0 -13
- package/docs/hyperliquid-builder-code.md +0 -50
- package/docs/locals-only-fee-waiver.md +0 -34
- package/docs/oracle-pack-standard.md +0 -29
- package/docs/profiles.md +0 -151
- package/docs/public-surface.md +0 -206
- package/examples/add-a-chain.mjs +0 -65
- package/examples/oracle-pack-template.mjs +0 -38
- package/examples/research-a-token.mjs +0 -70
- package/plugins/oracle-owner-gate/__init__.py +0 -288
- package/plugins/oracle-owner-gate/plugin.yaml +0 -9
- package/protocols/templates/safe-erc20/SECURITY.md +0 -16
- package/protocols/templates/safe-erc20/foundry.toml +0 -12
- package/protocols/templates/safe-erc20/remappings.txt +0 -2
- package/protocols/templates/safe-erc20/src/SafeERC20.sol +0 -53
- package/protocols/templates/safe-erc20/test/SafeERC20.t.sol +0 -72
- package/scripts/adversarial-bench.mjs +0 -114
- package/scripts/build-dist.mjs +0 -119
- package/scripts/build-inscription.py +0 -230
- package/scripts/check-doc-drift.mjs +0 -123
- package/scripts/check-test-count.mjs +0 -105
- package/scripts/e2e-hl-markets.mjs +0 -21
- package/scripts/e2e-hl-perps.mjs +0 -48
- package/scripts/e2e-hypercore-staking.mjs +0 -128
- package/scripts/e2e-solana-bitcoin.mjs +0 -183
- package/scripts/guard-publish.mjs +0 -22
- package/scripts/protocol-template-gate.mjs +0 -15
- package/scripts/public-api-scan.mjs +0 -23
- package/scripts/publish-dist.mjs +0 -67
- package/scripts/record-hero-cli.sh +0 -80
- package/scripts/secret-scan.mjs +0 -169
- package/scripts/smoke-dist-bins.mjs +0 -89
- package/scripts/verify-dist.mjs +0 -29
- package/scripts/verify-v3-venues.mjs +0 -233
- package/skins/oracle.yaml +0 -56
- package/src/action-receipts.mjs +0 -265
- package/src/action-semantics.mjs +0 -62
- package/src/address-book.mjs +0 -208
- package/src/agent-auth.mjs +0 -206
- package/src/approval-guard.mjs +0 -282
- package/src/attestation-secret.mjs +0 -94
- package/src/audit-log.mjs +0 -196
- package/src/auth/oauth.mjs +0 -672
- package/src/auto-slippage.mjs +0 -378
- package/src/capability-posture.mjs +0 -125
- package/src/cards.mjs +0 -422
- package/src/chains.mjs +0 -62
- package/src/cli/chain-catalog.mjs +0 -215
- package/src/cli/chain-state.mjs +0 -74
- package/src/cli/commands/auth.mjs +0 -193
- package/src/cli/commands/bootstrap.mjs +0 -87
- package/src/cli/commands/chain.mjs +0 -143
- package/src/cli/commands/chat.mjs +0 -465
- package/src/cli/commands/credential.mjs +0 -9
- package/src/cli/commands/data-mcp.mjs +0 -18
- package/src/cli/commands/data.mjs +0 -75
- package/src/cli/commands/doctor.mjs +0 -136
- package/src/cli/commands/equities.mjs +0 -41
- package/src/cli/commands/farm.mjs +0 -129
- package/src/cli/commands/fees.mjs +0 -109
- package/src/cli/commands/follow.mjs +0 -130
- package/src/cli/commands/harness.mjs +0 -63
- package/src/cli/commands/help.mjs +0 -7
- package/src/cli/commands/init.mjs +0 -27
- package/src/cli/commands/mcp.mjs +0 -154
- package/src/cli/commands/model.mjs +0 -106
- package/src/cli/commands/plugins.mjs +0 -275
- package/src/cli/commands/prepare.mjs +0 -12
- package/src/cli/commands/public.mjs +0 -20
- package/src/cli/commands/resolve.mjs +0 -64
- package/src/cli/commands/route.mjs +0 -12
- package/src/cli/commands/runner.mjs +0 -9
- package/src/cli/commands/scan.mjs +0 -12
- package/src/cli/commands/setup.mjs +0 -294
- package/src/cli/commands/sign.mjs +0 -21
- package/src/cli/commands/signer.mjs +0 -9
- package/src/cli/commands/swap.mjs +0 -102
- package/src/cli/commands/upgrade.mjs +0 -12
- package/src/cli/commands/vault.mjs +0 -9
- package/src/cli/commands/version.mjs +0 -22
- package/src/cli/first-run.mjs +0 -23
- package/src/cli/kernel.mjs +0 -262
- package/src/cli/mcp-targets/chatgpt.mjs +0 -51
- package/src/cli/mcp-targets/claude-code.mjs +0 -39
- package/src/cli/mcp-targets/claude-desktop.mjs +0 -24
- package/src/cli/mcp-targets/codex.mjs +0 -41
- package/src/cli/mcp-targets/cursor.mjs +0 -32
- package/src/cli/mcp-targets/shared.mjs +0 -71
- package/src/cli/mcp-targets/vscode.mjs +0 -39
- package/src/cli/messaging-platforms.mjs +0 -217
- package/src/cli/model-config.mjs +0 -78
- package/src/cli/operator-dispatch.mjs +0 -194
- package/src/cli/oracle-harness.py +0 -415
- package/src/cli/paths.mjs +0 -107
- package/src/cli/runtime.mjs +0 -351
- package/src/cli/setup-state.mjs +0 -168
- package/src/cli/spawn-child.mjs +0 -43
- package/src/data/catalog.mjs +0 -858
- package/src/data/desk-data.mjs +0 -877
- package/src/data/http.mjs +0 -253
- package/src/data/names.mjs +0 -188
- package/src/data/provider-endpoint.mjs +0 -94
- package/src/data/providers/aave-v3.mjs +0 -139
- package/src/data/providers/across-bridge.mjs +0 -41
- package/src/data/providers/aerodrome.mjs +0 -245
- package/src/data/providers/aevo.mjs +0 -33
- package/src/data/providers/approval-indexer.mjs +0 -133
- package/src/data/providers/approvals.mjs +0 -594
- package/src/data/providers/babylon-staking.mjs +0 -149
- package/src/data/providers/balancer.mjs +0 -209
- package/src/data/providers/beefy-yields.mjs +0 -45
- package/src/data/providers/birdeye-tokens.mjs +0 -33
- package/src/data/providers/bisq-markets.mjs +0 -145
- package/src/data/providers/bitcoin-esplora.mjs +0 -216
- package/src/data/providers/bitcoin-meta.mjs +0 -378
- package/src/data/providers/blockscout.mjs +0 -14
- package/src/data/providers/bob-rootstock.mjs +0 -198
- package/src/data/providers/braiins-insights.mjs +0 -103
- package/src/data/providers/bridge-aggregator.mjs +0 -259
- package/src/data/providers/bridges.mjs +0 -242
- package/src/data/providers/bungee.mjs +0 -29
- package/src/data/providers/celestia-da.mjs +0 -38
- package/src/data/providers/changenow.mjs +0 -135
- package/src/data/providers/compound.mjs +0 -32
- package/src/data/providers/connext.mjs +0 -28
- package/src/data/providers/convex.mjs +0 -30
- package/src/data/providers/cow-protocol.mjs +0 -110
- package/src/data/providers/cowswap.mjs +0 -415
- package/src/data/providers/curve.mjs +0 -201
- package/src/data/providers/das-assets.mjs +0 -59
- package/src/data/providers/debridge.mjs +0 -109
- package/src/data/providers/defillama.mjs +0 -88
- package/src/data/providers/dexscreener.mjs +0 -43
- package/src/data/providers/drift.mjs +0 -32
- package/src/data/providers/dydx.mjs +0 -41
- package/src/data/providers/evm-rpc.mjs +0 -225
- package/src/data/providers/farming.mjs +0 -291
- package/src/data/providers/geckoterminal.mjs +0 -34
- package/src/data/providers/gmx.mjs +0 -496
- package/src/data/providers/hiro-stacks.mjs +0 -150
- package/src/data/providers/hl-assets.mjs +0 -165
- package/src/data/providers/hl-info.mjs +0 -108
- package/src/data/providers/hl-markets.mjs +0 -210
- package/src/data/providers/hl-outcome.mjs +0 -61
- package/src/data/providers/hl-perps.mjs +0 -419
- package/src/data/providers/hl-staking.mjs +0 -353
- package/src/data/providers/hl-ws.mjs +0 -119
- package/src/data/providers/hyperevm-dex.mjs +0 -49
- package/src/data/providers/jito-mev.mjs +0 -117
- package/src/data/providers/jupiter-venues.mjs +0 -119
- package/src/data/providers/jupiter.mjs +0 -315
- package/src/data/providers/kamino-strategies.mjs +0 -143
- package/src/data/providers/l2beat-tvl.mjs +0 -42
- package/src/data/providers/lifi.mjs +0 -166
- package/src/data/providers/liquid-esplora.mjs +0 -152
- package/src/data/providers/magiceden-ordinals.mjs +0 -141
- package/src/data/providers/magiceden-sol.mjs +0 -406
- package/src/data/providers/mempool-lightning.mjs +0 -144
- package/src/data/providers/mempool-mining.mjs +0 -142
- package/src/data/providers/meteora-dlmm.mjs +0 -168
- package/src/data/providers/morpho.mjs +0 -177
- package/src/data/providers/nft-gallery.mjs +0 -163
- package/src/data/providers/nft-portfolio.mjs +0 -494
- package/src/data/providers/odos.mjs +0 -156
- package/src/data/providers/ondo.mjs +0 -31
- package/src/data/providers/oneinch.mjs +0 -174
- package/src/data/providers/opensea-multichain.mjs +0 -136
- package/src/data/providers/opensea-nft.mjs +0 -371
- package/src/data/providers/orca.mjs +0 -29
- package/src/data/providers/ordinals-runes.mjs +0 -180
- package/src/data/providers/pancakeswap.mjs +0 -28
- package/src/data/providers/paraswap.mjs +0 -125
- package/src/data/providers/pendle.mjs +0 -188
- package/src/data/providers/poly-clob.mjs +0 -283
- package/src/data/providers/poly-public.mjs +0 -96
- package/src/data/providers/poly-ws.mjs +0 -103
- package/src/data/providers/polygon-staking.mjs +0 -227
- package/src/data/providers/portfolio-history.mjs +0 -394
- package/src/data/providers/portfolio.mjs +0 -600
- package/src/data/providers/pyth-price-feeds.mjs +0 -172
- package/src/data/providers/raydium.mjs +0 -35
- package/src/data/providers/relayscan-mev.mjs +0 -72
- package/src/data/providers/rfq.mjs +0 -15
- package/src/data/providers/rh-agent.mjs +0 -59
- package/src/data/providers/sanctum-lst.mjs +0 -180
- package/src/data/providers/satflow.mjs +0 -341
- package/src/data/providers/signed-material-guard.mjs +0 -108
- package/src/data/providers/solana-rpc.mjs +0 -197
- package/src/data/providers/solend-lending.mjs +0 -166
- package/src/data/providers/spark-lend.mjs +0 -42
- package/src/data/providers/squid-router.mjs +0 -29
- package/src/data/providers/stargate.mjs +0 -29
- package/src/data/providers/sushiswap.mjs +0 -30
- package/src/data/providers/symbiotic-restaking.mjs +0 -41
- package/src/data/providers/thorchain.mjs +0 -36
- package/src/data/providers/traderjoe.mjs +0 -28
- package/src/data/providers/twap.mjs +0 -152
- package/src/data/providers/uniswap-v3.mjs +0 -457
- package/src/data/providers/uniswap-v4.mjs +0 -414
- package/src/data/providers/vertex.mjs +0 -41
- package/src/data/providers/wallet-watch.mjs +0 -264
- package/src/data/providers/wormhole.mjs +0 -28
- package/src/data/providers/yearn.mjs +0 -31
- package/src/data/providers/zerox.mjs +0 -167
- package/src/data/public-api-scan.mjs +0 -61
- package/src/data/quote-placeholder.mjs +0 -31
- package/src/desk/swap-prepare.mjs +0 -194
- package/src/equities/bridge.mjs +0 -388
- package/src/equities/fixtures/bridge_routes.json +0 -159
- package/src/equities/fixtures/dst_transitions.json +0 -29
- package/src/equities/fixtures/equity_dex_pools.json +0 -154
- package/src/equities/fixtures/hip3_l2books.json +0 -2711
- package/src/equities/fixtures/nyse_holidays.json +0 -1
- package/src/equities/fixtures/oracles.json +0 -100
- package/src/equities/fixtures/rh_pool_state.json +0 -244
- package/src/equities/fixtures/solana_xstocks.json +0 -704
- package/src/equities/fixtures/ton_xstocks.json +0 -35
- package/src/equities/fixtures/universe.json +0 -411
- package/src/equities/fixtures/venues.json +0 -3542
- package/src/equities/fixtures.mjs +0 -405
- package/src/equities/funding.mjs +0 -142
- package/src/equities/index.mjs +0 -166
- package/src/equities/liveness.mjs +0 -148
- package/src/equities/marketHours.mjs +0 -249
- package/src/equities/normalize.mjs +0 -295
- package/src/equities/num.mjs +0 -146
- package/src/equities/prepare.mjs +0 -95
- package/src/equities/rank.mjs +0 -260
- package/src/equities/sourceNum.mjs +0 -69
- package/src/equities/types.mjs +0 -220
- package/src/equities/venues/arcus-perp.mjs +0 -84
- package/src/equities/venues/arcus-spot.mjs +0 -105
- package/src/equities/venues/hyperliquid-hip3.mjs +0 -224
- package/src/equities/venues/rh-uniswap.mjs +0 -216
- package/src/equities/venues/solana-xstocks.mjs +0 -139
- package/src/equities/venues/ton-stonfi.mjs +0 -139
- package/src/exact-integer.mjs +0 -72
- package/src/exec-policy.mjs +0 -419
- package/src/flags.mjs +0 -15
- package/src/fresh-window.mjs +0 -76
- package/src/gmx-attestation.mjs +0 -176
- package/src/index.mjs +0 -107
- package/src/licensing/locals-only.mjs +0 -25
- package/src/nft-gas-war-guard.mjs +0 -139
- package/src/onboarding/agent-keys.mjs +0 -161
- package/src/onboarding/harness-configs.mjs +0 -102
- package/src/onboarding/index.mjs +0 -18
- package/src/onboarding/tiers.mjs +0 -139
- package/src/oracle-env.mjs +0 -55
- package/src/package-manifest.mjs +0 -42
- package/src/plugins/agent-plugin-loader.mjs +0 -225
- package/src/portfolio-risk.mjs +0 -170
- package/src/prepare-envelope.mjs +0 -204
- package/src/profile-upgrade.mjs +0 -277
- package/src/protocol-execution.mjs +0 -84
- package/src/protocol-templates/gate.mjs +0 -179
- package/src/protocol-templates/prepare-deploy.mjs +0 -81
- package/src/public-api/agent-grants.mjs +0 -57
- package/src/public-api/buzz-integration.mjs +0 -267
- package/src/public-api/connect-agent.mjs +0 -424
- package/src/public-api/grants.mjs +0 -142
- package/src/public-api/http.mjs +0 -707
- package/src/public-api/metering.mjs +0 -111
- package/src/public-control/aa-adapter.mjs +0 -402
- package/src/public-control/build-registry.mjs +0 -227
- package/src/public-control/bundler-client.mjs +0 -357
- package/src/public-control/grant-indexer.mjs +0 -296
- package/src/public-control/policy-render.mjs +0 -69
- package/src/public-control/policy-schema.mjs +0 -322
- package/src/public-control/runtime-config.mjs +0 -275
- package/src/public-control/session-key-model.mjs +0 -379
- package/src/public-control/session-orchestrator.mjs +0 -418
- package/src/rfq/intent.mjs +0 -180
- package/src/rfq/sources.mjs +0 -181
- package/src/route-attestation.mjs +0 -132
- package/src/router/best-execution.mjs +0 -326
- package/src/router/index.mjs +0 -250
- package/src/router/integrator-fee.mjs +0 -135
- package/src/router/prepare-bridge.mjs +0 -293
- package/src/router/prepare-route.mjs +0 -398
- package/src/router/proposal.mjs +0 -311
- package/src/router/risk-classifier.mjs +0 -119
- package/src/router/route-sources.mjs +0 -364
- package/src/scanner/chains.config.mjs +0 -543
- package/src/scanner/contract.mjs +0 -270
- package/src/scanner/evm-scanner.mjs +0 -394
- package/src/scanner/index.mjs +0 -9
- package/src/scanner/v2-venue.mjs +0 -351
- package/src/scanner/v3-venue.mjs +0 -308
- package/src/scopes.mjs +0 -44
- package/src/sell-simulation.mjs +0 -167
- package/src/signals/engine.mjs +0 -146
- package/src/signals/index.mjs +0 -1
- package/src/token-transfer-guard.mjs +0 -188
- package/src/tui/app.mjs +0 -438
- package/src/tui/backend.mjs +0 -215
- package/src/tui/format.mjs +0 -272
- package/src/tui/gateway-client.mjs +0 -177
- package/src/tui/input.mjs +0 -501
- package/src/tui/memory.mjs +0 -105
- package/src/tui/meta-learn.mjs +0 -85
- package/src/tui/renderer.mjs +0 -443
- package/src/tui/scheduler.mjs +0 -205
- package/src/tui/skills-loader.mjs +0 -140
- package/src/tui/standalone-client.mjs +0 -752
- package/src/tui/theme.mjs +0 -81
- package/src/vault-attestation.mjs +0 -146
- package/src/venues.mjs +0 -206
- package/src/watch-preferences.mjs +0 -85
- /package/{profiles → dist/assets/profiles}/_template/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/_template/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/bitcoin-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/bitcoin-agent/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/hyperliquid-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/hyperliquid-agent/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/oracle/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/oracle/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/polymarket-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/polymarket-agent/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/profile.schema.json +0 -0
- /package/{profiles → dist/assets/profiles}/protocol-builder/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/protocol-builder/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/robinhood-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/robinhood-agent/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/solana-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/solana-agent/profile.json +0 -0
- /package/{profiles → dist/assets/profiles}/stable-agent/SOUL.md +0 -0
- /package/{profiles → dist/assets/profiles}/stable-agent/profile.json +0 -0
- /package/{skills → dist/assets/skills}/balance/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/bitcoin-l1.md +0 -0
- /package/{skills → dist/assets/skills}/chain-defi-ecosystem-profiling.md +0 -0
- /package/{skills → dist/assets/skills}/evm-contract-research.md +0 -0
- /package/{skills → dist/assets/skills}/hyperliquid.md +0 -0
- /package/{skills → dist/assets/skills}/multichain-exec-desk.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-action-semantics/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-best-execution/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-best-execution.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-bitcoin/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-chain-graphs-telegram-cards/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-chat/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-chat/chain.SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-chat/setup.SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-circuit-breaker/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-contract-research/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-desk/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-desk.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-dex-launch/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-equities/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-grants/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-hypercore-staking/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-hyperliquid/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-meme-token-sniper/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-multichain-nft-launch/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-multichain-token-launch/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-nft-gacha-launch/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-nft-mint-gas-war/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-polymarket/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-protocol-builder/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-protocol-security/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-public-product/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-receipts/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-receipts.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-rfq-tokenized-assets/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-smart-wallet-scanner/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-solana/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-solana-nft/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/oracle-token-research/SKILL.md +0 -0
- /package/{skills → dist/assets/skills}/solana.md +0 -0
package/src/equities/index.mjs
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// Public API for cross-chain on-chain equities best execution.
|
|
2
|
-
// Prepare-only. Fixtures-backed offline by default; live probes are separate.
|
|
3
|
-
|
|
4
|
-
import { parseDecimal } from './num.mjs';
|
|
5
|
-
import { rank as rankQuotes } from './rank.mjs';
|
|
6
|
-
import { prepare as prepareRh } from './prepare.mjs';
|
|
7
|
-
import { filterQuotes } from './liveness.mjs';
|
|
8
|
-
import { quoteAll as hip3All, quote as hip3Quote, listDexs } from './venues/hyperliquid-hip3.mjs';
|
|
9
|
-
import { quoteAll as spotAll } from './venues/arcus-spot.mjs';
|
|
10
|
-
import { quoteAll as perpAll } from './venues/arcus-perp.mjs';
|
|
11
|
-
import { quoteAll as rhAll, quote as rhQuote, listPools } from './venues/rh-uniswap.mjs';
|
|
12
|
-
import { quoteAll as solAll, quote as solQuote, listTokens as solTokens } from './venues/solana-xstocks.mjs';
|
|
13
|
-
import { quoteAll as tonAll, quote as tonQuote, listTokens as tonTokens } from './venues/ton-stonfi.mjs';
|
|
14
|
-
import { getVenues } from './fixtures.mjs';
|
|
15
|
-
|
|
16
|
-
export { rank as rankEquityQuotes } from './rank.mjs';
|
|
17
|
-
export { prepare as prepareEquity } from './prepare.mjs';
|
|
18
|
-
export { validateQuote, validateRankResult } from './types.mjs';
|
|
19
|
-
export { loadFixtures, getVenues, getUniverse } from './fixtures.mjs';
|
|
20
|
-
|
|
21
|
-
function sizeArg(sizeUsd) {
|
|
22
|
-
if (sizeUsd == null || sizeUsd === '') return null;
|
|
23
|
-
if (typeof sizeUsd === 'object' && sizeUsd && typeof sizeUsd.mantissa === 'bigint') return sizeUsd;
|
|
24
|
-
return parseDecimal(String(sizeUsd));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Collect fixture-backed quotes for one equity ticker across all venues.
|
|
29
|
-
*/
|
|
30
|
-
export function collectEquityQuotes(ticker, { sizeUsd = null } = {}) {
|
|
31
|
-
if (typeof ticker !== 'string' || !ticker.trim()) {
|
|
32
|
-
throw new TypeError('ticker is required');
|
|
33
|
-
}
|
|
34
|
-
const t = ticker.trim().toUpperCase();
|
|
35
|
-
const size = sizeArg(sizeUsd);
|
|
36
|
-
const out = [];
|
|
37
|
-
|
|
38
|
-
const xyz = hip3Quote({ symbol: `xyz:${t}`, sizeUsd: size });
|
|
39
|
-
if (xyz) out.push(xyz);
|
|
40
|
-
else {
|
|
41
|
-
for (const q of hip3All({ ticker: t, sizeUsd: size })) out.push(q);
|
|
42
|
-
}
|
|
43
|
-
for (const q of perpAll({ sizeUsd: size })) if (q.symbol === t) out.push(q);
|
|
44
|
-
for (const q of spotAll({ sizeUsd: size })) if (q.symbol === t) out.push(q);
|
|
45
|
-
const rh = rhQuote({ symbol: t, sizeUsd: size });
|
|
46
|
-
if (rh) out.push(rh);
|
|
47
|
-
const sol = solQuote({ symbol: t, sizeUsd: size });
|
|
48
|
-
if (sol) out.push(sol);
|
|
49
|
-
const ton = tonQuote({ symbol: t, sizeUsd: size });
|
|
50
|
-
if (ton) out.push(ton);
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Rank equities venues for a ticker. Offline/fixtures by default.
|
|
56
|
-
*
|
|
57
|
-
* @param {object} p
|
|
58
|
-
* @param {string} p.ticker e.g. NVDA, SPY, TSLA
|
|
59
|
-
* @param {string|number} [p.sizeUsd]
|
|
60
|
-
* @param {number} [p.horizonHours] funding-adjusted heterogeneous ranking
|
|
61
|
-
*/
|
|
62
|
-
export function bestEquityRoute(p = {}) {
|
|
63
|
-
const ticker = String(p.ticker || p.symbol || '').trim().toUpperCase();
|
|
64
|
-
if (!ticker) throw new Error('bestEquityRoute requires ticker');
|
|
65
|
-
const sizeUsd = sizeArg(p.sizeUsd ?? p.size ?? null);
|
|
66
|
-
const quotes = collectEquityQuotes(ticker, { sizeUsd });
|
|
67
|
-
if (quotes.length === 0) {
|
|
68
|
-
return {
|
|
69
|
-
ticker,
|
|
70
|
-
rankedOn: 'gross',
|
|
71
|
-
ranked: [],
|
|
72
|
-
winner: null,
|
|
73
|
-
runnersUp: [],
|
|
74
|
-
improvementBps: null,
|
|
75
|
-
sourcesAnswered: 0,
|
|
76
|
-
sourcesTried: 0,
|
|
77
|
-
failed: [],
|
|
78
|
-
excluded: [],
|
|
79
|
-
bestPreparable: null,
|
|
80
|
-
instrumentMix: 'homogeneous',
|
|
81
|
-
note: 'no venue answered for this ticker in the pinned fixture capture',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// Offline fixtures use their own capturedAt. Advance 1s so freshness passes.
|
|
85
|
-
const nowMs = p.nowMs ?? (quotes[0].capturedAt + 1000);
|
|
86
|
-
const result = rankQuotes(quotes, {
|
|
87
|
-
nowMs,
|
|
88
|
-
horizonHours: p.horizonHours != null ? Number(p.horizonHours) : undefined,
|
|
89
|
-
skipLiveness: p.skipLiveness === true,
|
|
90
|
-
});
|
|
91
|
-
return { ticker, ...result };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Inventory + liveness snapshot for equities venues (fixtures).
|
|
96
|
-
*/
|
|
97
|
-
export function equityVenues() {
|
|
98
|
-
const inventory = [
|
|
99
|
-
{ venue: 'hyperliquid_hip3', tier: 'quote-only', chain: 'hyperliquid', n: hip3All().length },
|
|
100
|
-
{ venue: 'arcus_perp', tier: 'quote-only', chain: 'robinhood-4663', n: perpAll().length },
|
|
101
|
-
{ venue: 'arcus_spot', tier: 'quote-only', chain: 'robinhood-4663', n: spotAll().length },
|
|
102
|
-
{ venue: 'rh_uniswap', tier: 'prepare', chain: 4663, n: listPools().length },
|
|
103
|
-
{ venue: 'solana_xstocks', tier: 'quote-only', chain: 'solana', n: solTokens().length },
|
|
104
|
-
{ venue: 'ton_stonfi', tier: 'quote-only', chain: 'ton', n: tonTokens().length },
|
|
105
|
-
];
|
|
106
|
-
const all = [...hip3All(), ...perpAll(), ...spotAll(), ...rhAll(), ...solAll(), ...tonAll()];
|
|
107
|
-
const nowMs = (all[0]?.capturedAt ?? Date.now()) + 1000;
|
|
108
|
-
const { survivors, excluded } = filterQuotes(all, { nowMs });
|
|
109
|
-
return {
|
|
110
|
-
inventory,
|
|
111
|
-
hip3Dexs: listDexs(),
|
|
112
|
-
liveness: {
|
|
113
|
-
survivors: survivors.length,
|
|
114
|
-
excluded: excluded.length,
|
|
115
|
-
byReason: excluded.reduce((acc, e) => {
|
|
116
|
-
acc[e.reason] = (acc[e.reason] ?? 0) + 1;
|
|
117
|
-
return acc;
|
|
118
|
-
}, {}),
|
|
119
|
-
},
|
|
120
|
-
capturedAt: getVenues().capturedAt,
|
|
121
|
-
custody: 'prepare-only',
|
|
122
|
-
note: 'quote-only venues are ranked for discovery; only rh_uniswap is preparable in v1',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Prepare unsigned RH Uniswap artifact for an equity ticker.
|
|
128
|
-
* Requires a real recipient wallet. Never signs.
|
|
129
|
-
*/
|
|
130
|
-
export function prepareEquityRoute(p = {}) {
|
|
131
|
-
const ticker = String(p.ticker || p.symbol || '').trim().toUpperCase();
|
|
132
|
-
const recipient = p.recipient || p.taker;
|
|
133
|
-
if (!ticker) throw new Error('prepareEquityRoute requires ticker');
|
|
134
|
-
if (!recipient) throw new Error('prepareEquityRoute requires recipient (signing wallet)');
|
|
135
|
-
const sizeUsd = sizeArg(p.sizeUsd ?? p.size ?? null);
|
|
136
|
-
const q = rhQuote({ symbol: ticker, sizeUsd });
|
|
137
|
-
if (!q) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
failureKind: 'no-prepare-path',
|
|
141
|
-
ticker,
|
|
142
|
-
note: 'no RH Uniswap prepare-tier route for this ticker in fixtures',
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
const artifact = prepareRh({
|
|
147
|
-
symbol: ticker,
|
|
148
|
-
sizeUsd,
|
|
149
|
-
recipient,
|
|
150
|
-
nowMs: p.nowMs ?? q.capturedAt + 1000,
|
|
151
|
-
});
|
|
152
|
-
return { ok: true, ticker, artifact, bestPreparableVenue: 'rh_uniswap' };
|
|
153
|
-
} catch (err) {
|
|
154
|
-
return {
|
|
155
|
-
ok: false,
|
|
156
|
-
failureKind: 'prepare-refused',
|
|
157
|
-
ticker,
|
|
158
|
-
error: String(err.message || err),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** JSON-safe clone (bigint -> string). */
|
|
164
|
-
export function toJsonSafe(value) {
|
|
165
|
-
return JSON.parse(JSON.stringify(value, (_, v) => (typeof v === 'bigint' ? v.toString() : v)));
|
|
166
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// Liveness gate. This is the data integrity layer that adapters deliberately
|
|
2
|
-
// do not implement. Adapters report source truth, including dormant venues and
|
|
3
|
-
// crossed books. This module decides what is trustworthy enough to rank.
|
|
4
|
-
//
|
|
5
|
-
// Three outcome buckets (never collapse them):
|
|
6
|
-
// ranked-eligible quotes -> survive
|
|
7
|
-
// excluded[] -> answered, but untrustworthy (dormant, crossed, stale)
|
|
8
|
-
// failed[] -> reserved for sources that did not answer (not used here)
|
|
9
|
-
//
|
|
10
|
-
// Every exclusion carries a machine readable reason and evidence.
|
|
11
|
-
|
|
12
|
-
import { cmp } from './num.mjs';
|
|
13
|
-
import { sessionState } from './marketHours.mjs';
|
|
14
|
-
|
|
15
|
-
export const REASONS = Object.freeze({
|
|
16
|
-
DORMANT_VENUE: 'dormant-venue',
|
|
17
|
-
CROSSED_BOOK: 'crossed-book',
|
|
18
|
-
STALE_QUOTE: 'stale-quote',
|
|
19
|
-
STALE_ORACLE: 'stale-oracle',
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Defaults. Live quotes older than this are excluded. Fixtures carry a pinned
|
|
23
|
-
// capturedAt so offline tests are deterministic against a frozen "now".
|
|
24
|
-
export const DEFAULT_QUOTE_FRESHNESS_MS = 120_000;
|
|
25
|
-
// Oracle maxStaleness on chain is 86400s (lending grade). Far too loose for
|
|
26
|
-
// ranking. We apply our own bound during core session.
|
|
27
|
-
export const DEFAULT_ORACLE_FRESHNESS_MS = 600_000;
|
|
28
|
-
|
|
29
|
-
function exclude(list, entry) {
|
|
30
|
-
list.push(Object.freeze({
|
|
31
|
-
venue: entry.venue,
|
|
32
|
-
symbol: entry.symbol ?? null,
|
|
33
|
-
reason: entry.reason,
|
|
34
|
-
evidence: entry.evidence ?? null,
|
|
35
|
-
}));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isDormantQuote(quote) {
|
|
39
|
-
// HIP-3: vol24h in raw. Arcus: vol24h in raw. Spot: vol24h in raw.
|
|
40
|
-
// A missing volume is treated as unknown, not dormant. Only an explicit zero
|
|
41
|
-
// (or sub-epsilon) volume marks the venue/market dormant.
|
|
42
|
-
const vol = quote.raw?.vol24h;
|
|
43
|
-
if (vol === 0 || vol === 0.0) return true;
|
|
44
|
-
if (typeof vol === 'number' && Number.isFinite(vol) && vol <= 0) return true;
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isCrossed(quote) {
|
|
49
|
-
if (!quote.bid || !quote.ask) return false;
|
|
50
|
-
return cmp(quote.bid, quote.ask) > 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isStale(quote, nowMs, freshnessMs) {
|
|
54
|
-
if (!Number.isSafeInteger(quote.capturedAt)) return false;
|
|
55
|
-
return nowMs - quote.capturedAt > freshnessMs;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Filter a list of validating Quotes into survivors and exclusions.
|
|
60
|
-
*
|
|
61
|
-
* @param {object[]} quotes
|
|
62
|
-
* @param {object} [opts]
|
|
63
|
-
* @param {number} [opts.nowMs] clock, defaults to Date.now(); tests inject
|
|
64
|
-
* @param {number} [opts.quoteFreshnessMs]
|
|
65
|
-
* @param {number} [opts.oracleFreshnessMs]
|
|
66
|
-
* @param {boolean} [opts.includeDormant] if true, dormant markets pass with a tag
|
|
67
|
-
* @returns {{ survivors: object[], excluded: object[] }}
|
|
68
|
-
*/
|
|
69
|
-
export function filterQuotes(quotes, opts = {}) {
|
|
70
|
-
if (!Array.isArray(quotes)) throw new TypeError('quotes must be an array');
|
|
71
|
-
|
|
72
|
-
const nowMs = opts.nowMs ?? Date.now();
|
|
73
|
-
const quoteFreshnessMs = opts.quoteFreshnessMs ?? DEFAULT_QUOTE_FRESHNESS_MS;
|
|
74
|
-
const oracleFreshnessMs = opts.oracleFreshnessMs ?? DEFAULT_ORACLE_FRESHNESS_MS;
|
|
75
|
-
const includeDormant = opts.includeDormant === true;
|
|
76
|
-
|
|
77
|
-
const survivors = [];
|
|
78
|
-
const excluded = [];
|
|
79
|
-
|
|
80
|
-
for (const quote of quotes) {
|
|
81
|
-
// Dormant venue / market.
|
|
82
|
-
if (isDormantQuote(quote)) {
|
|
83
|
-
if (includeDormant) {
|
|
84
|
-
survivors.push(Object.freeze({ ...quote, staleData: true }));
|
|
85
|
-
} else {
|
|
86
|
-
exclude(excluded, {
|
|
87
|
-
venue: quote.venue,
|
|
88
|
-
symbol: quote.symbolRaw ?? quote.symbol,
|
|
89
|
-
reason: REASONS.DORMANT_VENUE,
|
|
90
|
-
evidence: { vol24h: quote.raw?.vol24h ?? null },
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Crossed book.
|
|
97
|
-
if (isCrossed(quote)) {
|
|
98
|
-
exclude(excluded, {
|
|
99
|
-
venue: quote.venue,
|
|
100
|
-
symbol: quote.symbolRaw ?? quote.symbol,
|
|
101
|
-
reason: REASONS.CROSSED_BOOK,
|
|
102
|
-
evidence: {
|
|
103
|
-
bid: quote.bid ? String(quote.bid.mantissa) + 'e-' + quote.bid.scale : null,
|
|
104
|
-
ask: quote.ask ? String(quote.ask.mantissa) + 'e-' + quote.ask.scale : null,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Stale quote.
|
|
111
|
-
if (isStale(quote, nowMs, quoteFreshnessMs)) {
|
|
112
|
-
exclude(excluded, {
|
|
113
|
-
venue: quote.venue,
|
|
114
|
-
symbol: quote.symbolRaw ?? quote.symbol,
|
|
115
|
-
reason: REASONS.STALE_QUOTE,
|
|
116
|
-
evidence: {
|
|
117
|
-
capturedAt: quote.capturedAt,
|
|
118
|
-
nowMs,
|
|
119
|
-
ageMs: nowMs - quote.capturedAt,
|
|
120
|
-
freshnessMs: quoteFreshnessMs,
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Stale oracle reference (only when the quote carries one as its mid/mark source
|
|
127
|
-
// identity is the oracle itself). RH oracle-only reads are reference tier and
|
|
128
|
-
// ranked separately; when a quote's only price is an oracle print past our
|
|
129
|
-
// bound during core session, exclude it.
|
|
130
|
-
if (quote.tier === 'read-only' && quote.raw?.oracleRaw !== undefined) {
|
|
131
|
-
const session = sessionState(nowMs);
|
|
132
|
-
const bound = session === 'core' ? oracleFreshnessMs : oracleFreshnessMs * 2;
|
|
133
|
-
if (isStale(quote, nowMs, bound)) {
|
|
134
|
-
exclude(excluded, {
|
|
135
|
-
venue: quote.venue,
|
|
136
|
-
symbol: quote.symbolRaw ?? quote.symbol,
|
|
137
|
-
reason: REASONS.STALE_ORACLE,
|
|
138
|
-
evidence: { capturedAt: quote.capturedAt, nowMs, session },
|
|
139
|
-
});
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
survivors.push(quote);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { survivors, excluded };
|
|
148
|
-
}
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// US equity market session classification.
|
|
2
|
-
//
|
|
3
|
-
// A price printed at 3am is not the same quality of fact as a price printed at
|
|
4
|
-
// noon. This module says which of three sessions an instant falls in so the
|
|
5
|
-
// rest of the product can price staleness risk honestly.
|
|
6
|
-
//
|
|
7
|
-
// 'core' [09:30, 16:00) ET real price discovery, tightest spreads
|
|
8
|
-
// 'extended' [04:00, 09:30) ET pre market, and [16:00, 20:00) ET post market
|
|
9
|
-
// thin books, wider spreads, higher staleness risk
|
|
10
|
-
// 'dark' everything else, plus every weekend and every NYSE holiday
|
|
11
|
-
//
|
|
12
|
-
// The original brief modelled 04:00 to 20:00 as one uniform lit window. That is
|
|
13
|
-
// wrong: it matches the Arcus regularTradingHours flag but not market reality,
|
|
14
|
-
// because staleness risk differs materially between core and pre/post. Core and
|
|
15
|
-
// extended are therefore distinct states here.
|
|
16
|
-
//
|
|
17
|
-
// BOUNDARY CONVENTION: every interval is half open, [start, end), in ET wall
|
|
18
|
-
// clock. 09:30:00.000 ET exactly is 'core'; 16:00:00.000 ET exactly is
|
|
19
|
-
// 'extended'; 04:00:00.000 ET is 'extended' and 20:00:00.000 ET is 'dark'.
|
|
20
|
-
//
|
|
21
|
-
// ET is derived from UTC using fixtures/dst_transitions.json only. No UTC offset
|
|
22
|
-
// is hardcoded and no host timezone database is consulted, so this module is
|
|
23
|
-
// deterministic and offline. Instants outside the fixture's coverage throw
|
|
24
|
-
// rather than guess an offset: unknown is not zero.
|
|
25
|
-
|
|
26
|
-
import { getDstTransitions, getNyseHolidays } from './fixtures.mjs';
|
|
27
|
-
|
|
28
|
-
const MS_PER_SECOND = 1000;
|
|
29
|
-
const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
30
|
-
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
31
|
-
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
32
|
-
|
|
33
|
-
// Eastern Standard Time is UTC-5, Eastern Daylight Time is UTC-4. These are the
|
|
34
|
-
// two possible ET offsets, selected per instant by the fixture, never assumed.
|
|
35
|
-
const EST_OFFSET_MS = -5 * MS_PER_HOUR;
|
|
36
|
-
const EDT_OFFSET_MS = -4 * MS_PER_HOUR;
|
|
37
|
-
|
|
38
|
-
// ET wall clock minutes since midnight.
|
|
39
|
-
const PRE_MARKET_OPEN_MIN = 4 * 60; // 04:00
|
|
40
|
-
const CORE_OPEN_MIN = 9 * 60 + 30; // 09:30
|
|
41
|
-
const CORE_CLOSE_MIN = 16 * 60; // 16:00
|
|
42
|
-
const POST_MARKET_CLOSE_MIN = 20 * 60; // 20:00
|
|
43
|
-
|
|
44
|
-
const SESSION_EDGES_MIN = Object.freeze([
|
|
45
|
-
PRE_MARKET_OPEN_MIN,
|
|
46
|
-
CORE_OPEN_MIN,
|
|
47
|
-
CORE_CLOSE_MIN,
|
|
48
|
-
POST_MARKET_CLOSE_MIN,
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
export const SESSION_STATES = Object.freeze(['core', 'extended', 'dark']);
|
|
52
|
-
|
|
53
|
-
// The fixture stores startUtc / endUtc as epoch SECONDS (verified: 1772953200
|
|
54
|
-
// decodes to 2026-03-08T07:00:00Z, the 2026 spring forward; as milliseconds it
|
|
55
|
-
// would decode to 1970). Normalized to milliseconds once, at load.
|
|
56
|
-
let dstIndex;
|
|
57
|
-
let holidaySet;
|
|
58
|
-
|
|
59
|
-
function buildDstIndex() {
|
|
60
|
-
const rows = getDstTransitions().dstTransitions.map((entry) => ({
|
|
61
|
-
year: entry.year,
|
|
62
|
-
startMs: entry.startUtc * MS_PER_SECOND,
|
|
63
|
-
endMs: entry.endUtc * MS_PER_SECOND,
|
|
64
|
-
}));
|
|
65
|
-
rows.sort((a, b) => a.startMs - b.startMs);
|
|
66
|
-
|
|
67
|
-
// Coverage runs from the start of the first covered year to the end of the
|
|
68
|
-
// last covered year, in UTC. Outside that range we have no ground truth.
|
|
69
|
-
const firstYear = rows[0].year;
|
|
70
|
-
const lastYear = rows[rows.length - 1].year;
|
|
71
|
-
return Object.freeze({
|
|
72
|
-
rows: Object.freeze(rows),
|
|
73
|
-
firstYear,
|
|
74
|
-
lastYear,
|
|
75
|
-
coverageStartMs: Date.UTC(firstYear, 0, 1),
|
|
76
|
-
coverageEndMs: Date.UTC(lastYear + 1, 0, 1),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function dst() {
|
|
81
|
-
dstIndex ??= buildDstIndex();
|
|
82
|
-
return dstIndex;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// holidayDayIndex entries are epoch days counted in UTC: 20454 * 86400000 is
|
|
86
|
-
// 1970-01-01 + 20454 days = 2026-01-01, which matches the aligned dates[0].
|
|
87
|
-
// Verified positionally against all 50 entries of the dates array.
|
|
88
|
-
function holidays() {
|
|
89
|
-
if (holidaySet === undefined) {
|
|
90
|
-
holidaySet = new Set(getNyseHolidays().holidayDayIndex);
|
|
91
|
-
}
|
|
92
|
-
return holidaySet;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function assertEpochMs(epochMs, label) {
|
|
96
|
-
if (!Number.isSafeInteger(epochMs)) {
|
|
97
|
-
throw new TypeError(`${label}: epochMs must be a safe integer number of milliseconds`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function assertCovered(epochMs, label) {
|
|
102
|
-
const { coverageStartMs, coverageEndMs, firstYear, lastYear } = dst();
|
|
103
|
-
if (epochMs < coverageStartMs || epochMs >= coverageEndMs) {
|
|
104
|
-
throw new RangeError(
|
|
105
|
-
`${label}: ${new Date(epochMs).toISOString()} is outside DST fixture coverage ` +
|
|
106
|
-
`(${firstYear} through ${lastYear}). Refusing to assume a UTC offset.`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// True when the instant falls inside a fixture daylight saving window. The
|
|
112
|
-
// fixture boundaries are the exact UTC instants the offset changes, so the
|
|
113
|
-
// comparison is half open on both ends of the DST window too.
|
|
114
|
-
function isDaylightTime(epochMs) {
|
|
115
|
-
for (const row of dst().rows) {
|
|
116
|
-
if (epochMs >= row.startMs && epochMs < row.endMs) return true;
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function etOffsetMs(epochMs) {
|
|
122
|
-
return isDaylightTime(epochMs) ? EDT_OFFSET_MS : EST_OFFSET_MS;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Decompose an instant into ET civil fields without any host timezone lookup.
|
|
126
|
-
// Shifting the epoch by the ET offset and then reading UTC getters yields ET
|
|
127
|
-
// wall clock, which is exactly what the session rules are written against.
|
|
128
|
-
function etFields(epochMs) {
|
|
129
|
-
const shifted = epochMs + etOffsetMs(epochMs);
|
|
130
|
-
const dayIndex = Math.floor(shifted / MS_PER_DAY);
|
|
131
|
-
const msIntoDay = shifted - dayIndex * MS_PER_DAY;
|
|
132
|
-
const date = new Date(shifted);
|
|
133
|
-
return {
|
|
134
|
-
dayIndex,
|
|
135
|
-
minuteOfDay: Math.floor(msIntoDay / MS_PER_MINUTE),
|
|
136
|
-
msIntoDay,
|
|
137
|
-
weekday: date.getUTCDay(), // 0 Sunday through 6 Saturday, in ET
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function isTradingDay({ dayIndex, weekday }) {
|
|
142
|
-
if (weekday === 0 || weekday === 6) return false;
|
|
143
|
-
return !holidays().has(dayIndex);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function stateForMinuteOfDay(minuteOfDay) {
|
|
147
|
-
if (minuteOfDay < PRE_MARKET_OPEN_MIN) return 'dark';
|
|
148
|
-
if (minuteOfDay < CORE_OPEN_MIN) return 'extended';
|
|
149
|
-
if (minuteOfDay < CORE_CLOSE_MIN) return 'core';
|
|
150
|
-
if (minuteOfDay < POST_MARKET_CLOSE_MIN) return 'extended';
|
|
151
|
-
return 'dark';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Which session the instant falls in.
|
|
156
|
-
* @param {number} epochMs epoch milliseconds, UTC
|
|
157
|
-
* @returns {'core'|'extended'|'dark'}
|
|
158
|
-
*/
|
|
159
|
-
export function sessionState(epochMs) {
|
|
160
|
-
assertEpochMs(epochMs, 'sessionState');
|
|
161
|
-
assertCovered(epochMs, 'sessionState');
|
|
162
|
-
|
|
163
|
-
const fields = etFields(epochMs);
|
|
164
|
-
if (!isTradingDay(fields)) return 'dark';
|
|
165
|
-
return stateForMinuteOfDay(fields.minuteOfDay);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Convenience predicate: no lit venue is quoting into real price discovery.
|
|
170
|
-
* @param {number} epochMs epoch milliseconds, UTC
|
|
171
|
-
* @returns {boolean}
|
|
172
|
-
*/
|
|
173
|
-
export function isDark(epochMs) {
|
|
174
|
-
assertEpochMs(epochMs, 'isDark');
|
|
175
|
-
return sessionState(epochMs) === 'dark';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Convert an ET civil day plus an ET minute of day back to an epoch instant.
|
|
179
|
-
// The offset depends on the instant itself, so this resolves the offset by fixed
|
|
180
|
-
// point: guess with one offset, recompute the offset at the candidate instant,
|
|
181
|
-
// and re-derive. Two passes converge for every real ET wall time, and the
|
|
182
|
-
// verify step below rejects the wall times that do not exist (the skipped hour
|
|
183
|
-
// on spring forward day). Session edges are 04:00 and later so they are never
|
|
184
|
-
// inside the 02:00 to 03:00 gap, but the guard is kept rather than assumed away.
|
|
185
|
-
function etWallToEpochMs(dayIndex, minuteOfDay) {
|
|
186
|
-
const naive = dayIndex * MS_PER_DAY + minuteOfDay * MS_PER_MINUTE;
|
|
187
|
-
let candidate = naive - EST_OFFSET_MS;
|
|
188
|
-
for (let pass = 0; pass < 2; pass += 1) {
|
|
189
|
-
const next = naive - etOffsetMs(candidate);
|
|
190
|
-
if (next === candidate) break;
|
|
191
|
-
candidate = next;
|
|
192
|
-
}
|
|
193
|
-
const check = etFields(candidate);
|
|
194
|
-
if (check.dayIndex !== dayIndex || check.minuteOfDay !== minuteOfDay) return null;
|
|
195
|
-
return candidate;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* The next instant at which the session state changes, at or after epochMs.
|
|
200
|
-
*
|
|
201
|
-
* Scans forward day by day over real ET calendar days so weekends, holidays,
|
|
202
|
-
* and DST changes are all handled by the same code path that classifies a
|
|
203
|
-
* single instant. Returns { at, from, to } where `at` is the first millisecond
|
|
204
|
-
* of the new state (edges are inclusive starts, matching sessionState).
|
|
205
|
-
*
|
|
206
|
-
* @param {number} epochMs epoch milliseconds, UTC
|
|
207
|
-
* @returns {{at: number, from: 'core'|'extended'|'dark', to: 'core'|'extended'|'dark'}}
|
|
208
|
-
*/
|
|
209
|
-
export function nextTransition(epochMs) {
|
|
210
|
-
assertEpochMs(epochMs, 'nextTransition');
|
|
211
|
-
assertCovered(epochMs, 'nextTransition');
|
|
212
|
-
|
|
213
|
-
const from = sessionState(epochMs);
|
|
214
|
-
const startFields = etFields(epochMs);
|
|
215
|
-
|
|
216
|
-
// A dark stretch can span a long weekend plus a holiday, and the fixture has
|
|
217
|
-
// no gap longer than a few days, so a bounded scan is enough. 400 days covers
|
|
218
|
-
// any run of dark days the calendar can produce inside fixture coverage.
|
|
219
|
-
const MAX_DAYS_SCANNED = 400;
|
|
220
|
-
|
|
221
|
-
for (let offset = 0; offset < MAX_DAYS_SCANNED; offset += 1) {
|
|
222
|
-
const dayIndex = startFields.dayIndex + offset;
|
|
223
|
-
const edges = [];
|
|
224
|
-
|
|
225
|
-
// Day boundary itself is a candidate edge: the state can change at ET
|
|
226
|
-
// midnight when a lit day rolls into a dark weekend day or vice versa.
|
|
227
|
-
if (offset > 0) edges.push(0);
|
|
228
|
-
for (const edgeMin of SESSION_EDGES_MIN) edges.push(edgeMin);
|
|
229
|
-
|
|
230
|
-
for (const edgeMin of edges) {
|
|
231
|
-
const at = etWallToEpochMs(dayIndex, edgeMin);
|
|
232
|
-
if (at === null) continue; // nonexistent wall time on a spring forward day
|
|
233
|
-
if (at <= epochMs) continue;
|
|
234
|
-
if (at >= dst().coverageEndMs) {
|
|
235
|
-
throw new RangeError(
|
|
236
|
-
`nextTransition: next boundary after ${new Date(epochMs).toISOString()} falls ` +
|
|
237
|
-
`outside DST fixture coverage (through ${dst().lastYear}).`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
const to = sessionState(at);
|
|
241
|
-
if (to !== from) return Object.freeze({ at, from, to });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
throw new RangeError(
|
|
246
|
-
`nextTransition: no session change found within ${MAX_DAYS_SCANNED} days of ` +
|
|
247
|
-
`${new Date(epochMs).toISOString()}`,
|
|
248
|
-
);
|
|
249
|
-
}
|