Любое руководство по конвертации валют в Power BI относится к одному из двух лагерей. Лагерь DAX показывает изящную меру и негласно предполагает, что таблица ExchangeRate в вашей модели уже есть. Лагерь API показывает фрагмент Power Query, который прекрасно работает в Power BI Desktop и ломается в момент публикации, потому что запланированное обновление его не принимает.
Это руководство закрывает обе половины, по порядку: как забрать курсы валют в реальном времени в Power BI из REST API так, чтобы публикация в службе Power BI пережила это; как смоделировать курсы, чтобы ваши цифры можно было защитить; и как конвертировать суммы — на этапе импорта или на этапе запроса, в зависимости от того, что вашему отчёту действительно нужно.
Все примеры Power Query и DAX ниже написаны по документированным структурам ответов API Finexly.
Сначала определите, какая у вас задача конвертации
«Конвертация валют в Power BI» — это три разные инженерные задачи под одним названием, и выбрать не ту — самая дорогая ошибка во всей статье.
- Много исходных валют, одна валюта отчётности. В таблице продаж есть строки в EUR, GBP и JPY, а финансовому директору нужна одна цифра в USD. Конвертируйте на импорте. Курс — свойство транзакции, а не отчёта.
- Одна исходная валюта, много валют отчётности. Всё хранится в USD, а пользователь выбирает валюту отображения срезом. Конвертируйте на запросе, в DAX. Предрассчитывать каждую валюту непрактично.
- Много исходных валют, много валют отчётности. Приведите всё к одной опорной валюте на импорте, а затем наложите сверху случай 2. Не пытайтесь решить это одним шагом.
Правило, которое повторяют опытные моделировщики и которое стоит повторить ещё раз: конвертируйте так рано, как только можете себе позволить. Каждая конвертация, отложенная до момента запроса, берёт плату на каждом визуальном элементе, при каждой смене фильтра и каждом клике по срезу.
Как правильно забрать курсы в Power Query
Начните с таблицы курсов. Самый эффективный вызов — запрос по нескольким парам сразу: вы получаете все нужные валюты одним запросом вместо одного запроса на валюту.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.finexly.com/v1/convert?q=USD_EUR,USD_GBP,USD_JPY"{
"USD_EUR": { "rate": 0.9215 },
"USD_GBP": { "rate": 0.7892 }
}Вот эквивалент в виде запроса M для Power Query. Создайте пустой запрос (Главная → Создать источник → Пустой запрос → Расширенный редактор) и вставьте:
let
ApiKey = "YOUR_API_KEY",
Pairs = "USD_EUR,USD_GBP,USD_JPY,USD_CAD,USD_AUD,USD_CHF",
Source = Json.Document(
Web.Contents(
"https://api.finexly.com",
[
RelativePath = "v1/convert",
Query = [ q = Pairs ],
Headers = [ #"Authorization" = "Bearer " & ApiKey ]
]
)
),
ToTable = Record.ToTable(Source),
Expanded = Table.ExpandRecordColumn(ToTable, "Value", {"rate"}, {"Rate"}),
Split = Table.SplitColumn(
Expanded, "Name",
Splitter.SplitTextByDelimiter("_", QuoteStyle.None),
{"BaseCurrency", "Currency"}
),
Typed = Table.TransformColumnTypes(
Split,
{{"BaseCurrency", type text}, {"Currency", type text}, {"Rate", type number}}
),
Stamped = Table.AddColumn(Typed, "RetrievedAt", each DateTimeZone.UtcNow(), type datetimezone)
in
StampedНазовите его FxRates. Вы получите таблицу из четырёх столбцов — BaseCurrency, Currency, Rate, RetrievedAt — готовую к использованию в связях и мерах.
Ошибка Web.Contents, которая ломает запланированное обновление
Обратите внимание, чего запрос выше не делает: он никогда не склеивает параметры внутрь строки URL. Это самая частая причина, по которой отчёт с конвертацией валют работает на десктопе и умирает в облаке.
Если написать вот так:
// Do NOT do this
Source = Json.Document(
Web.Contents("https://api.finexly.com/v1/convert?q=" & Pairs)
)…Power BI Desktop обновит это без возражений, а служба Power BI откажет с сообщением «Этот набор данных содержит динамический источник данных. Обновление не поддерживается.» Службе нужно разрешить статический базовый URL на этапе анализа, чтобы привязать к нему учётные данные. Передача переменных частей через RelativePath и Query даёт ровно это: база остаётся https://api.finexly.com, а всё динамическое живёт в параметрах.
То же самое относится к любому запросу, который собирает URL из параметра, даты или значения из другой таблицы. Если вы вынесете из этой статьи одну вещь, пусть это будет RelativePath.
Как обращаться с ключом API, не зашивая его в код
ApiKey = "YOUR_API_KEY" прямо в коде годится для пятиминутного теста и не годится ни для чего общего. Код M семантической модели виден каждому, у кого есть разрешение на сборку.
Два рабочих варианта:
- Параметр Power Query (Главная → Управление параметрами), на который ссылаются как
ApiKey = KeyParam. Он по-прежнему хранится вместе с моделью, но он централизован, его легко ротировать, и его можно переопределять по средам через конвейеры развёртывания. - Тип учётных данных Web API. В службе Power BI откройте Параметры семантической модели → Учётные данные источника данных → Изменить учётные данные и выберите Веб-API, указав ключ там. Дальше служба сама подставляет заголовок
Authorization, а параметрHeadersиз вашего M можно убрать полностью. Секрет остаётся вне определения модели.
Что бы вы ни выбрали, используйте ключ, ограниченный задачами отчётности, и меняйте его, когда кто-то уходит из команды. Если ключ окажется в строке запроса URL, а не в заголовке, он может попасть в журналы доступа прокси и серверов — документация Finexly отдельно об этом предупреждает, и это верно для любого поставщика.
Постройте измерение валют, а не просто список курсов
Одна лишь таблица курсов даёт цифры. Она не даёт ни корректных подписей, ни корректного числа знаков после запятой, ни среза с осмысленной сортировкой. Заберите список валют как отдельное измерение:
let
ApiKey = "YOUR_API_KEY",
Source = Json.Document(
Web.Contents(
"https://api.finexly.com",
[
RelativePath = "v1/currencies",
Headers = [ #"Authorization" = "Bearer " & ApiKey ]
]
)
),
ToTable = Table.FromList(Source, Splitter.SplitByNothing(), {"Currency"}),
Typed = Table.TransformColumnTypes(ToTable, {{"Currency", type text}})
in
TypedЗатем добавьте два столбца, которые Power BI не может вывести за вас:
MinorUnits— сколько знаков после запятой валюта действительно использует. У JPY — 0, у KWD — 3, у большинства — 2. Отформатировать сумму в иенах с двумя знаками — заметная содержательная ошибка в финансовом отчёте, а округление не на том шаге её усугубляет. Руководство по округлению валют показывает, откуда берётся погрешность.FormatString— например,"\€#,0.00","\¥#,0". Понадобится дальше для динамических строк формата.
Оба столбца следуют стандарту ISO 4217, а не чему-то, что Power BI знает изначально; в справочнике ISO 4217 есть полная таблица. Отметьте эту таблицу как измерение, свяжите её с FxRates[Currency] как «один ко многим» и используйте именно её — а не таблицу курсов — источником для среза.
Путь A: конвертация на импорте (быстро, скучно, правильно)
Для сценария 1 — много валют на входе, одна на выходе — сделайте работу в Power Query и позвольте модели хранить одно чистое число.
- Загрузите запрос транзакций в Power Query.
- Главная → Объединить запросы, соединив
Transactions[Currency]сFxRates[Currency](левое внешнее). - Разверните объединённый столбец и оставьте
Rate. - Добавить столбец → Настраиваемый столбец:
= if [Currency] = "USD" then [Amount]
else if [Rate] = null then null
else [Amount] / [Rate]Обратите внимание на ветку null. Левое внешнее соединение с таблицей курсов, где какой-то валюты нет, даёт null, а null в арифметике Power Query молча даёт null вместо ошибки — это превращается в пустое значение в визуале и в итог, который незаметно занижен. Сделайте пробел явным, чтобы его можно было отфильтровать и увидеть.
Обратите внимание и на деление. USD_EUR = 0.9215 означает, что один USD покупает 0,9215 EUR, поэтому перевод суммы в EUR в USD — это деление. Перевод суммы в USD в EUR — умножение. Перепутать направление — вторая по частоте ошибка в мультивалютных отчётах, и при курсах около 1,0 она почти незаметна: погрешность в 3% в цифре EUR/USD выглядит как расхождение округления, пока кто-нибудь не начнёт сверку.
Путь B: конвертация на запросе в DAX
Для сценария 2 — одна валюта хранения и валюта отображения, выбираемая пользователем — конвертация должна происходить в мере.
Наивная версия делает LOOKUPVALUE на каждую строку и становится медленной уже после нескольких сотен тысяч строк. Сначала агрегируйте, потом конвертируйте один раз:
Sales (Reporting Currency) =
VAR SelectedCurrency = SELECTEDVALUE ( Currency[Currency], "USD" )
VAR Rate =
CALCULATE (
SELECTEDVALUE ( FxRates[Rate] ),
FxRates[Currency] = SelectedCurrency
)
VAR Result =
IF (
SelectedCurrency = "USD",
[Sales Amount],
[Sales Amount] * Rate
)
RETURN
IF ( ISBLANK ( Rate ) && SelectedCurrency <> "USD", BLANK (), Result )Две детали, которые важнее, чем кажутся:
SELECTEDVALUEсо значением по умолчанию. Без запасного"USD"мера возвращает пустоту всякий раз, когда валюта не выбрана, — а это ровно то состояние, в котором отчёт открывается.- Явная защита от пустоты. Если у валюты нет курса, возвращайте пустоту намеренно, а не позволяйте
[Sales Amount] * BLANK()вернуть ноль. Ноль в карточке выручки — это ложь; пустота — видимый пробел.
Когда курс меняется со временем
Мера выше применяет один текущий курс ко всему набору данных. Это верно для вопроса «сколько стоила бы прошлогодняя выручка сегодня» и неверно почти для всего остального. Если в таблице курсов одна строка на валюту на день, сгруппируйте по дате перед конвертацией:
Sales (Historical Rates) =
SUMX (
VALUES ( 'Date'[Date] ),
VAR DayRate =
CALCULATE (
SELECTEDVALUE ( FxRates[Rate] ),
FxRates[Currency] = SELECTEDVALUE ( Currency[Currency], "USD" )
)
RETURN
[Sales Amount] * DayRate
)Итерация по VALUES('Date'[Date]), а не по таблице фактов, держит итератор маленьким — это дни, а не транзакции.
Динамические строки формата
Сконвертированное число с жёстко зашитым префиксом $ хуже, чем вообще без символа. В Power BI задайте формат меры как динамический и передайте выражение:
SELECTEDVALUE ( Currency[FormatString], "#,0.00" )Теперь карточка, показывающая ¥, покажет ¥ без десятичных знаков и без второй меры. Раньше для этого требовались группы вычислений в Analysis Services; динамические строки формата для мер принесли это в сам Power BI.
Какой курс вам на самом деле нужен
Это вопрос, отделяющий дашборд от отчёта, который подпишет финансовая служба, и ни один API не ответит на него за вас.
- Спот-курс на дату транзакции — для учёта отдельных операций. Максимальная точность, самая большая таблица курсов.
- Среднее за месяц или период — стандарт для статей отчёта о прибылях и убытках как по МСФО (IAS) 21, так и по ASC 830. Сглаживает внутримесячную волатильность и используется в большинстве консолидаций.
- Курс на конец периода — для статей баланса: денежные средства, дебиторская и кредиторская задолженность.
- Бюджетный или плановый курс — фиксированный курс на весь год, чтобы анализ отклонений отделял операционные результаты от валютного движения.
Серьёзной модели часто нужны два-три таких курса одновременно — как отдельные столбцы одной таблицы (SpotRate, AverageRate, ClosingRate), а не как разные таблицы. Если ваш отчёт питает что-то, что попадает в отчётность, руководство по курсам и налоговой отчётности разбирает, какой источник и какую метку времени вы должны уметь обосновать, а руководство по историческим курсам — как получать курсы на дату, а не текущие.
Выходные, из-за которых итоги тихо ломаются
Валютные рынки закрываются. В ежедневной таблице курсов, построенной из живого API, нет ни субботы, ни воскресенья, ни 25 декабря. Соедините транзакцию, датированную субботой, с такой таблицей — получите null, null станет пустотой, а пустота превратится в итог, заниженный ровно на выходные продажи.
Чините это в таблице курсов, а не в мере. Сгенерируйте полный список дат и заполните вниз:
let
Dates = List.Dates(#date(2026,1,1), Duration.Days(Date.From(DateTime.LocalNow()) - #date(2026,1,1)) + 1, #duration(1,0,0,0)),
DateTable = Table.FromList(Dates, Splitter.SplitByNothing(), {"Date"}),
Typed = Table.TransformColumnTypes(DateTable, {{"Date", type date}}),
Joined = Table.NestedJoin(Typed, {"Date"}, RateHistory, {"Date"}, "r", JoinKind.LeftOuter),
Expanded = Table.ExpandTableColumn(Joined, "r", {"Currency", "Rate"}),
Filled = Table.FillDown(Expanded, {"Currency", "Rate"})
in
FilledTable.FillDown переносит пятничный курс через выходные — это общепринятая трактовка и, что важнее, трактовка заявленная, а не случайная. Отсортируйте по валюте и дате перед заполнением, иначе через пробел перенесётся курс другой валюты.
Если ваш тариф не включает исторические эндпоинты, историю можно наращивать вперёд: при каждом обновлении дописывайте сегодняшние курсы в сохранённую таблицу — подойдут поток данных или таблица Fabric Lakehouse — и через квартал у вас будет настоящий временной ряд. Это не задним числом, но стоит одного вызова API в день.
Расписание обновлений и арифметика квоты
Power BI Pro допускает 8 запланированных обновлений в день для семантической модели; ёмкости Premium и Fabric — 48. Это и есть число, которое должна покрыть ваша квота API, и арифметика тут добрее, чем принято думать.
Таблица курсов выше — это два вызова на обновление: один к /v1/currencies, один к /v1/convert. Итого:
| Частота обновления | Обновлений в месяц | Вызовов API в месяц | Тариф Finexly |
|---|---|---|---|
| 8 в день (максимум Pro) | ~240 | ~480 | Бесплатный (1 000/мес) |
| 48 в день (Fabric, каждые 30 мин) | ~1 440 | ~2 880 | Starter |
| 48 в день + почасовая история в потоке данных | ~2 160 | ~4 320 | Growth |
Потолок в 10 в минуту — вот за чем стоит следить. Если вы соберёте запрос, вызывающий /v1/rate по разу на валюту внутри Table.AddColumn, двадцать валют дадут двадцать вызовов за пару секунд и серию ответов 429 посреди обновления. Именно поэтому и существует многопарный вызов /v1/convert. Группируйте и кэшируйте: руководство по кэшированию и обработке ошибок описывает схемы повторов с экспоненциальной задержкой, которые в равной мере применимы к запланированному обновлению.
Шлюзы, Excel и Fabric
Несколько замечаний по среде, каждое из которых экономит полдня:
- Шлюз не нужен. Облачный REST API — не локальный источник, поэтому локальный шлюз данных для этого не требуется. Если обновление падает и кто-то предлагает поставить шлюз, это почти всегда ошибка динамического источника в другом обличье.
- Excel использует тот же движок. Power Query в Excel принимает ровно тот же M. Если ваша аудитория живёт в книгах, а не в дашбордах, руководство по курсам в Excel разбирает
WEBSERVICE,LAMBDAи матрицу версий; есть и аналог для Google Таблиц. - Fabric Dataflow Gen2 — лучшее место для таблицы курсов, как только она нужна более чем одному отчёту. Загрузите курсы один раз, дайте всем семантическим моделям читать одну и ту же таблицу — и расход API перестанет расти вместе с числом отчётов.
- Сверьтесь с известной цифрой перед публикацией. Возьмите одну пару в конвертере валют и сравните с тем, что показывает ваша модель для той же пары на тот же момент. Если расходятся — у вас проблема направления или округления, и найти её лучше сейчас, а не на заседании совета директоров.
Часто задаваемые вопросы
Может ли Power BI конвертировать валюты без API? Может, если курсы предоставите вы: таблица, которую ведут вручную, выгрузка из финансовой системы или представление в базе данных. Встроенного источника курсов в Power BI нет. API нужен тогда, когда курсы должны обновляться без того, чтобы кто-то помнил об этом.
Почему отчёт с конвертацией обновляется в Power BI Desktop, но падает в службе?
Почти всегда из-за ошибки динамического источника данных. Ваш вызов Web.Contents собирает URL склейкой строк. Перенесите переменные части в параметры RelativePath и Query, чтобы базовый URL был статическим, опубликуйте заново и введите учётные данные снова.
Где конвертировать — в Power Query или в DAX? В Power Query, если у отчёта одна валюта отчётности: быстрее и проще. В DAX, если пользователь выбирает валюту во время работы. Если нужно и то и другое — приведите всё к опорной валюте в Power Query, а меру DAX положите сверху.
Сколько запросов к API тратит одно обновление Power BI?
Два, если вы объединяете все пары в один вызов /v1/convert. При максимуме Power BI Pro в 8 обновлений в день это около 480 запросов в месяц — в пределах бесплатного тарифа. Дорого становится, только если вызывать API по разу на валюту или по разу на строку.
Как обрабатывать выходные и праздники в ежедневной таблице курсов? Сформируйте непрерывную таблицу дат, присоедините курсы левым соединением, отсортируйте по валюте и дате и заполните вниз. Пятничный курс переносится на выходные. Важно, чтобы такая трактовка была осознанной и задокументированной, а не чтобы строки молча исчезали.
Какой курс использовать для финансовой отчётности? Средние курсы периода — для статей отчёта о прибылях и убытках, курсы на конец периода — для статей баланса, и по IAS 21, и по ASC 830. Храните их как отдельные столбцы одной таблицы курсов, чтобы отчёт переключался между ними без изменения модели.
Готовы поставить живые курсы за своими дашбордами? Получите бесплатный ключ API Finexly — без банковской карты. Начните с 1 000 запросов в месяц: этого достаточно, чтобы обновлять рабочую область Power BI Pro на максимальной частоте, — и переходите на старший тариф, когда понадобятся исторические данные или более частое обновление. Если вы всё ещё сравниваете поставщиков, страница сравнения показывает варианты рядом.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →