├── .dockerignore ├── .env ├── .env.dev ├── .env.development ├── .env.test ├── .github └── workflows │ └── javascript.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── README.md ├── docker-compose.yml ├── docs ├── aave-utilities.md ├── docker.md ├── font-breakdown.png ├── maintenance-mode.md ├── monitoring.md ├── old-production.md ├── old-staging.md ├── speed.md └── tests.md ├── eslint.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts ├── build-deps.sh ├── release.sh ├── server.js ├── update-deps.sh ├── update-production.sh └── update-test-screenshots.sh ├── src ├── ambient.d.ts ├── app.d.ts ├── app.html ├── hooks.client.ts ├── hooks.server.ts ├── lib │ ├── __mocks__ │ │ └── config.ts │ ├── actions │ │ ├── image.ts │ │ ├── scroll.ts │ │ └── viewport.ts │ ├── assets │ │ ├── brand-mark.svg │ │ ├── ethlisbon.webp │ │ ├── icons │ │ │ ├── 24h.svg │ │ │ ├── accessibility.svg │ │ │ ├── arrow-down.svg │ │ │ ├── arrow-left-down.svg │ │ │ ├── arrow-left-up.svg │ │ │ ├── arrow-left.svg │ │ │ ├── arrow-right-down.svg │ │ │ ├── arrow-right-up.svg │ │ │ ├── arrow-right.svg │ │ │ ├── arrow-up.svg │ │ │ ├── backtesting.svg │ │ │ ├── blockchain.svg │ │ │ ├── book.svg │ │ │ ├── cancel.svg │ │ │ ├── check-square.svg │ │ │ ├── chevron-down.svg │ │ │ ├── chevron-left.svg │ │ │ ├── chevron-right.svg │ │ │ ├── chevron-up.svg │ │ │ ├── community.svg │ │ │ ├── console.svg │ │ │ ├── copy-to-clipboard.svg │ │ │ ├── dictionary.svg │ │ │ ├── discord.svg │ │ │ ├── download.svg │ │ │ ├── empty.svg │ │ │ ├── error.svg │ │ │ ├── exchange.svg │ │ │ ├── external-link.svg │ │ │ ├── facebook.svg │ │ │ ├── filter.svg │ │ │ ├── fullscreen.svg │ │ │ ├── github.svg │ │ │ ├── history.svg │ │ │ ├── info.svg │ │ │ ├── knowledge.svg │ │ │ ├── lending-reserve.svg │ │ │ ├── link.svg │ │ │ ├── linkedin.svg │ │ │ ├── mail.svg │ │ │ ├── medium.svg │ │ │ ├── menu.svg │ │ │ ├── newspaper.svg │ │ │ ├── oracle.svg │ │ │ ├── pair.svg │ │ │ ├── performance-increase.svg │ │ │ ├── profitable.svg │ │ │ ├── python.svg │ │ │ ├── question-circle.svg │ │ │ ├── read-glasses.svg │ │ │ ├── reading.svg │ │ │ ├── refresh.svg │ │ │ ├── rss.svg │ │ │ ├── search.svg │ │ │ ├── secure.svg │ │ │ ├── server-sync.svg │ │ │ ├── strategy.svg │ │ │ ├── success.svg │ │ │ ├── sun.svg │ │ │ ├── telegram.svg │ │ │ ├── trend-down.svg │ │ │ ├── trend-up.svg │ │ │ ├── twitter.svg │ │ │ ├── unlink.svg │ │ │ ├── wallet.svg │ │ │ ├── warning.svg │ │ │ └── youtube.svg │ │ ├── illustrations │ │ │ ├── analyze-data-1.svg │ │ │ ├── analyze-data-2.svg │ │ │ ├── blockchain-1.svg │ │ │ ├── bull-vs-bear.svg │ │ │ ├── communication-1.svg │ │ │ ├── data-cloud-1.svg │ │ │ ├── database-1.svg │ │ │ ├── database-2.svg │ │ │ ├── newsletter.svg │ │ │ ├── newspaper-1.svg │ │ │ ├── security-1.svg │ │ │ └── working-together.svg │ │ ├── logo-horizontal.svg │ │ ├── logos │ │ │ ├── blockchains │ │ │ │ ├── arbitrum.svg │ │ │ │ ├── avalanche.svg │ │ │ │ ├── base.svg │ │ │ │ ├── berachain.svg │ │ │ │ ├── binance.svg │ │ │ │ ├── ethereum.svg │ │ │ │ ├── polygon.svg │ │ │ │ └── unichain.svg │ │ │ ├── exchanges │ │ │ │ ├── pancakeswap.svg │ │ │ │ ├── sushiswap.svg │ │ │ │ └── uniswap.svg │ │ │ ├── partners │ │ │ │ └── enzyme.svg │ │ │ ├── tokens │ │ │ │ ├── aave.svg │ │ │ │ ├── arb.svg │ │ │ │ ├── avax.svg │ │ │ │ ├── bnb.svg │ │ │ │ ├── btc.svg │ │ │ │ ├── enzyme.svg │ │ │ │ ├── eth.svg │ │ │ │ ├── lagoon.svg │ │ │ │ ├── matic.svg │ │ │ │ ├── pol.svg │ │ │ │ ├── usdc.svg │ │ │ │ ├── usdt.svg │ │ │ │ ├── usd₮0.svg │ │ │ │ ├── velvet.svg │ │ │ │ ├── wbtc.svg │ │ │ │ └── weth.svg │ │ │ └── wallets │ │ │ │ ├── browser-wallet.svg │ │ │ │ ├── metamask.svg │ │ │ │ └── walletconnect.svg │ │ ├── misc │ │ │ └── mbp-15.webp │ │ ├── partners │ │ │ ├── avalanche.svg │ │ │ ├── blizzard.svg │ │ │ ├── chartiq.svg │ │ │ ├── infinity-ventures-crypto.svg │ │ │ ├── mentha-partners.svg │ │ │ ├── pet-rock-capital.svg │ │ │ ├── republic-capital.svg │ │ │ ├── timescale.svg │ │ │ ├── tradingview.svg │ │ │ └── typesense.svg │ │ └── tos │ │ │ ├── 155d6737cb.txt │ │ │ ├── 2024-03-20.txt │ │ │ ├── 2024-03-30.txt │ │ │ ├── 2024-12-19.txt │ │ │ └── README.me │ ├── blog │ │ └── client.ts │ ├── breadcrumb │ │ └── Breadcrumbs.svelte │ ├── chain │ │ └── tokenstandard.ts │ ├── charts │ │ ├── AreaSeries.svelte │ │ ├── BaselineSeries.svelte │ │ ├── BenchmarkSeries.svelte │ │ ├── CandleSeries.svelte │ │ ├── CandleVolumeSeries.svelte │ │ ├── ChartHeader.svelte │ │ ├── ChartTooltip.svelte │ │ ├── NetflowSeries.svelte │ │ ├── PairCandleChart.svelte │ │ ├── ReserveInterestChart.svelte │ │ ├── Series.svelte │ │ ├── SeriesContent.svelte │ │ ├── SeriesLabel.svelte │ │ ├── StrategyChart.svelte │ │ ├── TvChart.svelte │ │ ├── candle-data-feed.svelte.ts │ │ ├── helpers.ts │ │ ├── time-span.ts │ │ └── types.ts │ ├── components │ │ ├── Alert.svelte │ │ ├── AlertItem.svelte │ │ ├── AlertList.svelte │ │ ├── Banner.svelte │ │ ├── BlogPostTile.svelte │ │ ├── BlogRoll.svelte │ │ ├── Button.svelte │ │ ├── ContentCard.svelte │ │ ├── ContentCardsSection.svelte │ │ ├── ContentCardsTemplate.svelte │ │ ├── CopyWidget.svelte │ │ ├── CryptoAddressWidget.svelte │ │ ├── DataBadge.svelte │ │ ├── DataBox.svelte │ │ ├── Dialog.svelte │ │ ├── EntitySymbol.svelte │ │ ├── Footer.svelte │ │ ├── Grid.svelte │ │ ├── HashAddress.svelte │ │ ├── Header.svelte │ │ ├── HeroBanner.svelte │ │ ├── HeroVideo.svelte │ │ ├── Logo.svelte │ │ ├── Markdown.svelte │ │ ├── Menu.svelte │ │ ├── MenuItem.svelte │ │ ├── MoneyInput.svelte │ │ ├── NavPanel.svelte │ │ ├── PageHeader.svelte │ │ ├── PageHeading.svelte │ │ ├── Profitability.svelte │ │ ├── Profitability.test.ts │ │ ├── Section.svelte │ │ ├── SegmentedControl.svelte │ │ ├── Select.svelte │ │ ├── SourceCode.svelte │ │ ├── Spinner.svelte │ │ ├── SummaryBox.svelte │ │ ├── TargetableLink.svelte │ │ ├── TextInput.svelte │ │ ├── Timestamp.svelte │ │ ├── Timestamp.test.ts │ │ ├── Tooltip.svelte │ │ ├── Tooltip.test.ts │ │ ├── TradingDataInfo.svelte │ │ ├── TradingDataInfoRow.svelte │ │ ├── css │ │ │ ├── animations.css │ │ │ ├── body-link.css │ │ │ ├── breakpoints.css │ │ │ ├── bullish-bearish.css │ │ │ ├── color.css │ │ │ ├── datatable.css │ │ │ ├── index.css │ │ │ ├── input.css │ │ │ ├── layout.css │ │ │ ├── metrics-table.css │ │ │ ├── progress.css │ │ │ ├── radius-new.css │ │ │ ├── radius.css │ │ │ ├── reset.css │ │ │ ├── shadows.css │ │ │ ├── skeleton.css │ │ │ ├── space.css │ │ │ ├── sr-only.css │ │ │ ├── terminal.css │ │ │ ├── tile.css │ │ │ ├── time.css │ │ │ ├── transitions.css │ │ │ ├── truncate.css │ │ │ ├── typography-new.css │ │ │ └── typography.css │ │ ├── datatable │ │ │ ├── DataTable.svelte │ │ │ ├── MobileSortSelect.svelte │ │ │ ├── PageButton.svelte │ │ │ ├── SearchHeaderRow.svelte │ │ │ ├── TableBody.svelte │ │ │ ├── TableFooter.svelte │ │ │ ├── TableHeader.svelte │ │ │ ├── TableRow.svelte │ │ │ └── TableRowTarget.svelte │ │ └── index.ts │ ├── config.ts │ ├── eth-defi │ │ ├── abi │ │ │ └── TermsOfService.json.ts │ │ ├── eip-3009.ts │ │ ├── helpers.ts │ │ └── schemas │ │ │ ├── core.ts │ │ │ ├── token.ts │ │ │ └── transaction.ts │ ├── explorer │ │ ├── BorrowAprCell.svelte │ │ ├── ExchangeTable.svelte │ │ ├── LendingReserveLabel.svelte │ │ ├── LendingReserveTable.svelte │ │ ├── PairSymbolCell.svelte │ │ ├── PairTable.svelte │ │ ├── TokenTable.svelte │ │ ├── lending-reserve-client.ts │ │ ├── pair-client.ts │ │ └── token-client.ts │ ├── header │ │ ├── AppHead.svelte │ │ ├── ColorModePicker.svelte │ │ ├── Navbar.svelte │ │ ├── PageLoadProgressBar.svelte │ │ └── SiteMode.svelte │ ├── helpers │ │ ├── assets.ts │ │ ├── chain.ts │ │ ├── date.test.ts │ │ ├── date.ts │ │ ├── exchange.ts │ │ ├── financial.test.ts │ │ ├── financial.ts │ │ ├── formatters.test.ts │ │ ├── formatters.ts │ │ ├── geo.test.ts │ │ ├── geo.ts │ │ ├── google-meta.ts │ │ ├── html.ts │ │ ├── lending-reserve.test.ts │ │ ├── lending-reserve.ts │ │ ├── object.test.ts │ │ ├── object.ts │ │ ├── option-group.svelte.ts │ │ ├── progressbar.ts │ │ ├── public-api.ts │ │ ├── retry-counter.ts │ │ ├── slugify.ts │ │ ├── style.ts │ │ ├── tokentax.ts │ │ └── url-params.ts │ ├── momentum │ │ └── MomentumTable.svelte │ ├── newsletter │ │ ├── OptInBanner.svelte │ │ ├── SubscribeForm.svelte │ │ └── client.ts │ ├── schemas │ │ ├── announcement.ts │ │ ├── blog.ts │ │ └── utility.ts │ ├── search │ │ ├── __mocks__ │ │ │ └── trading-entities.ts │ │ ├── components │ │ │ ├── Search.svelte │ │ │ ├── Search.test.ts │ │ │ ├── SearchHit.svelte │ │ │ └── SearchHitDescription.svelte │ │ ├── trading-entities.ts │ │ └── typesense-client.ts │ ├── swrCache.ts │ ├── trade-executor │ │ ├── assets │ │ │ └── load-error.jpg │ │ ├── client │ │ │ ├── chart.ts │ │ │ ├── state.ts │ │ │ └── strategy-info.ts │ │ ├── components │ │ │ ├── ChainFilter.svelte │ │ │ ├── KeyMetric.svelte │ │ │ ├── KeyMetricDescription.svelte │ │ │ ├── StrategyError.svelte │ │ │ ├── StrategyIcon.svelte │ │ │ └── TradingDescription.svelte │ │ ├── helpers │ │ │ ├── benchmark.svelte.ts │ │ │ ├── chart.ts │ │ │ ├── date.ts │ │ │ ├── formatters.ts │ │ │ ├── metrics.ts │ │ │ └── strategy-metric-help-texts.ts │ │ ├── models │ │ │ ├── position-info.test.ts │ │ │ ├── position-info.ts │ │ │ ├── position-tooltips.ts │ │ │ ├── strategy-info.ts │ │ │ ├── trade-info.test.ts │ │ │ ├── trade-info.ts │ │ │ └── trading-pair-info.ts │ │ ├── schemas │ │ │ ├── balance-update.ts │ │ │ ├── blockchain-transaction.ts │ │ │ ├── configuration.ts │ │ │ ├── identifier.ts │ │ │ ├── interest.ts │ │ │ ├── key-metric.ts │ │ │ ├── loan.ts │ │ │ ├── portfolio.ts │ │ │ ├── position.ts │ │ │ ├── reserve.ts │ │ │ ├── run-state.ts │ │ │ ├── state.ts │ │ │ ├── statistics-table.ts │ │ │ ├── statistics.ts │ │ │ ├── summary.ts │ │ │ ├── tos-contract-info.ts │ │ │ ├── trade.ts │ │ │ ├── utility-type-fixtures.ts │ │ │ ├── utility-types.ts │ │ │ └── valuation.ts │ │ └── vaults │ │ │ ├── base.ts │ │ │ ├── enzyme │ │ │ ├── abi │ │ │ │ ├── ComptrollerLib.json.ts │ │ │ │ ├── FundValueCalculator.json.ts │ │ │ │ ├── TermedVaultUSDCPaymentForwarder.json.ts │ │ │ │ ├── VaultLib.json.ts │ │ │ │ └── VaultUSDCPaymentForwarder.json.ts │ │ │ └── index.ts │ │ │ ├── hot_wallet │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── lagoon │ │ │ ├── abi │ │ │ │ └── Vault.json.ts │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ └── velvet │ │ │ └── index.ts │ ├── wallet │ │ ├── ConnectWallet.svelte │ │ ├── DepositBalance.svelte │ │ ├── DepositWarning.svelte │ │ ├── MyDeposits.svelte │ │ ├── PendingExchangeInfo.svelte │ │ ├── ShareBalances.svelte │ │ ├── TokenBalance.svelte │ │ ├── WalletAddress.svelte │ │ ├── WalletBalance.svelte │ │ ├── WalletInfo.svelte │ │ ├── WalletInfoItem.svelte │ │ ├── WalletSummary.svelte │ │ ├── WalletWidget.svelte │ │ ├── client.ts │ │ └── helpers.ts │ └── wizard │ │ ├── Wizard.svelte │ │ ├── WizardActions.svelte │ │ ├── WizardHeader.svelte │ │ ├── WizardNavItem.svelte │ │ └── state.svelte.ts ├── params │ ├── integer.ts │ ├── logoType.ts │ ├── momentum.ts │ ├── positionStatus.ts │ └── slug.ts └── routes │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ ├── +page.ts │ ├── AnnouncementBanner.svelte │ ├── ErrorPageInfo.svelte │ ├── FeaturedStrategies.svelte │ ├── HomeHeroBanner.svelte │ ├── ImpressiveNumbers.svelte │ ├── MaintenanceNotice.svelte │ ├── StrategyDifferentiator.svelte │ ├── about │ ├── +page.svelte │ ├── Audience.svelte │ ├── Feature.svelte │ ├── IntroHero.svelte │ ├── Partners.svelte │ └── Platform.svelte │ ├── blog │ ├── +page.svelte │ ├── +page.ts │ ├── README.md │ ├── SocialLinks.svelte │ ├── SocialMetaTags.svelte │ ├── [slug=slug] │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── BlogPostContent.svelte │ │ └── TableOfContents.svelte │ ├── image │ │ └── [...file] │ │ │ └── +server.ts │ ├── posts │ │ └── +server.ts │ ├── rss.xml │ │ └── +server.ts │ └── sitemap.xml │ │ └── +server.ts │ ├── community │ ├── +page.svelte │ └── +page.ts │ ├── dao │ ├── +page.svelte │ └── +page.ts │ ├── diagnostics │ ├── +page.server.ts │ └── +page.svelte │ ├── glossary │ ├── +layout.server.ts │ ├── +page.svelte │ ├── [slug=slug] │ │ ├── +page.svelte │ │ └── +page.ts │ ├── glossary.ts │ └── sitemap.xml │ │ └── +server.ts │ ├── logos │ └── [type=logoType] │ │ └── [slug] │ │ └── +server.ts │ ├── newsletter │ ├── +page.server.ts │ └── +page.svelte │ ├── qr │ ├── +page.svelte │ └── +page.ts │ ├── search │ ├── +page.svelte │ ├── FacetFilter.svelte │ ├── Filter.svelte │ ├── FilterPanel.svelte │ ├── NumericFilter.svelte │ ├── RangeFilter.svelte │ ├── RangeFilter.test.ts │ ├── SearchHitAdvanced.svelte │ ├── SearchHitMetrics.svelte │ ├── SearchPanel.svelte │ ├── SearchPanel.test.ts │ ├── SortSelect.svelte │ └── SortSelect.test.ts │ ├── sentry-test │ ├── +page.server.ts │ └── +page.svelte │ ├── sitemap-static.xml │ └── +server.ts │ ├── sitemap.xml │ └── +server.ts │ ├── slow-load │ ├── +page.svelte │ └── +page.ts │ ├── strategies │ ├── +layout.server.ts │ ├── +page.server.ts │ ├── +page.svelte │ ├── ChartThumbnail.svelte │ ├── StrategyDataSummary.svelte │ ├── StrategyTile.svelte │ ├── StrategyTvlChart.svelte │ ├── [strategy].ipynb │ │ └── +server.ts │ ├── [strategy] │ │ ├── (wizard) │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── connect-wallet │ │ │ │ ├── +layout.ts │ │ │ │ ├── balance │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── +page.ts │ │ │ │ ├── connect │ │ │ │ │ └── +page.svelte │ │ │ │ ├── introduction │ │ │ │ │ └── +page.svelte │ │ │ │ └── success │ │ │ │ │ └── +page.svelte │ │ │ ├── deposit │ │ │ │ ├── +layout.ts │ │ │ │ ├── balance │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── +page.ts │ │ │ │ ├── connect │ │ │ │ │ └── +page.svelte │ │ │ │ ├── introduction │ │ │ │ │ └── +page.svelte │ │ │ │ ├── payment │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── PaymentError.svelte │ │ │ │ ├── success │ │ │ │ │ └── +page.svelte │ │ │ │ └── tos │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── +page.ts │ │ │ ├── error │ │ │ │ └── +page.ts │ │ │ └── redeem │ │ │ │ ├── +layout.ts │ │ │ │ ├── connect │ │ │ │ └── +page.svelte │ │ │ │ ├── deposit-status │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ │ ├── introduction │ │ │ │ └── +page.svelte │ │ │ │ ├── shares-redemption │ │ │ │ ├── +page.svelte │ │ │ │ └── RedemptionError.svelte │ │ │ │ └── success │ │ │ │ └── +page.svelte │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ ├── +server.ts │ │ ├── MetricsBox.svelte │ │ ├── StrategyNav.svelte │ │ ├── StrategyPerformanceChart.svelte │ │ ├── SummaryMetrics.svelte │ │ ├── [status=positionStatus]-positions │ │ │ ├── +layout.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── +server.ts │ │ │ ├── PositionFlag.svelte │ │ │ ├── PositionTable.svelte │ │ │ ├── RemarksCell.svelte │ │ │ ├── ReservesRow.svelte │ │ │ └── [position=integer] │ │ │ │ ├── +layout.ts │ │ │ │ ├── +page.svelte │ │ │ │ ├── OtherMetrics.svelte │ │ │ │ ├── PositionProfitability.svelte │ │ │ │ ├── PositionSummary.svelte │ │ │ │ ├── TradeTable.svelte │ │ │ │ ├── snapshot │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ │ ├── trade-[trade=integer].json │ │ │ │ └── +server.ts │ │ │ │ └── trade-[trade=integer] │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ ├── BlockchainExplorerLink.svelte │ │ │ │ ├── TransactionStatus.svelte │ │ │ │ └── TransactionTable.svelte │ │ ├── backtest │ │ │ └── +page.svelte │ │ ├── description │ │ │ └── +page.svelte │ │ ├── fees │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ ├── netflow │ │ │ └── +page.svelte │ │ ├── performance │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ └── LongShortTable.svelte │ │ ├── period-performance │ │ │ └── +server.ts │ │ ├── snapshot │ │ │ ├── +page.ts │ │ │ └── +page@.svelte │ │ ├── source │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ ├── tech-details │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.ts │ │ │ ├── decision-making │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ │ ├── logs │ │ │ │ ├── +page.svelte │ │ │ │ ├── +page.ts │ │ │ │ ├── LogEntriesList.svelte │ │ │ │ └── LogEntry.svelte │ │ │ └── status │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ └── vault │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── sitemap.xml │ │ └── +server.ts │ └── tvl │ │ └── +server.ts │ ├── tos │ └── [fileName].txt │ │ └── +server.ts │ └── trading-view │ ├── +page.svelte │ ├── +page.ts │ ├── [chain=slug] │ ├── +layout.ts │ ├── +page.svelte │ ├── +page.ts │ ├── BlockInfoTile.svelte │ ├── ChainHeader.svelte │ ├── SummaryDataTile.svelte │ ├── TopEntities.svelte │ ├── TopExchanges.svelte │ ├── TopPairs.svelte │ ├── TopReserves.svelte │ ├── TopTokens.svelte │ ├── TradingEntitiesTable.svelte │ ├── [exchange] │ │ ├── +page.svelte │ │ ├── +page.ts │ │ ├── InfoSummary.svelte │ │ ├── InfoTable.svelte │ │ ├── [pair] │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── InfoSummary.svelte │ │ │ ├── InfoTable.svelte │ │ │ ├── TimePeriodSummaryColumn.svelte │ │ │ ├── TimePeriodSummaryTable.svelte │ │ │ └── api-and-historical-data │ │ │ │ └── +page.ts │ │ └── export-data │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── exchanges │ │ ├── +page.svelte │ │ └── +page.ts │ ├── lending │ │ ├── +page.svelte │ │ ├── +page.ts │ │ └── [protocol] │ │ │ ├── +page.ts │ │ │ └── [reserve] │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── InfoSummary.svelte │ │ │ └── InfoTable.svelte │ ├── tokens │ │ ├── +page.svelte │ │ ├── +page.ts │ │ └── [token] │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ ├── InfoSummary.svelte │ │ │ └── InfoTable.svelte │ └── trading-pairs │ │ ├── +page.svelte │ │ └── +page.ts │ ├── api │ └── +page.svelte │ ├── backtesting │ ├── +page.svelte │ └── +page.ts │ ├── blockchains │ ├── +page.svelte │ ├── +page.ts │ └── sitemap.xml │ │ └── +server.ts │ ├── exchanges │ ├── +page.svelte │ └── +page.ts │ ├── lending-reserves │ ├── +page.svelte │ ├── +page.ts │ └── sitemap.xml │ │ └── +server.ts │ ├── top-list │ ├── +page.svelte │ └── [direction=momentum] │ │ ├── +page.svelte │ │ └── +page.ts │ └── trading-pairs │ ├── +page.svelte │ └── +page.ts ├── static ├── 32747dc0fe894e0a9c52d0f4ef89c584.txt ├── 8080d9b849c848ff8c7a214ef77d0bd1.txt ├── avatars │ ├── arbitrum-btc-breakout.webp │ ├── arbitrum-btc-eth-stoch-rsi.webp │ ├── arbitrum-one-1.jpg │ ├── avalanche-1.jpg │ ├── base-ath.webp │ ├── base-memecoin-index.webp │ ├── base-memex.webp │ ├── base-sentimeme.webp │ ├── bsc-1.jpg │ ├── enzyme-arbitrum-eth-btc-rsi.webp │ ├── enzyme-ethereum-btc-eth-stoch-rsi.webp │ ├── enzyme-polygon-eth-breakout.webp │ ├── enzyme-polygon-eth-btc-rsi.webp │ ├── enzyme-polygon-eth-btc-usdc.webp │ ├── enzyme-polygon-eth-rolling-ratio.webp │ ├── enzyme-polygon-eth-usdc-sls.webp │ ├── enzyme-polygon-eth-usdc.webp │ ├── enzyme-polygon-matic-eth-usdc.webp │ ├── enzyme-polygon-matic-usdc.webp │ ├── enzyme-polygon-multipair.webp │ ├── ethereum-1.jpg │ ├── ethereum-memecoin-swing.webp │ ├── ethereum-memecoin-vol-basket.webp │ ├── polygon-1.jpg │ ├── polygon-eth-spot-short.webp │ └── polygon-multipair-momentum.webp ├── b8e3e4232bd34aea86a92cbfce3dc767.txt ├── brand-mark-100x100.png ├── favicon.ico ├── fonts │ ├── SourceCodePro │ │ ├── latin-italic.woff │ │ └── latin-normal.woff │ ├── SourceSerifPro │ │ ├── latin-400-italic.woff2 │ │ ├── latin-400-normal.woff2 │ │ ├── latin-600-italic.woff2 │ │ ├── latin-600-normal.woff2 │ │ ├── latin-700-italic.woff2 │ │ └── latin-700-normal.woff2 │ └── fonts5.css ├── logo-two-lines-new-no-text.svg └── robots.txt ├── svelte.config.js ├── tests ├── e2e │ ├── blog.test.ts │ ├── glossary.test.ts │ ├── index.test.ts │ ├── playwright.config.ts │ ├── sitemap-index.test.ts │ └── trading-view │ │ ├── chain-details.ts │ │ ├── chain-index.test.ts │ │ ├── exchange-details.test.ts │ │ ├── index.test.ts │ │ ├── pair-details.test.ts │ │ └── pair-index.test.ts ├── fixtures │ ├── chain-details.json │ ├── chains.json │ ├── datasets.json │ ├── impressive-numbers.json │ ├── pair-details.json │ ├── pairs.json │ ├── strategies │ │ ├── enzyme-polygon-matic-usdc │ │ │ └── metadata.json │ │ └── enzyme-polygon-multipair │ │ │ └── metadata.json │ ├── token │ │ └── details.json │ ├── top-momentum.json │ └── typesense │ │ └── collections │ │ └── trading-entities │ │ └── documents │ │ └── search.json ├── helpers.ts └── integration │ ├── announcement.test.ts │ ├── diagnostics.test.ts │ ├── index.test.ts │ ├── index.test.ts-snapshots │ ├── home-page-hero-banner-looks-correct-1-darwin.png │ └── home-page-hero-banner-looks-correct-1-linux.png │ ├── playwright.config.ts │ ├── search.test.ts │ ├── sitemap-index.test.ts │ ├── strategies │ └── index.test.ts │ └── trading-view │ ├── pair-details.test.ts │ └── token-details.test.ts ├── tsconfig.json ├── vite.config.ts └── vitest.setup.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # common 2 | **/.git/ 3 | **/node_modules/ 4 | **/.svelte-kit/ 5 | 6 | # frontend (root) 7 | build/ 8 | tests/ 9 | docs/ 10 | Dockerfile 11 | docker-compose.yml 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | vite.config.js.timestamp-* 7 | vite.config.ts.timestamp-* 8 | .idea 9 | .vscode 10 | .zed 11 | 12 | # Playwright testing 13 | /test-results 14 | /playwright-report 15 | 16 | # Not redistribuable 17 | static/fonts/NeueHaasGroteskDisplay 18 | static/fonts/NeueHaasGroteskText 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/fonts"] 2 | path = deps/fonts 3 | url = git@github.com:tradingstrategy-ai/fonts.git 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | @tradingstrategy-ai:registry=https://npm.pkg.github.com 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | /static 7 | /deps 8 | .idea 9 | .vscode 10 | 11 | # Ignore files for PNPM, NPM and YARN 12 | package.json 13 | pnpm-lock.yaml 14 | package-lock.json 15 | yarn.lock 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 120, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | }, 14 | { 15 | "files": "*.json.ts", 16 | "options": { 17 | "singleQuote": false, 18 | "quoteProps": "preserve" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /docs/font-breakdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/docs/font-breakdown.png -------------------------------------------------------------------------------- /docs/maintenance-mode.md: -------------------------------------------------------------------------------- 1 | # Chain maintenance mode 2 | 3 | A blockchain can be set for a maintenance mode in the case 4 | its data needs to be migrated, reindexed or such. 5 | 6 | ## Setting the maintenance mode 7 | 8 | Maintenance mode is controlled in [./src/lib/config.ts](../src/lib/config.ts) by `TS_PUBLIC_CHAINS_UNDER_MAINTENANCE` environment variable. 9 | 10 | It is a JSON map of slug:name pairs of chains under the maintenance. 11 | 12 | To set it: 13 | 14 | ```shell 15 | export TS_PUBLIC_CHAINS_UNDER_MAINTENANCE='{ "binance": "BNB Chain" }' 16 | ``` 17 | 18 | - Add this to `~/secrets.env` 19 | - Refresh the environment `source ~/secrets.env` 20 | - Then [restart the frontend docker](./docker.md) 21 | 22 | ```shell 23 | source ~/secrets.env 24 | # Get from https://github.com/tradingstrategy-ai/frontend/pkgs/container/frontend 25 | export TS_PUBLIC_FRONTEND_VERSION_TAG=v9 26 | docker-compose up -d --force-recreate frontend 27 | ``` 28 | 29 | Check maintanance page comes up 30 | 31 | - https://tradingstrategy.ai/trading-view/binance/pancakeswap-v2/hash-usdt-2 32 | -------------------------------------------------------------------------------- /docs/old-production.md: -------------------------------------------------------------------------------- 1 | # Old production (deprecated) 2 | 3 | Deprecated. See [Docker info](./docker.md) instead. 4 | 5 | This will run server-side generated (SSR) pages using node.js server. 6 | 7 | The server default port is 3000. 8 | 9 | ```shell 10 | tmux -CC -L web attach 11 | export PRODUCTION=true 12 | source ~/secrets.env 13 | 14 | # Re-institate SSH agent connection if needed 15 | eval `ssh-agent` 16 | 17 | cd ~/frontend 18 | scripts/update-and-restart-production.sh 19 | ``` 20 | 21 | [Port troubleshooting](https://www.tecmint.com/find-out-which-process-listening-on-a-particular-port/) 22 | 23 | ```shell 24 | netstat -ltnp | grep -w ':80' 25 | ``` 26 | 27 | # Testing production build locally 28 | 29 | ```shell 30 | export PRODUCTION=true 31 | export VITE_PUBLIC_BACKEND_URL=https://tradingstrategy.ai/api 32 | export FRONTEND_PORT=3000 33 | rm -rf build && node_modules/.bin/svelte-kit build && node build 34 | ``` 35 | 36 | This launched production build at http://localhost:3000/ 37 | 38 | Then you can check e.g. HTTP headers: 39 | 40 | ```shell 41 | wget -S http://localhost:3000 42 | ``` 43 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs['flat/recommended'], 15 | prettier, 16 | ...svelte.configs['flat/prettier'], 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node 22 | } 23 | } 24 | }, 25 | { 26 | files: ['**/*.svelte'], 27 | 28 | languageOptions: { 29 | parserOptions: { 30 | parser: ts.parser 31 | } 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | import presetEnv from 'postcss-preset-env'; 2 | import globalData from '@csstools/postcss-global-data'; 3 | import darkThemeClass from 'postcss-dark-theme-class'; 4 | 5 | export default { 6 | plugins: [ 7 | globalData({ 8 | files: ['./src/lib/components/css/breakpoints.css'] 9 | }), 10 | 11 | presetEnv(), 12 | 13 | darkThemeClass({ 14 | darkSelector: '[data-color-mode="dark"]', 15 | lightSelector: '[data-color-mode="light"]' 16 | }) 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /scripts/build-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Build submodule tracked dependencies 4 | # 5 | 6 | set -e 7 | 8 | # Copy fonts if the optional font submodule exists 9 | if [ -d deps/fonts/NeueHaasGroteskDisplay ]; then 10 | cp -r deps/fonts/NeueHaasGroteskDisplay/ static/fonts/NeueHaasGroteskDisplay 11 | cp -r deps/fonts/NeueHaasGroteskText/ static/fonts/NeueHaasGroteskText 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Create a new releease 4 | # 5 | # - increment version number by 1 6 | # - push a new release to Github 7 | # - trigger new image build 8 | # 9 | 10 | set -e 11 | 12 | # Get tags locally 13 | git fetch --all 14 | 15 | # https://gist.github.com/rponte/fdc0724dd984088606b0 16 | latest_tag=`git tag --sort=committerdate | tail -1` 17 | 18 | latest_version=${latest_tag:1} 19 | 20 | latest_commit=`git log --oneline -1` 21 | 22 | build_url="https://github.com/tradingstrategy-ai/frontend/pkgs/container/frontend" 23 | 24 | # https://ryanstutorials.net/bash-scripting-tutorial/bash-arithmetic.php 25 | let "new_version = $latest_version + 1" 26 | 27 | new_tag="v$new_version" 28 | 29 | echo "Latest commit is $latest_commit" 30 | 31 | # https://stackoverflow.com/a/3232082/315168 32 | read -r -p "New tag is $new_tag - make a release? [y/N] " response 33 | case "$response" in 34 | [yY][eE][sS]|[yY]) 35 | git tag $new_tag 36 | git push origin $new_tag 37 | echo "Pushed $new_tag - please find the build to complete at $build_url" 38 | ;; 39 | *) 40 | echo "No release :(" 41 | exit 42 | ;; 43 | esac 44 | 45 | -------------------------------------------------------------------------------- /scripts/update-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Update all git submodule dependencies 4 | # 5 | 6 | set -e 7 | set -x 8 | (cd deps/fonts && git pull origin main) 9 | -------------------------------------------------------------------------------- /scripts/update-production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Update frontend docker and restart 4 | # 5 | # Usage: ./update-production 6 | # 7 | 8 | set -e 9 | 10 | if [ $# -ne 1 ]; then 11 | echo "Please provide version tag as argument" 12 | echo "Usage: $0 " 13 | echo "See https://github.com/tradingstrategy-ai/frontend/pkgs/container/frontend" 14 | exit 1 15 | fi 16 | 17 | # Set version tag 18 | export TS_PUBLIC_FRONTEND_VERSION_TAG="$1" 19 | 20 | # Set project name based on directory name, or override with env var 21 | export COMPOSE_PROJECT_NAME=${TS_PROJECT_NAME:-$(basename $(pwd))} 22 | 23 | source ~/secrets.env 24 | 25 | # Source local config if exists (overrides shared secrets) 26 | if [ -f ./config.env ]; then 27 | source ./config.env 28 | fi 29 | 30 | docker compose up -d 31 | 32 | echo "All ok" 33 | sleep 5 34 | docker ps --filter name="${COMPOSE_PROJECT_NAME}-frontend-1" 35 | docker compose logs --tail=20 36 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | // Ambient type declarations (separate from app.d.ts) 2 | // see: https://kit.svelte.dev/docs/types#app 3 | declare global { 4 | // some numbers (e.g. from API) are given as strings 5 | type Numberlike = number | string; 6 | 7 | type Maybe = T | null | undefined; 8 | type MaybeNumber = Maybe; 9 | type MaybeNumberlike = Maybe; 10 | type MaybeString = Maybe; 11 | type MaybeDate = Maybe; 12 | type MaybePromise = T | Promise; 13 | 14 | type Formatter = (value: T, ...args: any[]) => string; 15 | 16 | type Fetch = (input: RequestInfo, init?: RequestInit) => Promise; 17 | 18 | type Address = `0x${string}`; 19 | 20 | // utility to expand intersection types for better inline type feedback 21 | type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; 22 | } 23 | 24 | export {}; 25 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'unplugin-icons/types/svelte'; 2 | import type { CountryCode } from '$lib/helpers/geo'; 3 | import type { TimeBucket } from '$lib/schemas/utility'; 4 | 5 | // For information about these interfaces, see: 6 | // https://kit.svelte.dev/docs/types#app 7 | declare global { 8 | namespace App { 9 | interface Error { 10 | message: string; 11 | chainName?: string; 12 | stack?: string[]; 13 | eventId?: string; 14 | } 15 | 16 | interface Locals { 17 | admin?: boolean; 18 | ipCountry?: CountryCode; 19 | announcementDismissedAt?: Date; 20 | } 21 | 22 | interface PageState { 23 | timeBucket?: TimeBucket; 24 | } 25 | 26 | // interface PageData {} 27 | // interface Platform {} 28 | } 29 | } 30 | 31 | export {}; 32 | -------------------------------------------------------------------------------- /src/hooks.client.ts: -------------------------------------------------------------------------------- 1 | import type { ClientInit } from '@sveltejs/kit'; 2 | import * as Sentry from '@sentry/sveltekit'; 3 | import { sentryDsn, siteMode, version } from '$lib/config'; 4 | 5 | Sentry.init({ 6 | dsn: sentryDsn, 7 | environment: siteMode, 8 | release: `frontend@${version}`, 9 | tracesSampleRate: 0.1 10 | }); 11 | 12 | export const handleError = Sentry.handleErrorWithSentry(); 13 | 14 | // adding empty init to silence build warning 15 | export const init: ClientInit = async () => {}; 16 | -------------------------------------------------------------------------------- /src/lib/__mocks__/config.ts: -------------------------------------------------------------------------------- 1 | export const backendUrl = 'http://example.com'; 2 | 3 | export const geoBlock = { 4 | 'strategies:view': ['CU', 'IR', 'KP', 'RU', 'SY'] 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/actions/image.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Svelte action to remove an image when it fails to load. 3 | * 4 | * @usage 5 | * image 6 | */ 7 | export function removeOnError(node: HTMLImageElement) { 8 | const remove = () => node.remove(); 9 | node.addEventListener('error', remove); 10 | return { 11 | destroy: () => node.removeEventListener('error', remove) 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/actions/scroll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Svelte action to prevent body from scrolling, e.g., when a modal is open. 3 | * 4 | * see: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ 5 | * 6 | * @param disableScroll - whether to prevent scrolling; typically a prop or stateful variable 7 | * 8 | * @usage 9 | * 10 | */ 11 | export function disableScroll({ style }: HTMLBodyElement, disableScroll: boolean) { 12 | function update(disable: boolean) { 13 | if (disable) { 14 | style.top = `-${window.scrollY}px`; 15 | style.position = 'fixed'; 16 | style.width = '100%'; 17 | } else { 18 | const scrollY = style.top; 19 | style.top = ''; 20 | style.position = ''; 21 | style.width = ''; 22 | window.scrollTo(0, parseInt(scrollY || '0') * -1); 23 | } 24 | } 25 | 26 | update(disableScroll); 27 | 28 | return { 29 | update, 30 | destroy: () => update(false) 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/actions/viewport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mobile Safari does not correctly reflect viewport height with % or vh units 3 | * when address bar or virtual keyboard are open. It does, however, support 4 | * the VisualViewport JS API for getting the (real) visual viewport size. 5 | * 6 | * See: https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API 7 | */ 8 | export function setViewportHeight(node: HTMLElement) { 9 | const docRoot = document.documentElement; 10 | 11 | function setValue({ style }: HTMLElement, value: string) { 12 | style.setProperty('--viewport-height', value); 13 | } 14 | 15 | function handleResize() { 16 | setValue(node, `${visualViewport?.height}px`); 17 | } 18 | 19 | function destroy() { 20 | visualViewport?.removeEventListener('resize', handleResize); 21 | setValue(node, ''); 22 | setValue(docRoot, ''); 23 | } 24 | 25 | setValue(docRoot, '100vh'); 26 | visualViewport && handleResize(); 27 | visualViewport?.addEventListener('resize', handleResize); 28 | 29 | return { destroy }; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/assets/brand-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/assets/ethlisbon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/src/lib/assets/ethlisbon.webp -------------------------------------------------------------------------------- /src/lib/assets/icons/24h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-left-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-left-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-right-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-right-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/backtesting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/book.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/check-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/console.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/assets/icons/copy-to-clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/assets/icons/exchange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/medium.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/newspaper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/pair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/performance-increase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/rss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/assets/icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/telegram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/trend-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/assets/icons/trend-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/assets/icons/unlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/assets/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/logos/blockchains/base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/assets/logos/blockchains/ethereum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/assets/logos/blockchains/unichain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/logos/tokens/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/assets/logos/tokens/usdt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/assets/logos/tokens/usd₮0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/logos/tokens/velvet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/lib/assets/logos/tokens/weth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/assets/logos/wallets/walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/assets/misc/mbp-15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/src/lib/assets/misc/mbp-15.webp -------------------------------------------------------------------------------- /src/lib/assets/tos/README.me: -------------------------------------------------------------------------------- 1 | # Terms of service management 2 | 3 | See [terms-of-service smart contract repo](https://github.com/tradingstrategy-ai/terms-of-service/) for the update instructions. 4 | 5 | -------------------------------------------------------------------------------- /src/lib/chain/tokenstandard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Token standard naming helper 3 | */ 4 | 5 | // chain_slug -> standard name 6 | export const TOKEN_STANDARDS = { 7 | ethereum: 'ERC-20', 8 | binance: 'BEP-20', 9 | polygon: 'ERC-20', 10 | avalanche: 'ERC-20' 11 | }; 12 | 13 | export function getTokenStandardName(chainSlug: string) { 14 | return TOKEN_STANDARDS[chainSlug] || 'ERC-20'; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/charts/SeriesContent.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {@render children()} 13 |
14 | 15 | 23 | -------------------------------------------------------------------------------- /src/lib/charts/time-span.ts: -------------------------------------------------------------------------------- 1 | import type { TimeSpan } from './types'; 2 | import { timeBucketToInterval } from './helpers'; 3 | 4 | const timeSpans = { 5 | '1W': { 6 | performanceLabel: 'past week', 7 | spanDays: 7, 8 | timeBucket: '1h' 9 | }, 10 | '1M': { 11 | performanceLabel: 'past month', 12 | spanDays: 30, 13 | timeBucket: '4h' 14 | }, 15 | '3M': { 16 | performanceLabel: 'past 90 days', 17 | spanDays: 90, 18 | timeBucket: '1d' 19 | }, 20 | Max: { 21 | performanceLabel: 'lifetime', 22 | timeBucket: '1d' 23 | } 24 | } as const; 25 | 26 | type TimeSpanKey = keyof typeof timeSpans; 27 | 28 | export const TimeSpans = { 29 | get keys() { 30 | return Object.keys(timeSpans) as TimeSpanKey[]; 31 | }, 32 | 33 | get(key: TimeSpanKey): TimeSpan { 34 | const timeSpan = timeSpans[key]; 35 | return { 36 | ...timeSpan, 37 | interval: timeBucketToInterval(timeSpan.timeBucket) 38 | }; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/components/Alert.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 46 | -------------------------------------------------------------------------------- /src/lib/components/BlogRoll.svelte: -------------------------------------------------------------------------------- 1 | 11 | 21 | 22 |
23 | {#each posts as post (post.id)} 24 | 25 | {:else} 26 |

No blog posts found (check if Ghost is properly configured)

27 | {/each} 28 |
29 | 30 | 37 | -------------------------------------------------------------------------------- /src/lib/components/ContentCardsTemplate.svelte: -------------------------------------------------------------------------------- 1 | 18 | 24 | 25 | 26 | {pageTitle} 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 | -------------------------------------------------------------------------------- /src/lib/components/EntitySymbol.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if logoUrl} 11 | 12 | {/if} 13 | {#if label || $$slots.default} 14 |
15 | {label} 16 |
17 | {/if} 18 |
19 | 20 | 32 | -------------------------------------------------------------------------------- /src/lib/components/Grid.svelte: -------------------------------------------------------------------------------- 1 | 13 | 19 | 20 |
21 | 22 |
23 | 24 | 51 | -------------------------------------------------------------------------------- /src/lib/components/HashAddress.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | {address.slice(0, sliceAt)}{address.slice(sliceAt)} 10 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /src/lib/components/HeroVideo.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | 10 | 21 | -------------------------------------------------------------------------------- /src/lib/components/Logo.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {@html svg} 7 |
8 | 9 | 23 | -------------------------------------------------------------------------------- /src/lib/components/SourceCode.svelte: -------------------------------------------------------------------------------- 1 | 13 | 21 | 22 |
23 | 24 |
25 | 26 | 38 | -------------------------------------------------------------------------------- /src/lib/components/Spinner.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 | 18 | 28 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /src/lib/components/Tooltip.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte'; 2 | import Tooltip from './Tooltip.svelte'; 3 | 4 | describe('Tooltip component', () => { 5 | // This test is intentionally checking for very specific markup: 6 | // The tooltip popup container MUST be a button element; otherwise 7 | // you end up with illegal element nesting which leads to page jank 8 | // (the popup content is not hidden on initial page render). 9 | // see: https://stackoverflow.com/questions/40531029 updates 3 & 4 10 | test('should use button tag for popup content', () => { 11 | mount(Tooltip, { target: document.body }); 12 | const popup = document.body.querySelector('.popup'); 13 | expect(popup?.tagName).toBe('BUTTON'); 14 | // button should be disabled to remove from tab index and prevent click events 15 | expect(popup).toBeDisabled(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/lib/components/TradingDataInfo.svelte: -------------------------------------------------------------------------------- 1 | 14 |
15 | 16 |
17 | 18 | 32 | -------------------------------------------------------------------------------- /src/lib/components/TradingDataInfoRow.svelte: -------------------------------------------------------------------------------- 1 | 20 | 24 | 25 |
26 | {label} 27 |
28 |
29 | {value} 30 |
31 | 32 | 46 | -------------------------------------------------------------------------------- /src/lib/components/css/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes pulse-opacity { 2 | 0% { 3 | opacity: 1; 4 | } 5 | 6 | 32% { 7 | opacity: 0.3; 8 | } 9 | 10 | 100% { 11 | opacity: 1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/components/css/body-link.css: -------------------------------------------------------------------------------- 1 | a.body-link { 2 | border-bottom: 1px solid currentColor; 3 | font-weight: 500; 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/components/css/bullish-bearish.css: -------------------------------------------------------------------------------- 1 | :is(.bullish, [data-direction='bullish']) { 2 | color: var(--c-bullish); 3 | } 4 | 5 | :is(.bearish, [data-direction='bearish']) { 6 | color: var(--c-bearish); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/components/css/index.css: -------------------------------------------------------------------------------- 1 | @import './reset.css'; 2 | @import './animations.css'; 3 | @import './body-link.css'; 4 | @import './bullish-bearish.css'; 5 | @import './color.css'; 6 | @import './datatable.css'; 7 | @import './input.css'; 8 | @import './layout.css'; 9 | @import './metrics-table.css'; 10 | @import './progress.css'; 11 | @import './radius.css'; 12 | @import './radius-new.css'; 13 | @import './shadows.css'; 14 | @import './skeleton.css'; 15 | @import './space.css'; 16 | @import './sr-only.css'; 17 | @import './terminal.css'; 18 | @import './tile.css'; 19 | @import './time.css'; 20 | @import './transitions.css'; 21 | @import './truncate.css'; 22 | @import './typography.css'; 23 | @import './typography-new.css'; 24 | -------------------------------------------------------------------------------- /src/lib/components/css/input.css: -------------------------------------------------------------------------------- 1 | input[type='number'] { 2 | -moz-appearance: textfield; 3 | 4 | &::-webkit-inner-spin-button, 5 | &::-webkit-outer-spin-button { 6 | -webkit-appearance: none; 7 | margin: 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/components/css/layout.css: -------------------------------------------------------------------------------- 1 | /* Global settings for design-system layout */ 2 | :root { 3 | --container-max-width: 86rem; 4 | --container-margin: 2rem; 5 | 6 | @media (--viewport-lg-down) { 7 | --container-margin: 1.75rem; 8 | } 9 | @media (--viewport-md-down) { 10 | --container-margin: 1.5rem; 11 | } 12 | @media (--viewport-sm-down) { 13 | --container-margin: 1rem; 14 | } 15 | } 16 | 17 | /* design-system layout utility classes */ 18 | .ds-container { 19 | --container-width: min(calc(100% - (var(--container-margin) * 2)), var(--container-max-width)); 20 | display: grid; 21 | padding-inline: calc((100% - var(--container-width)) / 2); 22 | } 23 | 24 | .ds-2-col { 25 | --column-gap: 2.5rem; 26 | --column-min: calc((1024px - 2 * var(--container-margin) - var(--column-gap)) / 2); 27 | --column-max: calc((var(--container-width) - var(--column-gap)) / 2); 28 | --template-min: min(var(--container-width), max(var(--column-min), var(--column-max))); 29 | display: grid; 30 | grid-template-columns: repeat(auto-fit, minmax(var(--template-min), 1fr)); 31 | gap: 1.5rem var(--column-gap); 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/components/css/metrics-table.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Metrics table styling 3 | * 4 | * Usage: 5 | * 6 | * ... 7 | *
8 | */ 9 | table.metrics-table { 10 | width: 100%; 11 | border-collapse: collapse; 12 | font: var(--f-ui-md-roman); 13 | letter-spacing: var(--ls-ui-md); 14 | 15 | @media (--viewport-xs) { 16 | font: var(--f-ui-sm-roman); 17 | letter-spacing: var(--ls-ui-sm); 18 | } 19 | 20 | :is(th, td) { 21 | padding: 0.625rem; 22 | 23 | &:first-child { 24 | text-align: left; 25 | } 26 | 27 | &:not(first-child) { 28 | text-align: right; 29 | } 30 | } 31 | 32 | thead { 33 | color: var(--c-text-extra-light); 34 | 35 | th { 36 | padding-bottom: 1em; 37 | font-size: 0.875em; 38 | } 39 | } 40 | 41 | tbody { 42 | border-block: 2px solid var(--c-text-ultra-light); 43 | 44 | /* zebra-striped rows */ 45 | tr:nth-child(even) { 46 | background: var(--c-box-2); 47 | } 48 | 49 | td:first-child { 50 | font-size: 0.875em; 51 | font-weight: 500; 52 | color: var(--c-text-light); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/components/css/progress.css: -------------------------------------------------------------------------------- 1 | progress { 2 | --background-color: var(--c-box-1); 3 | --progress-color: var(--c-success); 4 | --border-radius: var(--progress-border-radius, var(--radius-xs)); 5 | appearance: none; 6 | background: var(--background-color); 7 | border: none; 8 | border-radius: var(--border-radius); 9 | height: 0.75rem; 10 | overflow: hidden; 11 | 12 | /* WebKit styles - do not merge with Mozilla styles (breaks) */ 13 | &::-webkit-progress-bar { 14 | background: var(--background-color); 15 | border-radius: var(--border-radius); 16 | } 17 | 18 | &::-webkit-progress-value { 19 | background-color: var(--progress-color); 20 | border-radius: var(--border-radius) 0 0 var(--border-radius); 21 | } 22 | 23 | /* Mozilla styles - do not merge with WebKit styles (breaks) */ 24 | &::-moz-progress-bar { 25 | background: var(--progress-color); 26 | border: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/components/css/radius-new.css: -------------------------------------------------------------------------------- 1 | .ds-3 { 2 | --radius-xs: 0.375rem; 3 | --radius-ss: 0.5rem; 4 | --radius-sm: 0.625rem; 5 | --radius-sl: 0.75rem; 6 | --radius-ms: 0.875rem; 7 | --radius-md: 1rem; 8 | --radius-ml: 1.125rem; 9 | --radius-ls: 1.25rem; 10 | --radius-lg: 1.5rem; 11 | --radius-ll: 1.75rem; 12 | --radius-xl: 2rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/components/css/radius.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --radius-xxs: 0.375rem; 3 | --radius-xs: 0.75rem; 4 | 5 | --radius-sm: 1rem; 6 | --radius-md: 1.25rem; 7 | --radius-lg: 1.5rem; 8 | 9 | --radius-xl: 1.75rem; 10 | --radius-xxl: 2rem; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/components/css/shadows.css: -------------------------------------------------------------------------------- 1 | body { 2 | --shadow-1: 0 0 4px 0 color-mix(in srgb, transparent, var(--c-shadow) 20%); 3 | --shadow-2: 0 0 6px 0 color-mix(in srgb, transparent, var(--c-shadow) 10%); 4 | --shadow-3: 0 0 6px 0 color-mix(in srgb, transparent, var(--c-shadow) 5%); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/components/css/skeleton.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | color: transparent !important; 3 | position: relative; 4 | 5 | &::before { 6 | content: ''; 7 | position: absolute; 8 | width: var(--skeleton-width, 100%); 9 | height: var(--skeleton-height, 100%); 10 | top: calc((100% - var(--skeleton-height)) / 2); 11 | border-radius: var(--skeleton-radius, var(--radius-xs)); 12 | background: var(--c-box-3); 13 | animation: pulse-opacity 1s infinite ease-out; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/components/css/space.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Use with these properties: bottom, gap, left, margin, padding, right, top, transform 3 | */ 4 | 5 | :root { 6 | --space-xxxs: 0.125rem; 7 | --space-xxs: 0.25rem; 8 | --space-xs: 0.375rem; 9 | 10 | --space-ss: 0.5rem; 11 | --space-sm: 0.625rem; 12 | --space-sl: 0.75rem; 13 | 14 | --space-ms: 0.875rem; 15 | --space-md: 1rem; 16 | --space-ml: 1.125rem; 17 | 18 | --space-ls: 1.25rem; 19 | --space-lg: 1.5rem; 20 | --space-ll: 1.75rem; 21 | 22 | --space-xl: 2rem; 23 | --space-2xl: 2.25rem; 24 | --space-3xl: 2.5rem; 25 | --space-4xl: 2.75rem; 26 | --space-5xl: 3rem; 27 | --space-6xl: 3.5rem; 28 | --space-7xl: 4rem; 29 | --space-8xl: 5rem; 30 | --space-9xl: 6.25rem; 31 | --space-10xl: 7.5rem; 32 | --space-11xl: 8.75rem; 33 | --space-12xl: 10rem; 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/components/css/sr-only.css: -------------------------------------------------------------------------------- 1 | /* Use .sr-only to hide an element visually without hiding it from screen readers */ 2 | .sr-only { 3 | position: absolute; 4 | width: 1px; 5 | height: 1px; 6 | padding: 0; 7 | margin: -1px; 8 | overflow: hidden; 9 | clip: rect(0, 0, 0, 0); 10 | white-space: nowrap; 11 | border-width: 0; 12 | 13 | /* add .focusable to show again when it’s focused (e.g. by a keyboard-only user) */ 14 | &.focusable:is(:focus, :active) { 15 | position: static; 16 | width: auto; 17 | height: auto; 18 | padding: 0; 19 | margin: 0; 20 | overflow: visible; 21 | clip: auto; 22 | white-space: normal; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/components/css/terminal.css: -------------------------------------------------------------------------------- 1 | body { 2 | --c-terminal-bg: hsl(239, 31%, 6%); 3 | --c-terminal: hsl(var(--hue-1), 4%, 86%); 4 | --c-terminal-light: hsl(var(--hue-1), 3%, 72%); 5 | } 6 | 7 | :where(.terminal-viewport) { 8 | border-radius: var(--radius-md); 9 | padding: var(--space-md) var(--space-lg); 10 | background: var(--c-terminal-bg); 11 | color: var(--c-terminal); 12 | font: var(--f-mono-md-regular); 13 | letter-spacing: var(--f-mono-md-spacing, normal); 14 | 15 | &.sm { 16 | border-radius: var(--radius-sm); 17 | padding: var(--space-ms) var(--space-ls); 18 | font: var(--f-mono-sm-regular); 19 | letter-spacing: var(--f-mono-sm-spacing, normal); 20 | } 21 | 22 | &.xs { 23 | border-radius: var(--radius-sm); 24 | padding: var(--space-sl) var(--space-md); 25 | font: var(--f-mono-xs-regular); 26 | letter-spacing: var(--f-mono-xs-spacing, normal); 27 | } 28 | 29 | code { 30 | font: inherit; 31 | letter-spacing: inherit; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/components/css/tile.css: -------------------------------------------------------------------------------- 1 | /* 2 | Tile CSS utility - used for creating layers of color shades. 3 | Apply the .tile class along with one of the shade-layer classes: .a, .b, .c, .d 4 | */ 5 | .tile { 6 | border-radius: var(--radius-md); 7 | transition: background var(--time-sm) ease-out; 8 | 9 | &.a { 10 | background: var(--c-box-1); 11 | --background-hover: var(--c-box-2); 12 | } 13 | 14 | &.b { 15 | background: var(--c-box-2); 16 | --background-hover: var(--c-box-4); 17 | } 18 | 19 | &.c { 20 | background: var(--c-box-3); 21 | --background-hover: var(--c-box-4); 22 | } 23 | 24 | &.d { 25 | background: var(--c-box-4); 26 | } 27 | 28 | &:hover, 29 | &:hover & { 30 | background: var(--background-hover) !important; 31 | } 32 | 33 | &:is(:hover, :focus) { 34 | .button { 35 | background: var(--c-text) !important; 36 | color: var(--c-text-inverted) !important; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/components/css/time.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --time-xxs: 0.12s; 3 | --time-xs: 0.16s; 4 | --time-sm: 0.2s; 5 | --time-md: 0.24s; 6 | --time-lg: 0.32s; 7 | --time-xl: 0.36s; 8 | --time-xxl: 0.44s; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/components/css/transitions.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --transition-1: all var(--time-xs) ease-out; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/components/css/truncate.css: -------------------------------------------------------------------------------- 1 | /* 2 | Use this class to truncate text inside an HTMLElement 3 | */ 4 | .truncate { 5 | -webkit-box-orient: vertical; 6 | overflow: hidden; 7 | text-overflow: ellipsis; 8 | 9 | &.lines-2 { 10 | display: -webkit-box; 11 | -webkit-line-clamp: 2; 12 | } 13 | 14 | &.lines-3 { 15 | display: -webkit-box; 16 | -webkit-line-clamp: 3; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/components/datatable/PageButton.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if active || disabled} 9 | {label} 10 | {:else} 11 | 12 | {/if} 13 | 14 | 50 | -------------------------------------------------------------------------------- /src/lib/components/datatable/SearchHeaderRow.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /src/lib/components/datatable/TableRow.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {#each cells as cell (cell.id)} 13 | 14 | 15 | 16 | 17 | 18 | {/each} 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/datatable/TableRowTarget.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 28 | 29 | 42 | -------------------------------------------------------------------------------- /src/params/integer.ts: -------------------------------------------------------------------------------- 1 | export function match(param): param is `${number}` { 2 | return /^\d+$/.test(param); 3 | } 4 | -------------------------------------------------------------------------------- /src/params/logoType.ts: -------------------------------------------------------------------------------- 1 | import { logoTypes } from '$lib/helpers/assets'; 2 | 3 | const logoPaths = logoTypes.map((type) => type + 's'); 4 | 5 | export function match(param) { 6 | // @ts-ignore 7 | return logoPaths.includes(param); 8 | } 9 | -------------------------------------------------------------------------------- /src/params/momentum.ts: -------------------------------------------------------------------------------- 1 | export function match(param): param is 'daily-up' | 'daily-down' { 2 | return ['daily-up', 'daily-down'].includes(param); 3 | } 4 | -------------------------------------------------------------------------------- /src/params/positionStatus.ts: -------------------------------------------------------------------------------- 1 | import { type PositionStatus, positionStatus } from 'trade-executor/schemas/position'; 2 | 3 | export function match(param): param is PositionStatus { 4 | return positionStatus.safeParse(param).success; 5 | } 6 | -------------------------------------------------------------------------------- /src/params/slug.ts: -------------------------------------------------------------------------------- 1 | export function match(param) { 2 | return /^[a-z0-9_-]+$/.test(param); 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { getCachedStrategies } from 'trade-executor/client/strategy-info'; 2 | 3 | export async function load({ fetch }) { 4 | const strategies = await getCachedStrategies(fetch); 5 | 6 | return { 7 | strategies: strategies.filter((s) => s.frontpage && s.connected) 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | import { getPosts } from '$lib/blog/client'; 3 | 4 | // handle API fetch errors gracefully (see `catch` below) 5 | function logError(err: Error) { 6 | console.error('Request failed; rendering page without data.'); 7 | console.error(err); 8 | } 9 | 10 | export async function load({ fetch, setHeaders, data }) { 11 | // Cache the landing data for 5 minutes at the Cloudflare so pages are 12 | // served really fast if they get popular, and also for speed test 13 | setHeaders({ 14 | 'cache-control': 'public, max-age=300' // 5 minutes: 5 * 60 = 300 15 | }); 16 | 17 | return { 18 | strategies: data.strategies, 19 | chains: await fetchPublicApi(fetch, 'chains').catch(logError), 20 | impressiveNumbers: await fetchPublicApi(fetch, 'impressive-numbers').catch(logError), 21 | posts: await getPosts(fetch, { limit: 4 }) 22 | .then((r) => r.posts) 23 | .catch(logError) 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/MaintenanceNotice.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if maintenanceNotice} 8 |
9 | 10 | {@html maintenanceNotice} 11 | 14 | 15 |
16 | {/if} 17 | -------------------------------------------------------------------------------- /src/routes/StrategyDifferentiator.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | {title} 14 | 15 | 16 |
17 | {details} 18 |
19 |
20 | 21 | 46 | -------------------------------------------------------------------------------- /src/routes/blog/+page.ts: -------------------------------------------------------------------------------- 1 | import { getPosts, maxAge } from '$lib/blog/client'; 2 | 3 | export async function load({ fetch, setHeaders }) { 4 | setHeaders({ 5 | 'cache-control': `public, max-age=${maxAge}` 6 | }); 7 | 8 | return await getPosts(fetch, { limit: 40 }); 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/blog/README.md: -------------------------------------------------------------------------------- 1 | # Ghost blog integration with SvelteKit 2 | 3 | Optimised to work with server-side rendering and `adapter-node` 4 | 5 | [See the live site](https://tradingstrategy.ai/blog). 6 | 7 | ## Features 8 | 9 | - Designed so that your Ghost hosted instance can be password protected and not picked up by search engine 10 | - Blog roll page 11 | - [Blog post page](./[slug]/+page.svelte) 12 | - [RSS](./rss.xml/+server.ts) 13 | - Sitemap for Google Search Console 14 | - SEO with HTML meta tags 15 | - [Social media tags for Facebook, Twitter](./SocialMetaTags.svelte) 16 | - [Special HTML manipulation techniques to clean up Ghost HTML output](./[slug]/BlogPostContent.svelte) 17 | - [Automatically add table of contents listing based on headings](./[slug]/BlogPostContent.svelte) 18 | - [Proxy images locally](./image/server.ts) to make Twitter card preview images work correctly 19 | 20 | ## Ghost REST API 21 | 22 | Uses [Ghost REST Content API](https://ghost.org/docs/content-api/) 23 | -------------------------------------------------------------------------------- /src/routes/blog/SocialLinks.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 34 | -------------------------------------------------------------------------------- /src/routes/blog/[slug=slug]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { getPost, maxAge } from '$lib/blog/client'; 3 | 4 | export async function load({ fetch, params, setHeaders }) { 5 | const post = await getPost(fetch, params.slug).catch((e: any) => { 6 | const status = [404, 422].includes(e.status) ? 404 : 503; 7 | error(status, e.message); 8 | }); 9 | 10 | setHeaders({ 11 | 'cache-control': `public, max-age=${maxAge}` 12 | }); 13 | 14 | return { post }; 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/blog/[slug=slug]/TableOfContents.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | {#each entries as { id, innerText, tagName } (id)} 9 | 10 | {innerText} 11 | {/each} 12 | 13 |
14 | 15 | 29 | -------------------------------------------------------------------------------- /src/routes/blog/image/[...file]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Proxy Ghost images via a local route so social platforms 3 | * render preview images correctly. 4 | */ 5 | import { ghostConfig } from '$lib/config'; 6 | 7 | export async function GET({ params }) { 8 | const resp = await fetch(`${ghostConfig.apiUrl}/${params.file}`); 9 | const image = await resp.arrayBuffer(); 10 | 11 | const headers = { 12 | 'content-type': resp.headers.get('content-type'), 13 | 'cache-control': resp.headers.get('cache-control'), 14 | etag: resp.headers.get('etag') 15 | }; 16 | 17 | return new Response(image, { headers }); 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/blog/posts/+server.ts: -------------------------------------------------------------------------------- 1 | import { proxyPosts } from '$lib/blog/client.js'; 2 | 3 | export const GET = proxyPosts; 4 | -------------------------------------------------------------------------------- /src/routes/blog/sitemap.xml/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a sitemap for blog posts 3 | */ 4 | import type { BlogPostIndexItem } from '$lib/schemas/blog.js'; 5 | import { SitemapStream } from 'sitemap'; 6 | import { Readable } from 'stream'; 7 | import { getPosts, maxAge } from '$lib/blog/client'; 8 | 9 | export async function GET({ fetch, setHeaders, url }) { 10 | const { posts } = await getPosts(fetch, { limit: 'all' }); 11 | 12 | const stream = new SitemapStream({ hostname: url.origin }); 13 | const entries = posts.map((post: BlogPostIndexItem) => ({ 14 | url: `blog/${post.slug}`, 15 | lastmod: post.updated_at, 16 | priority: 0.8 17 | })); 18 | Readable.from(entries).pipe(stream); 19 | 20 | setHeaders({ 21 | 'content-type': 'application/xml', 22 | 'cache-control': `public, max-age=${maxAge}` 23 | }); 24 | 25 | // coerce stream to ReadableStream to make TypeScript happy 26 | return new Response(stream as unknown as ReadableStream); 27 | } 28 | -------------------------------------------------------------------------------- /src/routes/community/+page.ts: -------------------------------------------------------------------------------- 1 | export async function load() { 2 | return { skipFooter: true }; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/dao/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | Trading Strategy DAO LLC 8 | 9 | 10 | 11 |
12 |
13 | 19 |
20 | Smart contract address: 21 | To be announced 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/routes/dao/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load({ parent }) { 4 | const { admin } = await parent(); 5 | 6 | if (!admin) { 7 | error(404, 'Not found'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/diagnostics/+page.server.ts: -------------------------------------------------------------------------------- 1 | export async function load({ request }) { 2 | return { 3 | requestHeaders: Object.fromEntries(request.headers) 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/glossary/+layout.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Data loader for all /glossary routes 3 | */ 4 | import { error } from '@sveltejs/kit'; 5 | import { getCachedGlossary, GlossaryParseError } from './glossary'; 6 | 7 | export async function load({ fetch, setHeaders }) { 8 | let glossary; 9 | 10 | try { 11 | glossary = await getCachedGlossary(fetch); 12 | } catch (e) { 13 | if (e instanceof GlossaryParseError) { 14 | error(503, { 15 | message: 'Service Unavailable', 16 | stack: e.stack?.split('\n') 17 | }); 18 | } 19 | throw e; 20 | } 21 | 22 | // Setting cache-control and age headers to limit re-fetching 23 | // of this resource by browser and reverse proxy / CDN 24 | setHeaders({ 25 | 'cache-control': `public, max-age=${getCachedGlossary.ttl}`, 26 | age: getCachedGlossary.getAge(fetch).toFixed(0) 27 | }); 28 | 29 | return { glossary }; 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/glossary/[slug=slug]/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Load the server-side glossary dictionary and extract one term out of it. 3 | * 4 | * The glossary data is cached on CloudFlare is frequently accessed. 5 | */ 6 | import { error } from '@sveltejs/kit'; 7 | 8 | export async function load({ params, parent }) { 9 | const { glossary } = await parent(); 10 | 11 | const entry = glossary[params.slug]; 12 | 13 | if (!entry) { 14 | error(404, `Glossary entry not found: ${params.slug}`); 15 | } 16 | 17 | return { entry }; 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/glossary/sitemap.xml/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a sitemap for glossary only. 3 | * 4 | * - All glossary terms are boosted to crawling priority 1.0 5 | */ 6 | import { SitemapStream } from 'sitemap'; 7 | import { Readable } from 'stream'; 8 | import { getCachedGlossary } from '../glossary'; 9 | 10 | export async function GET({ fetch, setHeaders, url }) { 11 | const glossary = await getCachedGlossary(fetch); 12 | 13 | const stream = new SitemapStream({ hostname: url.origin }); 14 | const entries = Object.keys(glossary).map((slug) => ({ 15 | url: `glossary/${slug}`, 16 | priority: 1.0 17 | })); 18 | Readable.from(entries).pipe(stream); 19 | 20 | setHeaders({ 21 | 'content-type': 'application/xml', 22 | 'cache-control': `public, max-age=${getCachedGlossary.ttl}`, 23 | age: getCachedGlossary.getAge(fetch).toFixed(0) 24 | }); 25 | 26 | // coerce stream to ReadableStream to make TypeScript happy 27 | return new Response(stream as unknown as ReadableStream); 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/logos/[type=logoType]/[slug]/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, text } from '@sveltejs/kit'; 2 | 3 | export async function GET({ params }) { 4 | const { type, slug } = params; 5 | 6 | let data: string; 7 | 8 | try { 9 | data = (await import(`$lib/assets/logos/${type}/${slug}.svg?raw`)).default; 10 | } catch (e) { 11 | error(404, 'File not found'); 12 | } 13 | 14 | return text(data, { 15 | headers: { 16 | 'Content-Type': 'image/svg+xml' 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/newsletter/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 | 17 | -------------------------------------------------------------------------------- /src/routes/qr/+page.ts: -------------------------------------------------------------------------------- 1 | export async function load() { 2 | return { skipFooter: true }; 3 | } 4 | -------------------------------------------------------------------------------- /src/routes/search/FacetFilter.svelte: -------------------------------------------------------------------------------- 1 | 17 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/routes/search/NumericFilter.svelte: -------------------------------------------------------------------------------- 1 | 18 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/routes/sentry-test/+page.server.ts: -------------------------------------------------------------------------------- 1 | export async function load({ url }) { 2 | if (url.searchParams.has('server')) { 3 | throw new Error('Sentry test error - server'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/slow-load/+page.ts: -------------------------------------------------------------------------------- 1 | export async function load({ url }) { 2 | const page = Number(url.searchParams.get('page')) || 1; 3 | await new Promise((resolve) => setTimeout(resolve, page * 2000)); 4 | return { page }; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/strategies/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { assertNotGeoBlocked } from '$lib/helpers/geo'; 2 | 3 | export async function load({ locals, url }) { 4 | const { admin, ipCountry } = locals; 5 | assertNotGeoBlocked('strategies:view', ipCountry, admin); 6 | 7 | // URL param to demo the geo-block feature, e.g.: 8 | // https://tradingstrategy.ai/strategies?geoBlockDemo=RU 9 | const demoIpCountry = url.searchParams.get('geoBlockDemo'); 10 | if (demoIpCountry) { 11 | assertNotGeoBlocked('strategies:view', demoIpCountry); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/strategies/+page.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Using a server-only load function to serve cached strategies 3 | * 4 | * NOTE: do NOT add HTTP caching to this route, since it renders 5 | * different content based on admin role and IP country 6 | */ 7 | import { getCachedStrategies } from 'trade-executor/client/strategy-info'; 8 | import { fetchPublicApi } from '$lib/helpers/public-api'; 9 | 10 | async function fetchTvlData() { 11 | try { 12 | return (await fetchPublicApi(fetch, 'impressive-numbers')).strategies_tvl; 13 | } catch (e) { 14 | console.error('Request failed; rendering page without TVL data.'); 15 | console.error(e); 16 | } 17 | } 18 | 19 | export async function load({ fetch, locals }) { 20 | const { admin } = locals; 21 | 22 | // return all strategies for admins, "live" strategies for non-admins 23 | const strategies = getCachedStrategies(fetch).then((strategies) => { 24 | return admin ? strategies : strategies.filter((s) => s.tags?.includes('live')); 25 | }); 26 | 27 | // return TVL data for admins only 28 | const tvlData = admin ? fetchTvlData() : undefined; 29 | 30 | return { 31 | strategies: await strategies, 32 | tvlData: await tvlData 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/routes/strategies/StrategyTvlChart.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 | {#snippet subtitle()} 21 | Total value locked 22 | in live strategies 23 | {/snippet} 24 | 25 |
26 | 27 | 42 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy].ipynb/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, text } from '@sveltejs/kit'; 2 | import { publicApiError } from '$lib/helpers/public-api'; 3 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 4 | 5 | export async function GET({ fetch, params }) { 6 | const strategy = configuredStrategies.get(params.strategy); 7 | if (!strategy) error(404, 'Not found'); 8 | 9 | const resp = await fetch(`${strategy.url}/file?type=notebook`); 10 | 11 | if (!resp.ok) throw await publicApiError(resp); 12 | 13 | const headers = new Headers({ 14 | 'content-type': 'application/x-ipynb+json', 15 | 'content-disposition': `attachment; filename="${params.strategy}.ipynb"`, 16 | 'cache-control': 'public, max-age=1800' // 30 minutes: 30 * 60 = 1800 17 | }); 18 | 19 | if (resp.headers.has('last-modified')) { 20 | headers.set('last-modified', resp.headers.get('last-modified')!); 21 | } 22 | 23 | return text(await resp.text(), { headers }); 24 | } 25 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | captureException(e)}> 22 | 23 | {@render children()} 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/+layout.ts: -------------------------------------------------------------------------------- 1 | export const ssr = false; 2 | 3 | export async function load() { 4 | return { 5 | skipNavbar: true, 6 | skipFooter: true, 7 | skipSideNav: true, 8 | skipBreadcrumbs: true 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/+layout.ts: -------------------------------------------------------------------------------- 1 | export async function load() { 2 | return { 3 | slug: 'connect-wallet', 4 | 5 | title: 'Connect wallet', 6 | 7 | steps: [ 8 | { slug: 'introduction', label: 'Introduction' }, 9 | { slug: 'connect', label: 'Connect your wallet' }, 10 | { slug: 'balance', label: 'Wallet balance' }, 11 | { slug: 'success', label: 'Success' } 12 | ], 13 | 14 | dataSchema: undefined 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/balance/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/balance/+page.ts: -------------------------------------------------------------------------------- 1 | import type { TokenBalance } from '$lib/eth-defi/schemas/token.js'; 2 | import { config } from '$lib/wallet/client'; 3 | import { getAccount, getBalance } from '@wagmi/core'; 4 | 5 | export async function load({ parent }) { 6 | const { chain, vault } = await parent(); 7 | const { address } = getAccount(config) as { address: Address }; 8 | 9 | let denominationTokenPromise: Promise | undefined; 10 | 11 | if (vault.depositEnabled()) { 12 | denominationTokenPromise = vault.getDenominationTokenBalance(config, address); 13 | } 14 | 15 | return { 16 | nativeCurrency: await getBalance(config, { address, chainId: chain.id }), 17 | denominationToken: await denominationTokenPromise 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/connect/+page.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/introduction/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

11 | Connect your crypto wallet to Trading Strategy in just a few clicks! Choose your preferred wallet application, 12 | confirm the connection in your wallet, and view your current crypto balances. Let's get started! 13 |

14 |
15 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/connect-wallet/success/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

8 | Congratulations! You have successfully connected your wallet to Trading Strategy. You can now deposit funds in 9 | {strategy.name} and other strategies. Click "Done" to return to the strategy overview. Happy trading! 10 |

11 |
12 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/deposit/balance/+page.ts: -------------------------------------------------------------------------------- 1 | import { config } from '$lib/wallet/client'; 2 | import { getAccount, getBalance } from '@wagmi/core'; 3 | 4 | export async function load({ parent }) { 5 | const { chain, vault } = await parent(); 6 | const { address } = getAccount(config) as { address: Address }; 7 | 8 | return { 9 | nativeCurrency: await getBalance(config, { address, chainId: chain.id }), 10 | denominationToken: await vault.getDenominationTokenBalance(config, address) 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/deposit/connect/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/deposit/introduction/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |

13 | Ready to deposit in {strategy.name}? Deposit {denominationTokenInfo.label} to add this 14 | strategy to your DeFi trading portfolio. Connect your wallet, review your balances, enter your desired deposit amount, 15 | and make your payment. Let's get started! 16 |

17 |
18 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/error/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load() { 4 | error(400, 'Wizard not properly initialized'); 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/redeem/connect/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/(wizard)/redeem/deposit-status/+page.ts: -------------------------------------------------------------------------------- 1 | import { config } from '$lib/wallet/client'; 2 | import { getAccount, getBalance } from '@wagmi/core'; 3 | 4 | export async function load({ parent }) { 5 | const { chain, vault } = await parent(); 6 | const address = getAccount(config).address!; 7 | 8 | return { 9 | tokenPromises: { 10 | nativeCurrency: getBalance(config, { address, chainId: chain.id }), 11 | vaultShares: vault.getShareBalance(config, address), 12 | vaultNetValue: vault.getShareValueUSD(config, address) 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, json } from '@sveltejs/kit'; 2 | import { getStrategyInfo } from 'trade-executor/client/strategy-info.js'; 3 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 4 | 5 | export async function GET({ fetch, params }) { 6 | const strategyConf = configuredStrategies.get(params.strategy); 7 | if (!strategyConf) error(404, 'Not found'); 8 | 9 | const strategy = await getStrategyInfo(fetch, strategyConf); 10 | 11 | return json({ 12 | id: strategy.id, 13 | name: strategy.name, 14 | short_description: strategy.short_description, 15 | url: strategy.url 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/MetricsBox.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#if title} 7 |

{title}

8 | {/if} 9 | 10 |
11 | 12 | 33 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/+layout.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load({ parent }) { 4 | const { deferred } = await parent(); 5 | const state = await deferred.state; 6 | 7 | if (!state) { 8 | error(503, 'Error loading strategy state'); 9 | } 10 | 11 | return { state }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/+page.ts: -------------------------------------------------------------------------------- 1 | import { getTradingPositionInfoArray } from 'trade-executor/models/position-info'; 2 | 3 | export async function load({ params, parent }) { 4 | // status can be `open`, `closed` or `frozen` (see params/positionStatus.ts) 5 | const { status } = params; 6 | const { admin, strategy, state } = await parent(); 7 | const { hiddenPositions } = strategy; 8 | 9 | let positions = getTradingPositionInfoArray(state, status); 10 | 11 | if (!admin) { 12 | positions = positions.filter((p) => !hiddenPositions.includes(p.position_id)); 13 | } 14 | 15 | const reserves = Object.values(state.portfolio.reserves)[0]; 16 | 17 | return { positions, status, reserves }; 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/PositionFlag.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | {label} 18 | 19 | 20 |
21 |

{title}

22 | {@render children()} 23 |
24 |
25 | 26 | 49 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/+layout.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { getTradingPositionInfo } from 'trade-executor/models/position-info'; 3 | 4 | export async function load({ params, parent }) { 5 | // status can be `open`, `closed` or `frozen` (see params/positionStatus.ts) 6 | const { position: id, status } = params; 7 | const { state } = await parent(); 8 | 9 | const position = getTradingPositionInfo(state, status, id); 10 | 11 | if (!position) { 12 | error(404, 'Not found'); 13 | } 14 | 15 | return { 16 | breadcrumbs: { [id]: `Position #${id}` }, 17 | position, 18 | status, 19 | skipSideNav: true 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer].json/+server.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { getRawStrategyState } from 'trade-executor/client/state'; 3 | 4 | export async function GET({ fetch, params }) { 5 | const state = await getRawStrategyState(fetch, params.strategy); 6 | const position = state?.portfolio[`${params.status}_positions`][params.position]; 7 | const trade = position?.trades[params.trade]; 8 | 9 | if (!trade) error(404, 'Not found'); 10 | 11 | const payload = JSON.stringify(trade, null, 4); 12 | 13 | return new Response(payload, { 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | 'Content-Length': String(Buffer.byteLength(payload)) 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load({ params, parent }) { 4 | const tradeId = Number(params.trade); 5 | const { breadcrumbs, position } = await parent(); 6 | 7 | const trade = position.trades.find((t) => t.trade_id === tradeId); 8 | 9 | if (!trade) { 10 | error(404, 'Not found'); 11 | } 12 | 13 | return { 14 | trade, 15 | breadcrumbs: { 16 | ...breadcrumbs, 17 | [`trade-${tradeId}`]: `Trade #${tradeId}` 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/BlockchainExplorerLink.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if href} 11 | 12 | {tx_hash} 13 | 14 | {:else} 15 | {tx_hash} 16 | {/if} 17 | 18 | 27 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/TransactionStatus.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | {#if status} 8 | Success 9 | {:else} 10 | Failure: {revert_reason} 11 | {/if} 12 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/description/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Description | {strategy.name} | Trading Strategy 12 | 13 | 14 | 15 |
16 | {#if content} 17 | 18 | {:else} 19 | No details available. 20 | {/if} 21 |
22 | 23 | 35 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/fees/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { config } from '$lib/wallet/client.js'; 3 | 4 | export async function load({ parent }) { 5 | const { vault } = await parent(); 6 | 7 | if (!vault.depositEnabled()) { 8 | error(404, 'Not found'); 9 | } 10 | 11 | return { 12 | vault, // type-narrowed vault 13 | fees: await vault.getFees(config) 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/performance/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load({ parent }) { 4 | const { deferred } = await parent(); 5 | const strategyState = await deferred.state; 6 | 7 | if (!strategyState) { 8 | error(503, 'Error loading strategy state'); 9 | } 10 | 11 | return { strategyState }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/source/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 15 | 16 | 17 | Source code | {strategy.name} | Trading Strategy 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/source/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch the source code page. 3 | */ 4 | import { error } from '@sveltejs/kit'; 5 | import { publicApiError } from '$lib/helpers/public-api'; 6 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 7 | 8 | export async function load({ params, fetch }) { 9 | const strategy = configuredStrategies.get(params.strategy); 10 | if (!strategy) error(404, 'Not found'); 11 | 12 | let resp; 13 | try { 14 | resp = await fetch(`${strategy.url}/source`); 15 | } catch (e) { 16 | const stack = [`Error loading data from URL: ${strategy.url}/source`, e.message]; 17 | error(503, { message: 'Service Unavailable', stack }); 18 | } 19 | 20 | if (!resp.ok) throw await publicApiError(resp); 21 | 22 | return { 23 | code: await resp.text() 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/+layout.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | 28 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/+layout.ts: -------------------------------------------------------------------------------- 1 | export async function load() { 2 | return { 3 | breadcrumbs: { 4 | status: 'Instance status', 5 | logs: 'Logs', 6 | 'decision-making': 'Decision making' 7 | } 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | export async function load() { 4 | redirect(302, 'tech-details/status'); 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/decision-making/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch the strategy decision making visualisation. 3 | * 4 | * Generate a variant of decision making status image URL for both color schemes. 5 | */ 6 | import { error } from '@sveltejs/kit'; 7 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 8 | 9 | export async function load({ params }) { 10 | const strategy = configuredStrategies.get(params.strategy); 11 | if (!strategy) error(404, 'Not found'); 12 | 13 | const imageUrls: Record = {}; 14 | 15 | for (let theme of ['light', 'dark']) { 16 | const encoded = new URLSearchParams({ theme, type: 'large' }); 17 | imageUrls[theme] = `${strategy.url}/visualisation?${encoded}`; 18 | } 19 | 20 | return { imageUrls }; 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/logs/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch the server logs on the page load. 3 | */ 4 | import { error } from '@sveltejs/kit'; 5 | import { publicApiError } from '$lib/helpers/public-api'; 6 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 7 | 8 | export async function load({ params, fetch }) { 9 | const strategy = configuredStrategies.get(params.strategy); 10 | if (!strategy) error(404, 'Not found'); 11 | 12 | const resp = await fetch(`${strategy.url}/logs`); 13 | if (!resp.ok) throw await publicApiError(resp); 14 | 15 | return { 16 | logs: await resp.json() 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/tech-details/status/+page.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch the instance status metadta. 3 | */ 4 | import { error } from '@sveltejs/kit'; 5 | import { publicApiError } from '$lib/helpers/public-api'; 6 | import { configuredStrategies } from 'trade-executor/schemas/configuration'; 7 | import { type RunState, runStateSchema } from 'trade-executor/schemas/run-state'; 8 | 9 | export async function load({ params, fetch }) { 10 | const strategy = configuredStrategies.get(params.strategy); 11 | if (!strategy) error(404, 'Not found'); 12 | 13 | let runState: RunState; 14 | 15 | try { 16 | const resp = await fetch(`${strategy.url}/status`); 17 | if (!resp.ok) throw await publicApiError(resp); 18 | runState = runStateSchema.parse(await resp.json()); 19 | } catch (e) { 20 | const stack = [`Error loading data from URL: ${strategy.url}/status`, e.message]; 21 | error(503, { message: 'Service Unavailable', stack }); 22 | } 23 | 24 | return { runState }; 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/strategies/[strategy]/vault/+page.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | 3 | export async function load({ parent }) { 4 | const { vault } = await parent(); 5 | 6 | if (!vault.depositEnabled()) { 7 | error(404, 'Not found'); 8 | } 9 | 10 | // re-return type-narrowed vault 11 | return { vault }; 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/tos/[fileName].txt/+server.ts: -------------------------------------------------------------------------------- 1 | import { error, text } from '@sveltejs/kit'; 2 | 3 | export async function GET({ params }) { 4 | const { fileName } = params; 5 | 6 | let tosText: string; 7 | 8 | try { 9 | tosText = (await import(`$lib/assets/tos/${fileName}.txt?raw`)).default; 10 | } catch (e) { 11 | error(404, 'File not found'); 12 | } 13 | 14 | return text(tosText, { 15 | headers: { 16 | 'Content-Type': 'text/plain; charset=utf-8' 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/trading-view/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | export async function load({ fetch }) { 4 | try { 5 | const impressiveNumbers = await fetchPublicApi(fetch, 'impressive-numbers'); 6 | return { impressiveNumbers }; 7 | } catch (e) { 8 | console.error('Request failed; rendering page without data.'); 9 | console.error(e); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/+layout.ts: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit'; 2 | import { chainsUnderMaintenance } from '$lib/config'; 3 | import { fetchPublicApi } from '$lib/helpers/public-api'; 4 | 5 | export async function load({ fetch, params }) { 6 | const chain_slug = params.chain; 7 | 8 | // trigger error if chain is under maintenance 9 | const chainName = chainsUnderMaintenance[chain_slug]; 10 | if (chainName) { 11 | error(503, { chainName, message: `Chain under maintenance: ${chainName}` }); 12 | } 13 | 14 | return { 15 | chain: await fetchPublicApi(fetch, 'chain-details', { chain_slug }) 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/TopExchanges.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {format(row.human_readable_name)} 13 | {formatDollar(row.usd_volume_30d)} 14 | 15 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/TopPairs.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {format(row.pair_symbol)} 13 | {format(row.exchange_name)} 14 | {formatDollar(row.pair_tvl)} 15 | 16 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/TopReserves.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | {#if row.asset_name} 14 | 15 | {:else} 16 | --- 17 | {/if} 18 | 19 | {format(row.protocol_name)} 20 | {formatDollar(row.totalLiquidityUSD)} 21 | 22 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/TopTokens.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | {format(row.name)} 14 | {format(row.symbol)} 15 | 16 | {formatDollar(row.liquidity_latest)} 17 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/[exchange]/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | /** 4 | * NOTE: this `load` function is re-exported by `./export-data/+page.ts` 5 | * If it changes to require additional data, this may no longer make sense. 6 | */ 7 | export async function load({ params, fetch }) { 8 | const exchange_slug = params.exchange; 9 | const chain_slug = params.chain; 10 | 11 | return { 12 | exchange: await fetchPublicApi(fetch, 'exchange-details', { exchange_slug, chain_slug }) 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/[exchange]/[pair]/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | import { timeBucketEnum } from '$lib/schemas/utility.js'; 3 | 4 | export async function load({ fetch, params, setHeaders, url }) { 5 | const pair = await fetchPublicApi(fetch, 'pair-details', { 6 | chain_slug: params.chain, 7 | exchange_slug: params.exchange, 8 | pair_slug: params.pair 9 | }); 10 | 11 | const timeBucketParam = url.searchParams.get('timeBucket'); 12 | const timeBucket = timeBucketEnum.catch('1d').parse(timeBucketParam); 13 | 14 | // Cache the pair data pages for 30 minutes at the Cloudflare edge so the 15 | // pages are served really fast if they get popular, and also for speed test 16 | setHeaders({ 17 | 'cache-control': 'public, max-age=1800' // 30 minutes: 30 * 60 = 1800 18 | }); 19 | 20 | return { 21 | summary: pair.summary, 22 | details: pair.additional_details, 23 | timeBucket 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/[exchange]/[pair]/api-and-historical-data/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { backendUrl } from '$lib/config'; 3 | 4 | export async function load() { 5 | redirect(301, `${backendUrl}/explorer/#/Trading%20pair`); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/[exchange]/export-data/+page.ts: -------------------------------------------------------------------------------- 1 | // re-export load function from [exchange]/+page.ts 2 | export { load } from '../+page'; 3 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/exchanges/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | // https://tradingstrategy.ai/api/explorer/#/Exchange/web_exchanges 4 | export async function load({ fetch, params }) { 5 | return await fetchPublicApi(fetch, 'exchanges', { chain_slug: params.chain }); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/lending/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchLendingReserves } from '$lib/explorer/lending-reserve-client'; 2 | 3 | export async function load({ fetch, params }) { 4 | // Fetching all reserves and using client-side pagination/sort for now 5 | return await fetchLendingReserves(fetch, { chain_slug: params.chain, page_size: 1000 }); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/lending/[protocol]/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | // redirect to chain/lending index for now 4 | export async function load({ params }) { 5 | redirect(307, `/trading-view/${params.chain}/lending`); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/lending/[protocol]/[reserve]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { LendingReserve } from '$lib/explorer/lending-reserve-client'; 2 | import { fetchPublicApi } from '$lib/helpers/public-api'; 3 | import { timeBucketEnum } from '$lib/schemas/utility.js'; 4 | 5 | export async function load({ fetch, params, url }) { 6 | const reserve = (await fetchPublicApi(fetch, 'lending-reserve/details', { 7 | chain_slug: params.chain, 8 | protocol_slug: params.protocol, 9 | reserve_slug: params.reserve 10 | })) as LendingReserve; 11 | 12 | const timeBucketParam = url.searchParams.get('timeBucket'); 13 | const timeBucket = timeBucketEnum.catch('1d').parse(timeBucketParam); 14 | 15 | return { reserve, timeBucket }; 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/tokens/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchTokens } from '$lib/explorer/token-client'; 2 | 3 | export async function load({ fetch, params, url }) { 4 | const { searchParams } = url; 5 | const page = Number(searchParams.get('page')) || 0; 6 | const sort = searchParams.get('sort') || 'volume_24h'; 7 | const direction = searchParams.get('direction') || 'desc'; 8 | 9 | const data = await fetchTokens(fetch, { chain_slug: params.chain, page, sort, direction }); 10 | 11 | return { 12 | tokens: { ...data, page, sort, direction } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/trading-view/[chain=slug]/trading-pairs/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPairs } from '$lib/explorer/pair-client'; 2 | 3 | export async function load({ fetch, params, url }) { 4 | const { searchParams } = url; 5 | const page = Number(searchParams.get('page')) || 0; 6 | const sort = searchParams.get('sort') || 'tvl'; 7 | const direction = searchParams.get('direction') || 'desc'; 8 | 9 | const pairs = await fetchPairs(fetch, { 10 | chain_slugs: params.chain, 11 | page, 12 | sort, 13 | direction 14 | }); 15 | 16 | return { 17 | pairs, 18 | options: { page, sort, direction } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/trading-view/backtesting/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | export async function load({ fetch }) { 4 | return { 5 | datasets: await fetchPublicApi(fetch, 'datasets') 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/trading-view/blockchains/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | // https://tradingstrategy.ai/api/explorer/#/default/web_chain_details 4 | export async function load({ fetch }) { 5 | return { 6 | chains: await fetchPublicApi(fetch, 'chains') 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/routes/trading-view/blockchains/sitemap.xml/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate sitemap with entries for each chain 3 | */ 4 | import { SitemapStream } from 'sitemap'; 5 | import { fetchPublicApi } from '$lib/helpers/public-api'; 6 | 7 | const chainPages = ['', 'exchanges', 'lending', 'tokens', 'trading-pairs']; 8 | const path = 'trading-view'; 9 | const priority = 0.8; 10 | 11 | export async function GET({ fetch, setHeaders, url, route }) { 12 | const chains = await fetchPublicApi(fetch, 'chains'); 13 | 14 | const stream = new SitemapStream({ hostname: url.origin }); 15 | 16 | for (const { chain_slug } of chains) { 17 | for (const page of chainPages) { 18 | const url = [path, chain_slug, page].filter(Boolean).join('/'); 19 | stream.write({ url, priority }); 20 | } 21 | } 22 | 23 | stream.end(); 24 | 25 | setHeaders({ 26 | 'content-type': 'application/xml', 27 | 'cache-control': 'public, max-age=600' 28 | }); 29 | 30 | // coerce stream to ReadableStream to make TypeScript happy 31 | return new Response(stream as unknown as ReadableStream); 32 | } 33 | -------------------------------------------------------------------------------- /src/routes/trading-view/exchanges/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPublicApi } from '$lib/helpers/public-api'; 2 | 3 | // https://tradingstrategy.ai/api/explorer/#/Exchange/web_exchanges 4 | export async function load({ fetch }) { 5 | return await fetchPublicApi(fetch, 'exchanges'); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/lending-reserves/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchLendingReserves } from '$lib/explorer/lending-reserve-client'; 2 | 3 | export async function load({ fetch }) { 4 | // Fetching all reserves and using client-side pagination/sort for now 5 | return await fetchLendingReserves(fetch, { page_size: 1000 }); 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/trading-view/lending-reserves/sitemap.xml/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate sitemap with entries for each lending reserve 3 | */ 4 | import { SitemapStream } from 'sitemap'; 5 | import { Readable } from 'stream'; 6 | import { fetchLendingReserves } from '$lib/explorer/lending-reserve-client'; 7 | import { lendingReserveInternalUrl } from '$lib/helpers/lending-reserve'; 8 | 9 | export async function GET({ fetch, setHeaders, url }) { 10 | // Fetching all reserves (currently < 1000); may need to paginate in the future 11 | const reserves = await fetchLendingReserves(fetch, { page_size: 1000 }); 12 | 13 | const stream = new SitemapStream({ hostname: url.origin }); 14 | const entries = reserves!.rows.map((row) => ({ 15 | url: lendingReserveInternalUrl(row), 16 | priority: 0.8 17 | })); 18 | Readable.from(entries).pipe(stream); 19 | 20 | setHeaders({ 21 | 'content-type': 'application/xml', 22 | 'cache-control': 'public, max-age=600' 23 | }); 24 | 25 | // coerce smStream to ReadableStream to make TypeScript happy 26 | return new Response(stream as unknown as ReadableStream); 27 | } 28 | -------------------------------------------------------------------------------- /src/routes/trading-view/top-list/[direction=momentum]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { MomentumPair } from '$lib/momentum/MomentumTable.svelte'; 2 | import { fetchPublicApi } from '$lib/helpers/public-api'; 3 | 4 | export async function load({ params, fetch }) { 5 | const direction = params.direction.split('-')[1] as 'up' | 'down'; 6 | 7 | const data = await fetchPublicApi(fetch, 'top-momentum'); 8 | 9 | return { 10 | direction, 11 | pairs: data[`top_${direction}_24h_min_liq_1m`] as MomentumPair[] 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/trading-view/trading-pairs/+page.ts: -------------------------------------------------------------------------------- 1 | import { fetchPairs } from '$lib/explorer/pair-client'; 2 | 3 | export async function load({ fetch, url }) { 4 | const { searchParams } = url; 5 | const page = Number(searchParams.get('page')) || 0; 6 | const sort = searchParams.get('sort') || 'volume_30d'; 7 | const direction = searchParams.get('direction') || 'desc'; 8 | 9 | const pairs = await fetchPairs(fetch, { page, sort, direction }); 10 | 11 | return { 12 | pairs, 13 | options: { page, sort, direction } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /static/32747dc0fe894e0a9c52d0f4ef89c584.txt: -------------------------------------------------------------------------------- 1 | 32747dc0fe894e0a9c52d0f4ef89c584 -------------------------------------------------------------------------------- /static/8080d9b849c848ff8c7a214ef77d0bd1.txt: -------------------------------------------------------------------------------- 1 | 8080d9b849c848ff8c7a214ef77d0bd1 -------------------------------------------------------------------------------- /static/avatars/arbitrum-btc-breakout.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/arbitrum-btc-breakout.webp -------------------------------------------------------------------------------- /static/avatars/arbitrum-btc-eth-stoch-rsi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/arbitrum-btc-eth-stoch-rsi.webp -------------------------------------------------------------------------------- /static/avatars/arbitrum-one-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/arbitrum-one-1.jpg -------------------------------------------------------------------------------- /static/avatars/avalanche-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/avalanche-1.jpg -------------------------------------------------------------------------------- /static/avatars/base-ath.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/base-ath.webp -------------------------------------------------------------------------------- /static/avatars/base-memecoin-index.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/base-memecoin-index.webp -------------------------------------------------------------------------------- /static/avatars/base-memex.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/base-memex.webp -------------------------------------------------------------------------------- /static/avatars/base-sentimeme.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/base-sentimeme.webp -------------------------------------------------------------------------------- /static/avatars/bsc-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/bsc-1.jpg -------------------------------------------------------------------------------- /static/avatars/enzyme-arbitrum-eth-btc-rsi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-arbitrum-eth-btc-rsi.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-ethereum-btc-eth-stoch-rsi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-ethereum-btc-eth-stoch-rsi.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-breakout.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-breakout.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-btc-rsi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-btc-rsi.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-btc-usdc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-btc-usdc.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-rolling-ratio.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-rolling-ratio.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-usdc-sls.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-usdc-sls.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-eth-usdc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-eth-usdc.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-matic-eth-usdc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-matic-eth-usdc.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-matic-usdc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-matic-usdc.webp -------------------------------------------------------------------------------- /static/avatars/enzyme-polygon-multipair.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/enzyme-polygon-multipair.webp -------------------------------------------------------------------------------- /static/avatars/ethereum-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/ethereum-1.jpg -------------------------------------------------------------------------------- /static/avatars/ethereum-memecoin-swing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/ethereum-memecoin-swing.webp -------------------------------------------------------------------------------- /static/avatars/ethereum-memecoin-vol-basket.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/ethereum-memecoin-vol-basket.webp -------------------------------------------------------------------------------- /static/avatars/polygon-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/polygon-1.jpg -------------------------------------------------------------------------------- /static/avatars/polygon-eth-spot-short.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/polygon-eth-spot-short.webp -------------------------------------------------------------------------------- /static/avatars/polygon-multipair-momentum.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/avatars/polygon-multipair-momentum.webp -------------------------------------------------------------------------------- /static/b8e3e4232bd34aea86a92cbfce3dc767.txt: -------------------------------------------------------------------------------- 1 | b8e3e4232bd34aea86a92cbfce3dc767 -------------------------------------------------------------------------------- /static/brand-mark-100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/brand-mark-100x100.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/favicon.ico -------------------------------------------------------------------------------- /static/fonts/SourceCodePro/latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceCodePro/latin-italic.woff -------------------------------------------------------------------------------- /static/fonts/SourceCodePro/latin-normal.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceCodePro/latin-normal.woff -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-400-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-400-italic.woff2 -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-400-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-400-normal.woff2 -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-600-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-600-italic.woff2 -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-600-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-600-normal.woff2 -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-700-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-700-italic.woff2 -------------------------------------------------------------------------------- /static/fonts/SourceSerifPro/latin-700-normal.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/static/fonts/SourceSerifPro/latin-700-normal.woff2 -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | 4 | # Make sure Google Bot is not hitting JSON API directly 5 | Disallow: /api/ 6 | Allow: /api/sitemap/ 7 | Allow: /api/explorer/ 8 | Disallow: /*.json$ 9 | Disallow: /*.ipynb$ 10 | Disallow: /strategies/tvl 11 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SvelteKit configuration file. See: 3 | * https://kit.svelte.dev/docs/configuration 4 | */ 5 | import node from '@sveltejs/adapter-node'; 6 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 7 | 8 | /** @type {import('@sveltejs/kit').Config} */ 9 | const config = { 10 | kit: { 11 | adapter: node({ 12 | envPrefix: 'FRONTEND_' 13 | }), 14 | 15 | alias: { 16 | 'design-system-fonts': 'deps/fonts', 17 | 'trade-executor': 'src/lib/trade-executor' 18 | }, 19 | 20 | // prevent clickjacking (block 3rd-parties from including site via iframe) 21 | csp: { 22 | directives: { 23 | 'frame-ancestors': ['self'] 24 | } 25 | }, 26 | 27 | // disable CSRF origin checking for now; see: 28 | // - https://kit.svelte.dev/docs/configuration#csrf 29 | // - https://github.com/sveltejs/kit/tree/master/packages/adapter-node#origin-protocol_header-and-host_header 30 | csrf: { 31 | checkOrigin: false 32 | }, 33 | 34 | env: { 35 | publicPrefix: 'TS_PUBLIC_' 36 | } 37 | }, 38 | 39 | preprocess: vitePreprocess() 40 | }; 41 | 42 | export default config; 43 | -------------------------------------------------------------------------------- /tests/e2e/blog.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('blog index page', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/blog'); 6 | }); 7 | 8 | test('should load initial blog post tiles', async ({ page }) => { 9 | const posts = page.locator('[data-testid="blog-roll"] a'); 10 | const count = await posts.count(); 11 | expect(count).toBeGreaterThanOrEqual(5); 12 | }); 13 | 14 | test('clicking blog tile heading should open post', async ({ page }) => { 15 | const post = page.locator('[data-testid="blog-roll"] a').first(); 16 | const heading = post.getByRole('heading'); 17 | const url = await post.getAttribute('href'); 18 | const title = await heading.textContent(); 19 | await heading.click(); 20 | await expect(page).toHaveURL(url); 21 | await expect(page).toHaveTitle(title); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/e2e/glossary.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Glossary site section tests. 3 | * 4 | * These tests rely on https://tradingstrategy.ai/docs/glossary.html source data to be online 5 | * 6 | * To run: 7 | * 8 | * npx playwright test --config tests/e2e -g glossary 9 | * npx playwright install chromium 10 | */ 11 | import { expect, test } from '@playwright/test'; 12 | 13 | test.describe('glossary tests', () => { 14 | test.beforeEach(async ({ page }) => { 15 | await page.goto('/glossary'); 16 | }); 17 | 18 | test('should have a list of terms', async ({ page }) => { 19 | const terms = page.locator('a[data-testid="index-term"]'); 20 | const count = await terms.count(); 21 | expect(count).toBeGreaterThanOrEqual(5); 22 | }); 23 | 24 | test('clicking any term should open What is page', async ({ page }) => { 25 | const a = page.locator('a[data-testid="index-term"]').first(); 26 | await a.click(); 27 | await page.isVisible('[data-testid="glossary-heading"]'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('home page', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/'); 6 | }); 7 | 8 | test('should display impressive numbers', async ({ page }) => { 9 | const impressiveSection = page.getByTestId('impressive-numbers'); 10 | await expect(impressiveSection).toHaveText(/[\d.,]+ trading pairs/); 11 | await expect(impressiveSection).toHaveText(/\$[\d.,]+[kMB] liquidity/); 12 | await expect(impressiveSection).toHaveText(/[\d.,]+ decentralised exchanges/); 13 | }); 14 | 15 | test('should include blog roll', async ({ page }) => { 16 | const blogRoll = page.locator('[data-testid="blog-roll"]:visible'); 17 | await expect(blogRoll.getByRole('link', { name: 'Read article' })).toHaveCount(3); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | import { webServerConfig } from '../helpers'; 3 | 4 | const config: PlaywrightTestConfig = { 5 | webServer: webServerConfig('production'), 6 | reporter: process.env.GITHUB_ACTIONS ? [['dot'], ['github']] : 'list' 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /tests/e2e/trading-view/chain-index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { chains } from '../../../src/lib/helpers/chain'; 3 | 4 | test.describe('chain index page', () => { 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/trading-view/blockchains'); 7 | }); 8 | 9 | test('tiles should include exchange count', async ({ page }) => { 10 | const blockchains = page.getByRole('link', { name: /[\d,]+ exchanges/ }); 11 | const count = await blockchains.count(); 12 | expect(count).toBeGreaterThan(0); 13 | }); 14 | 15 | test('chain tile should link to chain details', async ({ page }) => { 16 | const chain = page.getByRole('link', { name: /Ethereum/ }); 17 | await chain.click(); 18 | await expect(page).toHaveURL(/ethereum/); 19 | }); 20 | 21 | test('tile data (from API) should match local chain helper', async ({ page }) => { 22 | for (const chain of chains) { 23 | const tile = page.getByTestId(`chain-${chain.id}-${chain.slug}`); 24 | if ((await tile.count()) > 0) { 25 | const title = tile.getByRole('heading', { name: chain.name, exact: true }); 26 | await expect(title).toBeVisible(); 27 | } 28 | } 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/e2e/trading-view/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('trading data overview', () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/trading-view'); 6 | }); 7 | 8 | test('should include blockchain count in blockchain tile', async ({ page }) => { 9 | const blockchains = page.getByText(/Currently indexing data from \d+ blockchains/); 10 | await expect(blockchains).toBeVisible(); 11 | }); 12 | 13 | test('should include exchange count in exchanges tile', async ({ page }) => { 14 | const exchanges = page.getByText(/Currently indexing data from [\d,]+ DEXes/); 15 | await expect(exchanges).toBeVisible(); 16 | }); 17 | 18 | test('should include pairs count in trading pairs tile', async ({ page }) => { 19 | const pairs = page.getByText(/Currently indexing data from [\d,]+ trading pairs/); 20 | await expect(pairs).toBeVisible(); 21 | }); 22 | 23 | test('should include database size in backtesting tile', async ({ page }) => { 24 | const database = page.getByText(/Currently providing [\d.,]+ TB of data/); 25 | await expect(database).toBeVisible(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/e2e/trading-view/pair-details.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('pair details should include pair summary info', async ({ page }) => { 4 | await page.goto('/trading-view/ethereum/uniswap-v2/wise-eth'); 5 | const pairInfo = page.getByTestId('pair-info'); 6 | await expect(pairInfo).toHaveText(/Token\s+WISE/); 7 | await expect(pairInfo).toHaveText(/Quoted in\s+ETH/); 8 | await expect(pairInfo).toHaveText(/Price\s+[\d.,]+\s+USD/); 9 | await expect(pairInfo).toHaveText(/Token price\s+[\d.,]+\s+ETH/); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/fixtures/chain-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "chain_name": "Ethereum", 3 | "chain_slug": "ethereum", 4 | "chain_id": 1, 5 | "chain_logo": "https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg", 6 | "chain_explorer": "https://etherscan.io", 7 | "homepage": "https://ethereum.org", 8 | "exchanges": 425, 9 | "pairs": 178932, 10 | "tracked_pairs": 26722, 11 | "tokens": 160382, 12 | "minute_candles": 425660896, 13 | "start_block": 9900000, 14 | "end_block": 17021615, 15 | "last_swap_at": "2023-04-11T01:27:35" 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/impressive-numbers.json: -------------------------------------------------------------------------------- 1 | { 2 | "oracles": 1, 3 | "blockchains": 3, 4 | "exchanges": 1000, 5 | "pairs": 15000, 6 | "tokens": 10000, 7 | "database_size": 1000000000, 8 | "liquidity": 1234567890.123 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/token/details.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wrapped Ether", 3 | "symbol": "WETH", 4 | "token_id": 2, 5 | "chain_id": 1, 6 | "chain_name": "Ethereum", 7 | "chain_slug": "ethereum", 8 | "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 9 | "total_supply": "4005054", 10 | "decimals": 18, 11 | "chainscan_metadata": {}, 12 | "chainscan_metadata_last_updated": "2022-08-07T22:27:36.946054", 13 | "liquidity_latest": 633081719.4531937, 14 | "volume_24h": 829720210.6890125, 15 | "liquidity_change_7d": 64307314267488930, 16 | "liquidity_change_30d": 13921881448451084, 17 | "liquidity_change_360d": 1031881992047522400, 18 | "liquidity_all_time_high": null, 19 | "liquidity_all_time_low": null, 20 | "pair_count": 113055, 21 | "explorer_link": "https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" 22 | } 23 | -------------------------------------------------------------------------------- /tests/fixtures/top-momentum.json: -------------------------------------------------------------------------------- 1 | { 2 | "top_up_24h_min_liq_1m": [], 3 | "top_down_24h_min_liq_1m": [] 4 | } 5 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * command used to start SvelteKit preview server 3 | * use `--skip-build` flag to skip build step (runs faster) 4 | * e.g.,: `npm run test:integration --skip-build` 5 | */ 6 | export function webServerCommand() { 7 | if (process.env.npm_config_skip_build) { 8 | return 'npm run preview'; 9 | } 10 | return 'npm run build && npm run preview'; 11 | } 12 | 13 | export function webServerConfig(mode: string) { 14 | return { 15 | command: `${webServerCommand()} -- --mode=${mode} --host 127.0.0.1`, 16 | port: 4173 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /tests/integration/index.test.ts-snapshots/home-page-hero-banner-looks-correct-1-darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/tests/integration/index.test.ts-snapshots/home-page-hero-banner-looks-correct-1-darwin.png -------------------------------------------------------------------------------- /tests/integration/index.test.ts-snapshots/home-page-hero-banner-looks-correct-1-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tradingstrategy-ai/frontend/2dfd6efd0abc34cb572a069e310c092956ed14d3/tests/integration/index.test.ts-snapshots/home-page-hero-banner-looks-correct-1-linux.png -------------------------------------------------------------------------------- /tests/integration/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | import { webServerConfig } from '../helpers'; 3 | 4 | const config: PlaywrightTestConfig = { 5 | webServer: webServerConfig('test'), 6 | reporter: process.env.GITHUB_ACTIONS ? [['dot'], ['github']] : 'list' 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /tests/integration/trading-view/pair-details.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { a } from 'vitest/dist/chunks/suite.BMWOKiTe.js'; 3 | 4 | test.describe('trading pair details page', () => { 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('trading-view/ethereum/uniswap-v2/eth-usdc'); 7 | }); 8 | 9 | test('should include pair info', async ({ page }) => { 10 | const pairInfo = page.getByTestId('pair-info'); 11 | await expect(pairInfo).toContainText('Price 0.116 USD'); 12 | await expect(pairInfo).toContainText('Token price 0.0000870 ETH'); 13 | }); 14 | 15 | test('should include TradingView chart canvas elements', async ({ page }) => { 16 | const tvChart = page.getByTestId('tv-chart'); 17 | await expect(tvChart).toBeVisible(); 18 | const count = await tvChart.locator('canvas').count(); 19 | expect(count).toBeGreaterThan(0); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "types": ["vitest/globals", "@testing-library/jest-dom"] 14 | } 15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 | // 18 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 19 | // from the referenced tsconfig.json - TypeScript does not merge them in 20 | } 21 | -------------------------------------------------------------------------------- /vitest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | // always use mock config in unit tests 5 | vi.mock('$lib/config'); 6 | 7 | // add a stub for `window.scrollTo` 8 | vi.stubGlobal('scrollTo', vi.fn()); 9 | 10 | // add a stub for Element anaimate 11 | Element.prototype.animate = vi.fn(() => ({ 12 | finished: Promise.resolve(), 13 | cancel: vi.fn(), 14 | pause: vi.fn(), 15 | play: vi.fn(), 16 | reverse: vi.fn() 17 | })); 18 | --------------------------------------------------------------------------------