Skip to main content

finance_query/
lib.rs

1//! # finance-query
2//!
3//! A Rust library for querying financial data.
4//! Inspired by yfinance, with smart lazy loading for efficient data fetching.
5//!
6//! ## Quick Start
7//!
8//! ```no_run
9//! use finance_query::Ticker;
10//!
11//! #[tokio::main]
12//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
13//!     // Simple: Create a ticker with default configuration
14//!     let ticker = Ticker::new("AAPL").await?;
15//!
16//!     // First access to any quote property fetches ALL quote modules in one request
17//!     if let Some(financials) = ticker.financial_data().await? {
18//!         println!("Financial data: {:?}", financials);
19//!     }
20//!
21//!     // Subsequent accesses use cached data (no additional network calls)
22//!     if let Some(profile) = ticker.asset_profile().await? {
23//!         println!("Company profile: {:?}", profile);
24//!     }
25//!
26//!     // Chart data is fetched separately and cached by interval/range
27//!     let chart = ticker.chart(
28//!         finance_query::Interval::OneDay,
29//!         finance_query::TimeRange::OneMonth
30//!     ).await?;
31//!     println!("Candles: {}", chart.candles.len());
32//!
33//!     // Builder pattern: Fluent configuration
34//!     let ticker_jp = Ticker::builder("7203.T")
35//!         .lang("ja-JP")
36//!         .region_code("JP")
37//!         .timeout(std::time::Duration::from_secs(30))
38//!         .build()
39//!         .await?;
40//!
41//!     Ok(())
42//! }
43//! ```
44//!
45//! ## Lazy Loading and Caching
46//!
47//! The library fetches on demand and caches by default:
48//! - **Quote data**: all quote modules fetched together on first property access, then reused
49//! - **Chart data**: fetched and cached per (interval, range) combination
50//! - **Recommendations**: fetched once and cached
51//!
52//! A handle caches each response for 60 seconds by default. Use `.cache(ttl)`
53//! on the builder to change that window, or `.no_cache()` to fetch fresh on
54//! every call.
55
56#![warn(missing_docs)]
57#![warn(rustdoc::missing_crate_level_docs)]
58
59// === Modules ===
60// Public modules
61/// External data source adapters (internal — use the public API modules).
62pub(crate) mod adapters;
63/// Error types and result definitions.
64pub mod error;
65/// Non-symbol-specific operations (search, lookup, screeners, market data, etc.).
66pub mod finance;
67pub mod edgar {
68    //! SEC EDGAR API client (keyless — always available, no feature flag needed).
69    //!
70    //!
71    //! Requires a one-time [`init`] call with a contact email address.
72    pub use crate::adapters::edgar::{
73        archive, company_facts, filing_index, init, init_with_config, init_with_rate, resolve_cik,
74        search, submissions,
75    };
76}
77
78// Internal modules
79mod backoff;
80mod constants;
81mod models;
82#[cfg(any(feature = "risk", feature = "backtesting"))]
83mod perf_metrics;
84mod providers;
85pub(crate) mod rate_limiter;
86mod scrapers;
87mod ticker;
88mod tickers;
89mod utils;
90
91// Feature-gated external data source modules
92#[cfg(feature = "alphavantage")]
93pub mod alphavantage {
94    //! Alpha Vantage configuration (requires the `alphavantage` feature).
95    //!
96    //! Call [`init`] before constructing a provider when the API key should
97    //! come from application configuration instead of the process environment.
98    pub use crate::adapters::alphavantage::{init, init_with_timeout};
99}
100
101#[cfg(feature = "fmp")]
102pub mod fmp {
103    //! Financial Modeling Prep configuration (requires the `fmp` feature).
104    //!
105    //! Quotes, charts, fundamentals, corporate, and research data are served
106    //! through [`Providers`](crate::Providers). Only key configuration is
107    //! exposed here; the adapter's raw response types stay internal.
108    pub use crate::adapters::fmp::{init, init_with_timeout};
109}
110
111#[cfg(feature = "polygon")]
112pub mod polygon {
113    //! Massive (formerly Polygon.io) configuration (requires the `polygon`
114    //! feature).
115    //!
116    //! REST operations route through [`Providers`](crate::Providers) and
117    //! Polygon's real-time channels through
118    //! [`streaming`](crate::streaming). Only key configuration is exposed
119    //! here; the adapter's raw response types stay internal.
120    pub use crate::adapters::polygon::{init, init_with_timeout};
121}
122
123#[cfg(feature = "fred")]
124pub mod fred {
125    //! FRED economic data API (requires `fred` feature).
126    //!
127    //! Access 800k+ macroeconomic time series and US Treasury yield curve data.
128    pub use crate::adapters::fred::{
129        ReleaseDate, init, init_with_timeout, release_dates, series, treasury_yields,
130    };
131    pub use crate::models::economic::{MacroObservation, MacroSeries, TreasuryYield};
132}
133
134#[cfg(feature = "crypto")]
135pub mod crypto {
136    //! CoinGecko cryptocurrency data (requires `crypto` feature).
137    //!
138    //! Keyless shortcuts, the crypto counterpart to [`finance`](crate::finance).
139    //! [`Market::crypto_trending`](crate::domains::Market::crypto_trending) and
140    //! [`crypto_global`](crate::domains::Market::crypto_global) reach the same
141    //! data through provider routing when other CRYPTO providers are configured.
142    pub use crate::adapters::coingecko::{
143        CoinQuote, coin, coins, fetch_crypto_global_response as global,
144        fetch_crypto_trending_response as trending, fetch_symbol_search_response as search,
145    };
146    pub use crate::models::crypto::{GlobalCryptoStats, TrendingCoin};
147    pub use crate::models::discovery::reference::SymbolMatch;
148}
149
150#[cfg(feature = "gdelt")]
151pub mod gdelt {
152    //! GDELT global news search (requires `gdelt` feature, keyless).
153    //!
154    //! Keyless shortcut for callers that want GDELT specifically;
155    //! [`Ticker::news`](crate::Ticker::news) reaches the same data through
156    //! `Capability::CORPORATE` when GDELT is routed.
157    pub use crate::adapters::gdelt::fetch_news_response as news;
158    pub use crate::models::corporate::news::News;
159}
160
161#[cfg(feature = "cftc")]
162pub mod cftc {
163    //! CFTC Commitments of Traders positioning (requires `cftc` feature, keyless).
164    //!
165    //! Keyless shortcut;
166    //! [`FuturesContract::commitments_of_traders`](crate::FuturesContract::commitments_of_traders)
167    //! reaches the same data through `Capability::FUTURES` when CFTC is routed.
168    pub use crate::adapters::cftc::fetch_commitments_of_traders_response as commitments_of_traders;
169    pub use crate::models::futures::cot::{CommitmentsOfTraders, CotObservation};
170}
171
172#[cfg(feature = "openfigi")]
173pub mod openfigi {
174    //! Security-identifier resolution via OpenFIGI (requires `openfigi`
175    //! feature, keyless).
176    //!
177    //! Resolves a CUSIP, ISIN, SEDOL, or FIGI to the instruments carrying it —
178    //! the missing step for any dataset that identifies holdings by CUSIP
179    //! rather than ticker, such as 13F filings.
180    //!
181    //! Lives here rather than behind the Providers API because resolution is
182    //! not tied to a symbol handle and maps onto no
183    //! [`Capability`](crate::Capability), the same reasoning that puts
184    //! [`edgar`](crate::edgar) and [`fred`](crate::fred) at the crate root.
185    //!
186    //! No API key is required; `OPENFIGI_API_KEY` is optional and only raises
187    //! the quota.
188    //!
189    //! ```no_run
190    //! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
191    //! use finance_query::openfigi;
192    //!
193    //! // One CUSIP maps to every venue listing of the security.
194    //! for listing in openfigi::resolve_cusip("037833100").await? {
195    //!     println!("{:?} on {:?}", listing.ticker, listing.exchange_code);
196    //! }
197    //! # Ok(())
198    //! # }
199    //! ```
200
201    use crate::error::Result;
202    pub use crate::models::discovery::figi::{MappingFilter, SecurityIdKind, SecurityMapping};
203
204    /// Resolve a CUSIP to every instrument carrying it.
205    ///
206    /// Returns an empty list when the identifier is well-formed but matches
207    /// nothing; a malformed identifier is an error.
208    pub async fn resolve_cusip(cusip: &str) -> Result<Vec<SecurityMapping>> {
209        crate::adapters::openfigi::resolve(SecurityIdKind::Cusip, cusip).await
210    }
211
212    /// Resolve an ISIN to every instrument carrying it.
213    pub async fn resolve_isin(isin: &str) -> Result<Vec<SecurityMapping>> {
214        crate::adapters::openfigi::resolve(SecurityIdKind::Isin, isin).await
215    }
216
217    /// Resolve a SEDOL to every instrument carrying it.
218    pub async fn resolve_sedol(sedol: &str) -> Result<Vec<SecurityMapping>> {
219        crate::adapters::openfigi::resolve(SecurityIdKind::Sedol, sedol).await
220    }
221
222    /// Resolve an identifier of any supported [`SecurityIdKind`].
223    pub async fn resolve(kind: SecurityIdKind, id: &str) -> Result<Vec<SecurityMapping>> {
224        crate::adapters::openfigi::resolve(kind, id).await
225    }
226
227    /// Resolve many identifiers of the same kind in as few requests as
228    /// possible (OpenFIGI accepts 10 per request without a key).
229    ///
230    /// The result is positional: element `i` answers `ids[i]`, with an empty
231    /// list where nothing matched.
232    pub async fn resolve_many(
233        kind: SecurityIdKind,
234        ids: &[&str],
235    ) -> Result<Vec<Vec<SecurityMapping>>> {
236        crate::adapters::openfigi::resolve_many(kind, ids).await
237    }
238
239    /// [`resolve_many`] narrowed to one exchange code, optionally including
240    /// delisted equities. A ticker resolves to its current holder, so check a
241    /// returned name before trusting it for a delisted company.
242    pub async fn resolve_many_with(
243        kind: SecurityIdKind,
244        ids: &[&str],
245        filter: &MappingFilter,
246    ) -> Result<Vec<Vec<SecurityMapping>>> {
247        crate::adapters::openfigi::resolve_many_with(kind, ids, filter).await
248    }
249}
250
251#[cfg(feature = "defi")]
252pub mod defi {
253    //! Market-wide DeFi data via DefiLlama (requires `defi` feature, keyless).
254    //!
255    //! Chain rankings and stablecoin supplies describe the market as a whole,
256    //! not one asset, so there is no symbol handle to hang them off — they sit
257    //! at the crate root the way [`edgar`](crate::edgar) and
258    //! [`fred`](crate::fred) do.
259    //!
260    //! Protocol-shaped data *is* symbol-shaped and lives on
261    //! [`CryptoCoin::tvl`](crate::CryptoCoin::tvl) instead.
262    //!
263    //! ```no_run
264    //! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
265    //! use finance_query::defi;
266    //!
267    //! for chain in defi::chains().await?.into_iter().take(5) {
268    //!     println!("{}: ${:?}", chain.name, chain.tvl);
269    //! }
270    //! # Ok(())
271    //! # }
272    //! ```
273
274    use crate::error::Result;
275    pub use crate::models::crypto::defi::{
276        ChainAllocation, ChainTvl, ProtocolTvl, StablecoinSupply, TvlPoint,
277    };
278
279    /// Fetch aggregate total value locked for every chain, largest first.
280    pub async fn chains() -> Result<Vec<ChainTvl>> {
281        crate::adapters::defillama::chains().await
282    }
283
284    /// Fetch circulating supply for every tracked stablecoin, largest first.
285    ///
286    /// Supplies are denominated in the coin's pegged asset — read `peg_type`
287    /// before summing across coins pegged to different currencies.
288    pub async fn stablecoins() -> Result<Vec<StablecoinSupply>> {
289        crate::adapters::defillama::stablecoins().await
290    }
291}
292
293pub mod feeds;
294
295#[cfg(feature = "risk")]
296pub mod risk;
297
298#[cfg(feature = "translation")]
299pub mod translation;
300
301// ============================================================================
302// High-level API - Primary interface for most use cases
303// ============================================================================
304pub mod domains;
305pub use models::quote::response::QuoteSummaryResponse;
306
307/// Yahoo `quoteSummary` module types carried by [`QuoteSummaryResponse`].
308///
309/// A [`QuoteProvider`] implementation populates these; they are also what
310/// [`Ticker::price`], [`Ticker::asset_profile`] and the other module
311/// accessors return.
312pub mod quote {
313    pub use crate::models::corporate::{
314        AssetProfile, CalendarEvents, CompanyOfficer, Earnings, EarningsHistory, EarningsTrend,
315        EquityPerformance, FundOwnership, FundPerformance, FundProfile, InsiderHolders,
316        InsiderTransactions, InstitutionOwnership, MajorHoldersBreakdown, NetSharePurchaseActivity,
317        RecommendationTrend, SecFilings, SummaryProfile, TopHoldings, UpgradeDowngradeHistory,
318    };
319    pub use crate::models::fundamentals::{DefaultKeyStatistics, FinancialData, SummaryDetail};
320    pub use crate::models::market::{IndexTrend, IndustryTrend, SectorTrend};
321    pub use crate::models::quote::price::Price;
322    pub use crate::models::quote::quote_type::QuoteTypeData;
323}
324pub use providers::CryptoProvider;
325pub use providers::EconomicProvider;
326pub use providers::ForexProvider;
327pub use providers::config::{Providers, ProvidersBuilder};
328pub use providers::{
329    CalendarProvider, ChartProvider, CommoditiesProvider, CorporateProvider, DiscoveryProvider,
330    FilingsProvider, FundamentalsProvider, FuturesProvider, IndicesProvider, MarketProvider,
331    OptionsProvider, ProviderAdapter, ProviderCore, ProviderSet, QuoteProvider, Routes,
332};
333pub use providers::{
334    Capability, CustomId, Fetch, Operation, Provider, ProviderHealth, RetryPolicy,
335};
336
337/// The attribute every capability trait implementation needs.
338///
339/// Re-exported so a downstream crate does not add its own `async-trait`
340/// dependency and risk a version mismatch with this one. The trade is that
341/// this crate's semver now covers `async-trait`: an `async-trait` 0.2 would
342/// be a breaking change here, because downstream impls use this attribute.
343pub use async_trait::async_trait;
344pub use ticker::{ClientHandle, Ticker, TickerBuilder};
345
346// Domain-specific query handles — constructable via Providers factory methods.
347pub use domains::CryptoCoin;
348pub use domains::ForexPair;
349pub use domains::{EconomicCatalog, EconomicIndicator};
350
351// Remaining Capability handles — indices, futures, commodities, filings, discovery
352pub use domains::Commodity;
353pub use domains::Discovery;
354pub use domains::Filings;
355pub use domains::FuturesContract;
356pub use domains::Index;
357pub use domains::Snapshot;
358// `Market` is unconditional — its grouped-daily/crypto methods route through
359// CHART/CRYPTO, which have their own (broader) per-method gating rather than
360// requiring fmp/polygon/alphavantage.
361pub use domains::Market;
362pub use domains::MarketCalendar;
363
364// Provider-specific financial data functions
365// (FMP, Polygon, Alpha Vantage — defined in the finance module)
366#[cfg(feature = "polygon")]
367pub use finance::symbol_sentiment;
368#[cfg(feature = "fmp")]
369pub use finance::{
370    AnalystEstimate, AnalystRecommendation, InsiderTransaction, Period, analyst_estimates,
371    analyst_recommendations, insider_trading,
372};
373#[cfg(feature = "alphavantage")]
374pub use finance::{EarningsCalendarEntry, IpoCalendarEntry, earnings_calendar, ipo_calendar};
375
376pub use tickers::{
377    BatchCapitalGainsResponse, BatchChartsResponse, BatchDividendsResponse,
378    BatchFinancialsResponse, BatchNewsResponse, BatchOptionsResponse, BatchQuotesResponse,
379    BatchRecommendationsResponse, BatchSparksResponse, BatchSplitsResponse, Tickers,
380    TickersBuilder,
381};
382
383#[cfg(feature = "indicators")]
384pub use tickers::BatchIndicatorsResponse;
385
386// ============================================================================
387// Error types and results
388// ============================================================================
389// Capability-routed response types (DISCOVERY / CALENDAR / MARKET / CHART)
390pub use models::calendar::market::{CalendarDetail, CalendarKind, MarketCalendarEntry};
391pub use models::chart::{PriceAdjustment, StockBar, StockBarsRequest};
392pub use models::discovery::listings::{StockListing, StockListingRequest, StockType, TickerChange};
393pub use models::discovery::reference::{
394    ExchangeInfo, ScreenerFilters, ScreenerMatch, SymbolDetails, SymbolMatch,
395};
396pub use models::market::performance::{
397    IndustryPe, MoverDirection, MoverQuote, SectorPe, SectorPerformance, SectorPerformanceHistory,
398};
399pub use models::pagination::{PageCursor, ProviderPage};
400
401pub use error::{ErrorCategory, FinanceError, Result};
402
403// ============================================================================
404// Options - Configure API requests
405// ============================================================================
406pub use finance::{LookupOptions, LookupType, SearchOptions};
407
408// ============================================================================
409// Parameter enums - Used with Ticker and finance methods
410// ============================================================================
411pub use constants::indices::Region as IndicesRegion;
412pub use constants::screeners::Screener;
413pub use constants::sectors::Sector;
414pub use constants::{Frequency, Interval, Region, StatementType, TimeRange, ValueFormat};
415
416// ============================================================================
417// Response types - Top-level types returned by API methods
418// ============================================================================
419pub use models::{
420    calendar::{CalendarEvent, EventKind},
421    chart::Chart,
422    chart::spark::Spark,
423    corporate::earnings_transcript::EarningsTranscript,
424    corporate::governance::{EmployeeCount, ExecutiveCompensation},
425    corporate::news::News,
426    corporate::press_release::PressRelease,
427    corporate::recommendation::Recommendation,
428    corporate::transcript::{Transcript, TranscriptWithMeta},
429    discovery::lookup::LookupResults,
430    discovery::screeners::ScreenerResults,
431    discovery::search::SearchResults,
432    discovery::trending::TrendingQuote,
433    filings::{
434        CompanyFacts, CongressionalTrade, EdgarSearchResults, EdgarSubmissions, FailToDeliver,
435        FilingSearchFilters, FilingSearchHit, FilingSection, FilingSectionForm, InsiderTrade,
436        InstitutionalHolding, ProviderFiling, ProviderFilings, RiskFactor,
437    },
438    fundamentals::{
439        CompanyProfile, EarningsSurprise, EtfCountryWeighting, EtfHolding, EtfProfile,
440        EtfSectorWeighting, FinancialRatiosTtm, FinancialStatement, GradingAction, KeyMetricsTtm,
441        PriceTargetConsensus, PriceTargetSummary, RatingConsensus, ShareFloat, ShortInterest,
442        ShortVolume,
443    },
444    market::currencies::Currency,
445    market::exchanges::Exchange,
446    market::hours::MarketHours,
447    market::industries::IndustryData,
448    market::market_summary::MarketSummaryQuote,
449    market::sectors::SectorData,
450    options::Options,
451    quote::Quote,
452    sentiment::{FearAndGreed, FearGreedLabel, SymbolSentiment},
453};
454// Offline VADER sentiment scoring (feature-gated)
455#[cfg(feature = "sentiment")]
456pub use models::sentiment::{Sentiment, SentimentLabel, analyze as analyze_sentiment};
457// Multi-provider capability response types (feature-gated)
458pub use models::commodities::CommodityQuote;
459pub use models::crypto::CryptoQuote;
460pub use models::economic::{
461    EconomicCategory, EconomicRelease, EconomicSeries, EconomicSeriesMatch,
462};
463#[cfg(feature = "fiscaldata")]
464pub use models::economic::{TreasuryAuction, TreasuryAuctionQuery, UpcomingAuction};
465pub use models::forex::ForexQuote;
466pub use models::futures::FuturesQuote;
467#[cfg(feature = "cftc")]
468pub use models::futures::cot::{CommitmentsOfTraders, CotObservation};
469pub use models::indices::{IndexConstituent, IndexConstituentChange, IndexQuote, MajorIndex};
470pub use models::quote::snapshot::{AssetClass, MarketSnapshot};
471
472// ============================================================================
473// Nested types - Commonly accessed fields within response types
474// ============================================================================
475pub use models::{
476    chart::{Candle, CapitalGain, ChartEvents, ChartMeta, Dividend, DividendAnalytics, Split},
477    corporate::recommendation::SimilarSymbol,
478    discovery::lookup::LookupQuote,
479    discovery::screeners::ScreenerQuote,
480    discovery::search::{
481        ResearchReport, ResearchReports, SearchNews, SearchNewsList, SearchQuote, SearchQuotes,
482    },
483    filings::filing_index::{EdgarFilingIndex, EdgarFilingIndexItem},
484    filings::{
485        CikEntry, EdgarFiling, EdgarFilingFile, EdgarFilingRecent, EdgarFilings, EdgarSearchHit,
486        EdgarSearchHitsContainer, EdgarSearchSource, EdgarSearchTotal, FactConcept, FactUnit,
487        FactsByTaxonomy,
488    },
489    market::hours::MarketTime,
490    market::market_summary::SparkData,
491    options::{Contracts, OptionChain, OptionContract, OptionsQuote},
492    quote::FormattedValue,
493};
494
495// ============================================================================
496// Query builders - Types for constructing custom screener queries
497// ============================================================================
498pub use constants::exchange_codes::ExchangeCode;
499pub use constants::industries::Industry;
500pub use models::discovery::screeners::{
501    ConditionValue, EquityField, EquityScreenerQuery, FundField, FundScreenerQuery,
502    LogicalOperator, Operator, QueryCondition, QueryGroup, QueryOperand, QuoteType, ScreenerField,
503    ScreenerFieldExt, ScreenerFundCategory, ScreenerPeerGroup, ScreenerQuery, SortType,
504};
505
506// ============================================================================
507// Real-time streaming
508// ============================================================================
509// WebSocket-based real-time price streaming with a Flow-like Stream API.
510pub mod streaming;
511
512// ============================================================================
513// Format type parameters — phantom types for compile-time format selection
514// ============================================================================
515
516/// Compile-time format type parameters for [`Quote`] and other
517/// `FormattedValue`-bearing structs.
518///
519/// | Marker | `F::Value<f64>` | Access pattern |
520/// |---|---|---|
521/// | [`format::Both`]  | `FormattedValue<f64>` | `.raw` / `.fmt` / `.long_fmt` |
522/// | [`format::Raw`]   | `f64`                 | direct (no unwrapping) |
523/// | [`format::Pretty`] | `String`             | human-readable string |
524///
525/// ```no_run
526/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
527/// use finance_query::{format, Ticker};
528/// let ticker = Ticker::new("AAPL").await?;
529/// let quote: finance_query::Quote<format::Raw> = ticker.quote().await?;
530/// # Ok(())
531/// # }
532/// ```
533pub mod format {
534    pub use crate::models::format::{Both, Pretty, Raw};
535}
536
537// ============================================================================
538// DataFrame support (requires "dataframe" feature)
539// ============================================================================
540// When enabled, structs with #[derive(ToDataFrame)] get a to_dataframe() method.
541// The derive macro auto-generates DataFrame conversion for all scalar fields.
542#[cfg(feature = "dataframe")]
543pub use finance_query_derive::ToDataFrame;
544
545// ============================================================================
546// Technical Indicators (requires "indicators" feature)
547// ============================================================================
548// Technical analysis indicators for price data (SMA, EMA, RSI, MACD, Bollinger Bands).
549// When enabled, Chart gets extension methods: chart.sma(), chart.ema(), chart.rsi(), etc.
550#[cfg(feature = "indicators")]
551pub mod indicators;
552
553#[cfg(feature = "indicators")]
554pub use indicators::{
555    // Summary types
556    AroonData,
557    // Individual indicator types
558    BollingerBands,
559    BollingerBandsData,
560    BullBearPowerData,
561    // Candlestick pattern types
562    CandlePattern,
563    DonchianChannelsData,
564    ElderRayData,
565    IchimokuData,
566    Indicator,
567    IndicatorError,
568    IndicatorResult,
569    IndicatorsSummary,
570    KeltnerChannelsData,
571    MacdData,
572    MacdResult,
573    PatternSentiment,
574    StochasticData,
575    SuperTrendData,
576    atr,
577    patterns,
578};
579
580// ============================================================================
581// Backtesting Engine (requires "backtesting" feature)
582// ============================================================================
583// Strategy backtesting with pre-built and custom strategies, position tracking,
584// stop-loss/take-profit, comprehensive performance metrics, parameter optimization,
585// walk-forward validation, Monte Carlo simulation, and multi-symbol portfolio.
586#[cfg(feature = "backtesting")]
587pub mod backtesting;
588
589// ============================================================================
590// Compile-time thread-safety assertions
591// ============================================================================
592// Ticker and Tickers must be Send + Sync so they can be shared across
593// async tasks and held across .await points (e.g., in Arc, tokio::spawn).
594const _: () = {
595    const fn assert_send_sync<T: Send + Sync>() {}
596    let _ = assert_send_sync::<Ticker>;
597    let _ = assert_send_sync::<Tickers>;
598};
599
600/// Parse PDF text lines from arbitrary bytes, for `fuzz/fuzz_targets`.
601///
602/// Not part of the public API. Requires the `fuzzing` feature, which no normal
603/// build enables.
604#[cfg(all(feature = "fuzzing", feature = "housetrades"))]
605#[doc(hidden)]
606pub fn __fuzz_pdf_extract_lines(bytes: Vec<u8>) -> Option<Vec<String>> {
607    crate::adapters::housetrades::pdf::extract_lines(bytes).ok()
608}
609
610/// Drive the PDF parsers that need no decryption, for `fuzz/fuzz_targets`.
611///
612/// Not part of the public API. Requires the `fuzzing` feature.
613#[cfg(all(feature = "fuzzing", feature = "housetrades"))]
614#[doc(hidden)]
615pub fn __fuzz_pdf_unencrypted(bytes: &[u8]) {
616    crate::adapters::housetrades::pdf::fuzz_unencrypted(bytes);
617}