Pular para o conteúdo

Conversão de moeda (FX declarativo via <CurrencyConversion>)

O Mondrian-4 suporta um elemento de schema <CurrencyConversion> que produz uma medida convertida como SUM(base_measure × exchange_rate), escolhendo automaticamente a taxa correta na data da própria linha de fato via uma tabela de taxas com intervalos de validade não sobrepostos. Você declara o que você quer; o engine compila o band join e a agregação para você.

Por que conversão de moeda declarativa?

Sem <CurrencyConversion>, converter uma medida para uma moeda diferente exige um calculated member manual que precisa reimplementar a busca por data efetiva sempre que a estrutura da tabela de taxas muda:

<CalculatedMember name="Revenue (USD)" dimension="Measures">
<Formula>
Sum(
Existing [Calendar].[Month].Members,
[Measures].[Revenue] * LookupCube("[fx_rate]", ...)
)
</Formula>
</CalculatedMember>

Com <CurrencyConversion> a mesma métrica é:

<CurrencyConversion name="Revenue (USD)" measure="Revenue"
rateTable="fx_rate" rateColumn="rate"
rateType="ECB" rateTypeColumn="rate_type"
factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id"
factDateColumn="month_key"
rateValidFromColumn="valid_from" rateValidToColumn="valid_to"
formatString="#,##0.00"/>

O loader valida que a medida base nomeada e todas as colunas referenciadas existem, gera o band join no momento da carga e dispara um erro claro em vez de produzir um resultado silenciosamente errado.

Posição no schema

Elementos <CurrencyConversion> são envoltos em um bloco <CurrencyConversions> dentro de um <MeasureGroup>, como irmão de <Measures> e <DimensionLinks>:

<Cube name="Monthly Revenue">
<MeasureGroups>
<MeasureGroup name="Revenue" table="mm_monthly">
<Measures>
<Measure name="Revenue" column="revenue" aggregator="sum"/>
</Measures>
<DimensionLinks>
<ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/>
</DimensionLinks>
<CurrencyConversions>
<CurrencyConversion name="Revenue (USD)" measure="Revenue"
rateTable="fx_rate" rateColumn="rate"
rateType="ECB" rateTypeColumn="rate_type"
factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id"
factDateColumn="month_key"
rateValidFromColumn="valid_from" rateValidToColumn="valid_to"
formatString="#,##0.00"/>
</CurrencyConversions>
</MeasureGroup>
</MeasureGroups>
</Cube>

Referência de atributos

AtributoChave XMLChave YAMLObrigatórioDescrição
namenamenamesimO nome da medida convertida gerada. Aparece em [Measures] como qualquer outra medida.
measuremeasuremeasuresimO nome de um <Measure> existente no mesmo measure group (o valor base a multiplicar).
rateTablerateTablerate_tablesimNome da tabela física que segura as taxas de câmbio. Deve existir no schema físico.
rateColumnrateColumnrate_columnsimColuna em rateTable que segura a taxa numérica de câmbio.
rateTyperateTyperate_typesimValor literal usado para filtrar a tabela de taxas a uma fonte (ex.: "ECB", "BLOOMBERG").
rateTypeColumnrateTypeColumnrate_type_columnsimColuna em rateTable que segura o discriminador de tipo de taxa.
factCurrencyColumnfactCurrencyColumnfact_currency_columnsimColuna na tabela de fato que identifica a moeda de origem de cada linha.
rateCurrencyColumnrateCurrencyColumnrate_currency_columnsimColuna em rateTable que identifica a moeda da qual a taxa converte.
factDateColumnfactDateColumnfact_date_columnsimColuna na tabela de fato usada para determinar qual intervalo de taxa se aplica.
rateValidFromColumnrateValidFromColumnrate_valid_from_columnsimColuna em rateTable para o início do intervalo (inclusivo).
rateValidToColumnrateValidToColumnrate_valid_to_columnsimColuna em rateTable para o fim do intervalo (exclusivo).
formatStringformatStringformat_stringnãoFormat string MDX para a medida convertida, ex.: "#,##0.00" ou "$#,##0".

Contrato de dados do intervalo por data efetiva

A tabela de taxas segura uma linha por combinação (currency, rate_type) por período de validade. O engine executa um band join que seleciona exatamente a taxa cujo intervalo contém a data da linha de fato:

fact.factDateColumn >= rate.rateValidFromColumn
AND fact.factDateColumn < rate.rateValidToColumn
AND fact.factCurrencyColumn = rate.rateCurrencyColumn
AND rate.rateTypeColumn = '<rateType literal>'

O join é inerentemente 1-para-1 — uma linha de fato corresponde no máximo a uma linha de taxa — desde que os intervalos da tabela de taxas sejam não sobrepostos. Isso significa que a agregação da medida convertida é simplesmente SUM(fact.measure × rate.rate), e qualquer medida não convertida consultada ao lado dela fica completamente intacta.

Estrutura recomendada da tabela de taxas

CREATE TABLE fx_rate (
currency_id VARCHAR(3) NOT NULL, -- e.g. 'EUR'
rate_type VARCHAR(16) NOT NULL, -- e.g. 'ECB'
rate DECIMAL(18,6) NOT NULL,
valid_from DATE NOT NULL, -- inclusive
valid_to DATE NOT NULL, -- exclusive
PRIMARY KEY (currency_id, rate_type, valid_from)
);

Use valid_to = '9999-12-31' (ou o próximo valid_from do próximo período) como sentinela para o intervalo aberto atual para que o predicado < valid_to sempre funcione limpo.

Requisito de backend e validação fail-closed

A validação é fail-closed: a carga do schema é abortada com uma mensagem de erro clara para qualquer das seguintes condições.

CondiçãoErro
measure não nomeia uma medida existente no measure groupCurrencyConversion "X": measure "Y" not found in measure group
rateTable não existe no schema físicoCurrencyConversion "X": rateTable "Y" not found in physical schema
Qualquer das colunas nomeadas não existe na tabela referenciadaCurrencyConversion "X": column "Y" not found in table "Z"
Backend Calcite não está ativoCurrencyConversion "X": requires Calcite backend

Não há resultado silenciosamente errado — cada configuração ruim é pega antes da primeira consulta rodar.

Exemplo trabalhado: receita mensal do demo Bank

O demo Bank traz um cubo Monthly Revenue cujas linhas de fato mm_monthly são denominadas em EUR. Uma tabela fx_rate segura duas taxas ECB anuais:

currency_idrate_typeratevalid_fromvalid_to
EURECB1.102024-01-012025-01-01
EURECB1.202025-01-012026-01-01

Os dados brutos de fato:

YearRevenue (EUR)
2024600
2025750
Total1350

Declaração no schema

<MeasureGroup name="Revenue" table="mm_monthly">
<Measures>
<Measure name="Revenue" column="revenue" aggregator="sum"/>
</Measures>
<DimensionLinks>
<ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/>
</DimensionLinks>
<CurrencyConversions>
<CurrencyConversion name="Revenue (USD)" measure="Revenue"
rateTable="fx_rate" rateColumn="rate"
rateType="ECB" rateTypeColumn="rate_type"
factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id"
factDateColumn="month_key"
rateValidFromColumn="valid_from" rateValidToColumn="valid_to"
formatString="#,##0.00"/>
</CurrencyConversions>
</MeasureGroup>

Resultados de ouro

LinhaRevenue (EUR)Revenue (USD)Cálculo
2024600660600 × 1.10 (taxa ECB 2024)
2025750900750 × 1.20 (taxa ECB 2025)
Grand total13501560660 + 900

As linhas de 2024 e 2025 usam taxas diferentes porque o band join escolhe a taxa cujo intervalo [valid_from, valid_to) contém o month_key de cada linha de fato. O Revenue (EUR) base não é afetado e permanece 1350.

Consulta MDX de exemplo

SELECT
{ [Measures].[Revenue],
[Measures].[Revenue (USD)] } ON COLUMNS,
[Calendar].[Year].Members ON ROWS
FROM [Monthly Revenue]

Resultado:

YearRevenue (EUR)Revenue (USD)
2024600660.00
2025750900.00
All Time1,3501,560.00

Relação com <CalculatedMembers>

<CurrencyConversion> compila para uma agregação SQL nativa (SUM(fact.revenue × rate.rate)) em vez de um <CalculatedMember> MDX. Isso significa:

  • A conversão é avaliada no banco, não no engine MDX — é tão rápida quanto qualquer medida agregada ordinária.
  • A medida convertida aparece em enumerações de membros XMLA ao lado de medidas regulares.
  • Pode ser referenciada por fórmulas de <CalculatedMember> (ex.: para computar uma margem na moeda alvo).

Se você precisa de uma fórmula que <CurrencyConversion> não pode expressar — por exemplo, converter apenas uma fatia de uma medida ou misturar taxas de múltiplas fontes — use um <CalculatedMember> plano junto com uma busca scripted separada. As duas abordagens podem coexistir no mesmo cubo.

Veja Avançado — Calculated members para a referência completa de <CalculatedMember>.