Будь осторожен! Это о деньгах!

У компании, в которой я когда-то работал, есть слоган: «Мы — технологическая компания с банковской лицензией». Хотя это звучит как реклама компании, на самом деле в какой-то степени это было правдой. В последние годы Python использовался во многих областях, включая, помимо прочего, анализ данных, обнаружение мошенничества, прогнозирование поведения пользователей и т. д., которые широко используются в финансовых учреждениях. Он также используется торговыми платформами, такими как Quantopian.

В этой статье я хотел бы поделиться несколькими советами по работе с финансовыми данными с помощью Python, основанными на моем личном опыте. Некоторые требуют вашего особого внимания, иначе вы можете потерять большие деньги! Этот список ни в коем случае не является полным, не стесняйтесь дополнять его, оставляя свои комментарии ниже.

Будьте осторожны с нулевыми суммами и пустыми суммами

В 99% случаев вам нужно работать со специальными суммами, такими как 0 или пустые суммы в финансовом учреждении. Между разработчиками и финансовыми экспертами всегда будет дискуссия о том, как с ними справиться. Должны ли мы их игнорировать, оставить или вызвать ошибку?

Помимо знания о существовании этих особых случаев, мы также должны понимать разницу между 0 и пустым и способ их обработки. Например, в моей ситуации недействительна только пустая сумма, которая должна вызывать ошибку, остальное допустимо. Тогда я бы легко получил такой код:

if not amount:
    logger.error("Received an invalid amount")

Однако этот код имеет потенциальный риск. В Python есть пара значений, рассматриваемых как False, включая 0 и None. Это означает, что сумма 0 также вызовет здесь исключение, а это не то, что нам нужно!

Более безопасный способ — явно указать недопустимые значения, например:

if amount in [None, '']:
    logger.error("Received an invalid amount")

Будьте осторожны с числами с плавающей запятой, они не точны

Если вы откроете интерпретатор Python и наберете 1.2-1.0, вы получите что-то вроде этого:

Если вы спросите 10-летнего ребенка, он скажет вам, что это неправильно. Как Python мог допустить такую ​​«ошибку»? Как рассчитывается это странное число? Почему небольшая разница? Программы, работающие с большими объемами конфиденциальных чисел, не должны мириться с этой разницей. Эта проблема также вызовет несоответствие во время теста.

Это не ошибка в Python. Это больше связано с тем, как число с плавающей запятой представлено в базовой системе. Число с плавающей запятой представлено аппаратно в виде двоичных дробей. К сожалению, не все десятичные части могут быть точно представлены в двоичном формате. Например, 0,125 можно представить в двоичном виде с конечными цифрами:

0011111

Но это не относится к 0,2. Это будет примерно так:

00111110010011001100110011001101...

Python не будет хранить бесконечно повторяющиеся дроби. Независимо от того, сколько цифр мы сохраняем, результат никогда не будет точным. Вы можете увидеть фактическое значение, хранящееся в Python, следующим образом:

>>> "{0:.20f}".format(1.2)
'1.19999999999999995559'

Есть сайт, посвященный этой теме. Вы также можете прочитать Python doc.

Но как нам решить эту проблему? Простое решение — использовать тип Decimal. Decimal предназначен для решения проблемы неточности поплавка. Он хранит числа в базе 10, и вы можете контролировать уровень точности. Поскольку он не хранится в двоичном формате, Decimal относительно менее эффективен, чем float.

Компьютеры должны обеспечивать арифметику, которая работает так же, как арифметика, которую люди изучают в школе.

Если мы проделаем тот же расчет, используя Decimal, на этот раз мы получим правильный результат. Пожалуйста, помните, вам нужно поставить строку '1.2' вместо числа с плавающей запятой 1.2, иначе Decimal преобразует число с плавающей запятой в его эквивалентные десятичные числа, таким образом используя неправильное число с самого начала.

>>> Decimal('1.2')-Decimal('1.0')
Decimal('0.2')
>>> Decimal(1.2)-Decimal(1.0)
Decimal('0.1999999999999999555910790150')

Ознакомьтесь с этой статьей, чтобы увидеть сравнение производительности чисел с плавающей запятой и десятичной дроби.



Какой метод округления использовать?

Как ответственный разработчик программного обеспечения, вы должны дважды подумать при работе с неоднозначными данными. При работе с деньгами нам часто приходится округлять суммы. Округление чисел приведет к потере точности, вы должны знать об этой проблеме и ее влиянии на огромный набор данных. Никогда не недооценивайте силу округления. В начале 1980 года индекс Ванкуверской фондовой биржи был усечен до трех знаков после запятой по каждой сделке вместо правильного округления. Накопившиеся усечения привели к огромной потере 25 баллов в месяц. Ошибка была окончательно исправлена ​​в 1983 году, и значение было изменено с 524,811 на 1098,892.

Существует множество стратегий округления. Здесь нет правильного и неправильного. Самое главное — понять принцип каждой стратегии и ее побочные эффекты.

  • Округление вверх: округляется до наименьшего целого числа, которое не меньше входного значения.
  • Округление в меньшую сторону: округляется до наибольшего целого числа, которое не больше входного значения.
  • Округление до половины: 0,5 называется ничьей. Дроби, равные или превышающие 0,5, всегда округляются в большую сторону. Это наиболее известная стратегия округления.
  • Округление наполовину в меньшую сторону: дроби, равные или меньшие 0,5, округляются в меньшую сторону.

Как насчет отрицательных чисел?

Вышеупомянутые стратегии округления имеют одну общую проблему: смещение округления. Округление (половина) вверх дает округление в сторону положительной бесконечности, а округление (половина) вниз дает округление в сторону отрицательной бесконечности. Это означает, что любое число, включая отрицательные, будет округлено до большего значения и округлено до меньшего значения.

Это приводит к другому побочному эффекту: результат несимметричен относительно нуля.

В этом примере 1,5 округляется до 2, а -1,5 округляется до -1.

Есть ли у нас идеальное решение?

Чтобы уменьшить погрешность округления, была изобретена стратегия округления от половины до четного. Этому я никогда не учился в старшей школе, однако это стандартное правило округления в стандарте IEEE-754. Эта стратегия предполагает, что возможности округления в большую или меньшую сторону в наборе данных равны. Таким образом, необходимо округлить связи (0,5) до ближайшего четного числа с желаемой точностью. Между тем, он сохраняет результаты симметричными относительно нуля.

Это также стратегия, используемая методом round() в Python.

>>> round(1.5)
2
>>> round(2.5)
2
>>> round(-1.5)
-2
>>> round(-2.5)
-2

Однако поведение round() для чисел с плавающей запятой иногда может сбивать с толку. В этом примере 3,675 округляется до 3,67 вместо 3,68. Это снова связано с предыдущим пунктом о неточности числа с плавающей запятой. 3.765 не может быть точно сохранен в операционной системе, что приводит к такому неожиданному результату. Использование класса Decimal может решить эту проблему.

>>> round(3.675,2)
3.67
>>> round(Decimal('3.675'), 2)
Decimal('3.68')

У округления от половины до даже есть другое интересное название: Округление банкиров. Обычно оно используется банкирами, как вы можете понять по его названию. Основная причина в том, что он беспристрастен, как я только что объяснил. Забавный факт, нет четких доказательств того, что это когда-либо было стандартом в банковской сфере.

Как правильно округлять числа в Python?

Помимо round(), есть еще 2 встроенных модуля, обеспечивающих различные стратегии округления. math.ceil() выполняет только округление в большую сторону, math.floor() выполняет только округление в меньшую сторону.

Еще одним мощным модулем является Decimal не только из-за его точного представления числа с плавающей запятой, но и из-за возможности легкого переключения на другую стратегию округления. Однако название некоторых стратегий округления в Decimal противоречит нашему предыдущему объяснению. Чтобы помочь вам быстро найти реализацию каждой стратегии округления на Python, я создал эту таблицу.

Многие мои идеи взяты из этой замечательной статьи от Real Python:



Обрабатывайте финансовые данные в правильном часовом поясе

Все мы знаем, что время – деньги! Мы не хотим терять деньги, потому что забыли включить систему 29 февраля 2020 года. На самом деле сегодня (31 октября 2021 года) это день, когда мы переключаемся на центральноевропейское время (CET) с центральноевропейского летнего времени (CEST), а именно от UTC+2 до UTC+1, что также является хорошим моментом для проверки настроек времени вашего приложения.

Различайте часовой пояс системы и часовой пояс вашего приложения

Ваше приложение может работать в часовом поясе, отличном от часового пояса системы. Обычно сервер Linux работает в часовом поясе UTC. Если ваше приложение работает в отдельном контейнере (например, в докере), вы можете принудительно запустить его в другом часовом поясе.

Как управлять часовым поясом в Python?

В Python объект даты может быть определен с часовым поясом или без него. Дата без часового пояса называется Naive, а дата с часовым поясом называется Aware. По умолчанию объект данных в Python имеет значение Naive.

>>> datetime.now()
datetime.datetime(2021, 10, 31, 22, 6, 34, 626859)

Модуль datetime предлагает класс timezone, представляющий часовой пояс, определяемый фиксированным смещением от UTC.

>>> from datetime import datetime, timezone, timedelta
>>> datetime.now(timezone(timedelta(hours=+2)))
datetime.datetime(2021, 10, 31, 23, 15, 28, 965657, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)))

С этим классом timezone есть проблема, заключающаяся в том, что вы можете определить только фиксированное смещение. Затем приложение обязано переключиться на другое смещение в случае перехода на летнее время. В документе Python также упоминается, что:

Правила корректировки времени во всем мире носят скорее политический, чем рациональный характер, они часто меняются, и нет единого стандарта, подходящего для всех приложений, кроме UTC.

Но есть ли решение?

Конечно, благодаря этому замечательному сообществу Python у нас есть сторонняя библиотека pytz, которая переносит базу данных Olson tz в Python. Можно определить часовой пояс по его местоположению (например, Европа/Амстердам), а не только по смещению в жестком коде. Библиотека автоматически настроит время в конце летнего дня.

>>> from pytz import timezone
>>> from datetime import datetime
>>> amsterdam = timezone('Europe/Amsterdam')
>>> datetime.now(amsterdam)
datetime.datetime(2021, 10, 31, 22, 30, 26, 451405, tzinfo=<DstTzInfo 'Europe/Amsterdam' CET+1:00:00 STD>)

Как насчет национальных праздников?

Финансовые учреждения, такие как банки, иногда приостанавливают свою деятельность на выходные или национальные праздники. Python datatime может сообщить нам день недели для даты, где понедельник равен 0, а воскресенье — 6, чтобы определить, является ли сегодня выходным.

>>> import datetime
>>> datetime.datetime.today().weekday()
6 # today is Sunday

Но это вызов, чтобы узнать все национальные праздники каждой страны. Сообщество Python разработало библиотеку holidays, в которую включены национальные праздники многих стран. Вот пример Дня короля в Нидерландах. Такая библиотека помогает нам автоматически управлять праздниками. Тем не менее, стоит проверить, включены ли все праздники, которые вы хотите, прежде чем использовать их в рабочей среде.

>>> from datetime import date
>>> import holidays
>>> date(2021,4,27) in holidays.NL()
True
>>> holidays.NL(years=2021)
{datetime.date(2021, 1, 1): 'Nieuwjaarsdag', datetime.date(2021, 4, 4): 'Eerste paasdag', datetime.date(2021, 4, 2): 'Goede Vrijdag', datetime.date(2021, 4, 5): 'Tweede paasdag', datetime.date(2021, 5, 13): 'Hemelvaart', datetime.date(2021, 5, 23): 'Eerste Pinksterdag', datetime.date(2021, 5, 24): 'Tweede Pinksterdag', datetime.date(2021, 12, 25): 'Eerste Kerstdag', datetime.date(2021, 12, 26): 'Tweede Kerstdag', datetime.date(2021, 4, 27): 'Koningsdag'}

Вы когда-нибудь слышали о праздничных днях?

Так же, как и банковское округление, еще одна терминология, связанная с банком, — банковский выходной. Это рабочий день, когда финансовые учреждения закрыты. В основном это относится к физическим отделениям, потому что онлайн-сервисы продолжают работать. Обычно национальные праздники в holidays библиотеке уже покрывают государственные праздники, поэтому в другой библиотеке нет необходимости. Но это не может остановить сообщество от разработки новых вещей, таких как говук-банк-праздники.

Используйте кортеж, чтобы избежать изменения атрибутов

Целостность данных всегда была важной темой в финансовых учреждениях. Это очень обширная и сложная тема. Для простоты в этом контексте мы имеем в виду только «не изменять содержание исходных данных». Примером может быть API-интерфейс, который получает платежный запрос от клиента, который включает информацию об отправителе, получателе, сумме, счете и т. д. Предполагается, что этот API-интерфейс передает эту информацию в другую систему, не изменяя ничего в этом запросе.

Один из способов избежать этой проблемы с технической точки зрения — сделать объект платежного запроса неизменяемым с помощью кортежа и класса данных. По умолчанию кортеж является неизменяемым, и вы также можете установить неизменяемый объект класса данных.

Вот пример namedtuple. Вы можете сопоставить каждую строку с объектом NamedTuple. Если вы попытаетесь изменить атрибут, вы получите исключение AttributeError.

Вы можете сделать то же самое в классе данных с помощью frozen=True, но вы не можете легко сопоставить строку с объектом класса данных. Вот как вы можете сделать это в классе данных.

Если вам интересно узнать об NamedTuple и классе данных, не стесняйтесь читать мою предыдущую статью.



Знайте силу доходности

Когда объем данных достигает определенного уровня, вы начинаете беспокоиться о проблеме с производительностью. Этот небольшой трюк заставляет программу более разумно использовать память. Представьте, что вы хотите узнать перестановки списка: [1,2,3,4]. Вы можете создать функцию calculate_permutations() и вернуть все 24 комбинации.

Иногда вам не обязательно знать их все сразу, а интересует только следующая комбинация. Затем вы можете использовать yield для экономии памяти.

Функция calculate_permutations_return() возвращает список, а calculate_permutations_yield() возвращает генератор. Оба являются итерируемыми, которые можно повторять. Разница в том, что генератор — это своего рода итерация, которую вы можете выполнять только один раз. Результат вычисляется на лету. Вы можете ясно видеть разницу в размерах этих двух функций. Сам объект-генератор намного меньше полного результата и не увеличивается с количеством элементов.

Это также связано с концепцией Lazy Evaluation в Python. Если вы хотите погрузиться глубже, вы можете прочитать одну из моих статей:



Python на самом деле заботится о таких вычислениях во встроенном модуле itertools. Этот модуль создает множество итераторов для эффективного цикла. Вы можете легко получить такие перестановки:

import itertools
print(itertools.permutations([1, 2, 3, 4]))

Заключение

Вот 5 советов Python по работе с финансовыми учреждениями. Надеюсь, вы найдете их полезными. Я уверен, что не затронул все интересные моменты. Пожалуйста, оставьте свои комментарии ниже, если у вас есть идеи, чтобы поделиться с нами.