finance-query v3.0.0

Translations#

info · Feature flag required

toml
finance-query = { version = "...", features = ["translation"] }

Add translation-offline for the fully local machine-translation backend (compiles CTranslate2 from source — requires cmake and a C++ toolchain).

Yahoo Finance returns natural-language fields (company summaries, sector names, news titles) in English regardless of the lang/region request parameters. The translation module post-processes responses so the existing .lang() builder surface actually localizes text.

Only human-readable fields are translated — names, sector/industry display strings, business summaries, news titles, officer titles, transcripts. Symbols, codes, URLs, and numbers are never touched.

Translation Tiers#

  1. Built-in dictionary (always available with translation): exact translations for the finite vocabulary of sector names, security types, and officer titles in 11 languages — German, Spanish, French, Italian, Portuguese, Dutch, Japanese, Korean, Simplified Chinese, Traditional Chinese, Russian. Zero latency, deterministic.
  2. Machine translation backend for free-form text (business summaries, news titles), with process-wide memoization:
    • translation-offline — fully local opus-mt bilingual models (~48 languages, no API key); a small per-language model (~80–210 MB) is downloaded on first use and cached
    • or a custom backend registered via set_backend

Without any backend, free-form fields are left in English while dictionary terms are still translated — enabling translation alone never breaks responses.

Via Ticker#

Setting a non-English language on the builder translates text fields automatically:

rust · no_run feature=translation
use finance_query::Ticker;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ticker = Ticker::builder("7203.T").lang("ja").build().await?;
    let quote = ticker.quote::<finance_query::format::Raw>().await?;
    // quote.sector_disp / long_business_summary are now Japanese.
    Ok(())
}

Batch tickers work the same way:

rust · no_run feature=translation
use finance_query::Tickers;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let tickers = Tickers::builder(["AAPL", "NVDA"]).lang("de").build().await?;
    let news = tickers.news().await?; // titles translated to German
    Ok(())
}

Via Providers#

Multi-provider setups configure the language once on ProvidersBuilder — every Ticker/Tickers handle created from it inherits the language (and translates automatically when it is non-English):

rust · no_run feature=translation
use finance_query::{Capability, Provider, Providers};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let providers = Providers::builder()
        .route(Capability::QUOTE, [Provider::Yahoo])
        .lang("ja")
        .build().await?;

    let ticker = providers.ticker("7203.T").build().await?;
    let quote = ticker.quote::<finance_query::format::Raw>().await?; // Japanese

    // Override per handle if needed:
    let en = providers.ticker("AAPL").lang("en-US").build().await?;
    Ok(())
}

.region() on ProvidersBuilder also sets the language (e.g. Region::Japanja-JP).

Standalone Values#

Results from finance::* functions can be translated explicitly:

rust · no_run feature=translation
use finance_query::{SearchOptions, finance, translation};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut results = finance::search("toyota", &SearchOptions::default()).await?;
    translation::translate(&mut results, "ja").await?;
    Ok(())
}

translate accepts any type implementing the Translatable trait — all text-bearing response models implement it, and Vec<T> / Option<T> compose.

Batches of raw English strings go through translate_texts, which applies the dictionary, the memo cache, and the active backend in that order, preserving input order. Dictionary-tier terms translate with no backend configured and no network — this example runs as a real test:

rust · feature=translation
use finance_query::translation::{self, Lang};

#[tokio::main]
async fn main() {
    let lang = Lang::parse("es").unwrap();
    let translated = translation::translate_texts(
        ["Technology", "Healthcare", "Chief Executive Officer"],
        &lang,
    )
    .await
    .unwrap();
    assert_eq!(translated, ["Tecnología", "Salud", "Director ejecutivo"]);
}
checked claims
translate_textsverified current
translate_dictionarymedian time 1.9 µs(limit < 25.0 µs)0limit 25.0 µs
translate_dictionaryCPU instructions 19,891(limit < 35,000)0limit 35,000

Language Tags#

Targets are BCP 47 language tags, parsed and normalized by Lang. This example runs as a real test:

rust · runnable
use finance_query::translation::Lang;

let lang = Lang::parse("zh-TW").unwrap();
assert_eq!(lang.code(), "zh-Hant"); // region implies Traditional script
assert!(!lang.is_english());

assert!(Lang::parse("en-US").unwrap().is_english()); // English → translation is a no-op
assert_eq!(Lang::parse("pt_BR").unwrap().code(), "pt"); // underscores accepted

println!(
    "zh-TW -> code={:?} is_english={}",
    lang.code(),
    lang.is_english()
);
println!("pt_BR -> code={:?}", Lang::parse("pt_BR").unwrap().code());
recorded outputcargo soothfast docs capture
zh-TW -> code="zh-Hant" is_english=false
pt_BR -> code="pt"
checked claims
Langverified current

English targets are always a no-op; structurally invalid tags return an error.

Offline Backend#

The translation-offline feature bundles a fully local CPU backend built on opus-mt bilingual models (one English→target model per language) run through CTranslate2 with int8 weights. Models are distributed as Argos Translate packages — the same models LibreTranslate uses:

rust · no_run feature=translation-offline
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    finance_query::translation::preload().await?;
    Ok(())
}

Language coverage#

The offline backend covers ~48 languages — the major world languages with a published opus-mt package (Simplified and Traditional Chinese are distinct packages). This is narrower than a single massively-multilingual model, and is a deliberate trade: per-language bilingual models are an order of magnitude smaller and give far tighter cross-language latency parity than one shared decoder.

A target language with no package degrades gracefully rather than erroring the response: free-form text stays English while the built-in dictionary tier still applies (sector names, security types, officer titles in its 11 languages). Only an explicit standalone translate() call to an unsupported language returns the "not supported by the offline translation model" error.

Performance#

Inference runs on the CPU (int8, oneDNN). Each model call sorts its sentences by length and batches them by token budget, so latency tracks real output tokens rather than padding — keeping the spread between languages tight. On a modern 8-core machine a typical response payload — ~20 news titles plus a couple of multi-sentence business summaries — translates in roughly 2–3 seconds cold; memoization makes repeated fields free within a process.

Two characteristics are inherent to the model set, not tuning gaps:

Earnings transcripts are the deliberate exception: a full call transcript is hundreds of kilobytes of prose and takes tens of seconds to translate, and the /transcripts/{symbol}/all server endpoint translates every transcript it returns. The server caches translated transcripts for 7 days (transcripts are immutable), so the cost is paid once per symbol/quarter/language. If you need guaranteed-fast transcript responses, request them in English and translate client-side, or warm the cache ahead of time.

Custom Backend#

Plug any translation engine (e.g. a hosted API) by implementing TranslationBackend (requires the async-trait crate) and registering it process-wide. A custom backend takes precedence over the built-in offline backend:

rust · runnable
use finance_query::translation::{self, Lang, TranslationBackend};
use std::sync::Arc;

struct MyBackend;

#[async_trait::async_trait]
impl TranslationBackend for MyBackend {
    fn id(&self) -> &'static str {
        "my-backend"
    }

    async fn translate_batch(
        &self,
        texts: &[String],
        target: &Lang,
    ) -> finance_query::Result<Vec<String>> {
        // Call your translation service; return one string per input, in order.
        let _ = target;
        Ok(texts.to_vec())
    }
}

#[tokio::main]
async fn main() {
    translation::set_backend(Arc::new(MyBackend));

    // Not in the built-in dictionary, so this routes to the custom backend,
    // which just echoes its input back unchanged.
    let lang = Lang::parse("es").unwrap();
    let translated = translation::translate_texts(["Quantum flux capacitor"], &lang)
        .await
        .unwrap();
    println!("{:?}", translated);
}
recorded outputcargo soothfast docs capture
["Quantum flux capacitor"]
checked claims
TranslationBackendverified current

Inputs are English and may contain multiple sentences; implementations must return one translated string per input, preserving order. Results are memoized process-wide, so repeated fields (e.g. the same sector name across symbols) hit the backend only once.

Server, CLI & MCP#

The same pipeline powers the other finance-query frontends:

Next Steps#

built with cargo soothfast docs build source