Монада инвестиции: простой язык описания и симуляции инвестиций
Можно просто посмотреть пример.
Краткое описание⌗
Накатал забавную Haskell-библиотечку для моделирования финансового поведения: инвестиций, личных трат, выплат, торговли на бирже, и прочих алгоритмов продолжительных и
условных накоплений и передвижений средств. Смысл простой: тип Investment
является синонимом специализации трансформера монад StateT
на монаду IO
:
-- | The investment monad type
type Investment a r = StateT (InvestmentState a) IO r
При этом тип InvestmentState
, описывающий текущее состояние инвестиции (или чьего-то кошелька и долгов…), определён так:
-- | Investment state
data InvestmentState a = InvestmentState
{ inputSum :: a
-- ^ The deposit, real money behind the investment
, outputSum :: a
-- ^ Withdraw account, the earnings from the investment
, balanceSum :: a
-- ^ The internal money, currently accumulated as a result of the investment (subject to withdraw)
, startTime :: UTCTime
-- ^ Investment creation date
, currentTime :: UTCTime
-- ^ Current date
, logBook :: [String]
-- ^ Log of funds movements, etc.
}
Как видно, InvestmentState
параметризуется неким типом: он описывает концепт ‘средств’, с которыми работает данная инвестиция – конкретную реализацию денег,
каких-то контрактов или иных ценностей.
Внутри инвестиции предусмотрено три счёта:
- Для учёта количества (внешних, абстрактных) денег, вложенных в инвестицию.
- Для учёта количества денег, выведенных (во вне) из инвестиции.
- Для учёта текущего баланса инвестиции.
Я рассуждал следующим образом: внутри инвестиции происходят какие-то процессы, которые то увеличивают, то уменьшают баланс. В какие-то определённые моменты времени можно вывести часть средств с баланса или, наоборот, внести дополнительный депозит (например, реинвестировать часть выведенных средств). В конце концов, мы анализируем инвестицию после прогона и смотрим разницу между депозитом и выводом средств – это и будет наш профит.
Средства в инвестиции обладают следующими свойствами:
-- | Types that can be treated as Funds
class (Eq a, Show a, Ord a, Fractional a) => Funds a
instance Funds Double
Операции в монаде Investment⌗
Просто приведу сигнатуры операций, доступных в монаде Investment
:
-- | Withdraw funds (transfer from buffer balance to the output sum), returns True on success
withdraw :: Funds a => a -> Investment a Bool
-- | Deposit funds (increase 'inputSum' indicating that even more money has been invested)
deposit :: Funds a => a -> Investment a ()
getDeposit :: Funds a => Investment a a
getEarnings :: Funds a => Investment a a
getBalance :: Funds a => Investment a a
-- | Get current UTC time from the investment
getUTCTime :: Investment a UTCTime
-- | Get current time: 24-hour and minute string (i.e. "13:15")
getHourAndMinute :: Investment a String
-- | Synonym for 'getHourAndMinute'
getTime = getHourAndMinute
-- | Get current month number (1..12)
getMonthNum :: Investment a Int
-- | Get current month name ("January")
getMonth :: Investment a String
-- | Get current month day num (1..31)
getMonthDay :: Investment a Int
-- | Get current day and month string ("27.01")
getDayAndMonth :: Investment a String
-- | Synonym for 'getDayAndMonth'
getDate = getDayAndMonth
-- | Get current full date ("27.01.2017")
getFullDate :: Investment a String
-- | Get current weekday name ("Sunday")
getWeekDay :: Investment a String
-- | Synonym for 'getWeekDay'
getToday = getWeekDay
-- | Append a string to the financeLog (prepends it with the current time)
writeLog :: String -> Investment a ()
Начальное состояние инвестиции и симуляция⌗
Симуляция устроена достаточно примитивно и представляет собой многочисленное повторение данного кода Investment
с обновлением “текущего времени” на каждом шаге.
Секунды не учитываются; часы “тикают” вперёд на одну минуту: например, симуляция одного часа инвестиции состоит ровно из 60 итераций
(симуляция 6 месяцев запустит код 4320 раз). Функции для запуска симуляции:
-- | Eval the investment monad for N minutes, giving back the changed state
evalInvestmentMinutes :: Integer -> Investment a r -> InvestmentState a -> IO (InvestmentState a)
-- | Run the investment monad N hours
evalInvestmentHours n = evalInvestmentMinutes (n * 60)
-- | Run the investment monad N days
evalInvestmentDays n = evalInvestmentHours (n * 24)
Начальная структура InvestmentState
конструируется из стартовых трёх счетов (вложенное бабло, выведенное, баланс) и времени UTCTime
, которое следует считать за точку
отсчёта (открытия инвестиции):
-- | Construct new investment state
makeInvestmentState :: Funds a => a -> a -> a -> UTCTime -> InvestmentState a
Пример не по назначению: скучная жизнь офисного работника⌗
В модуле Finance.Investment.DullLife
я поместил простенький пример описания и симуляции “финансовой жизни” некоторого офисного работника.
Джо получает зарплату вечером 23-го числа каждого месяца, пьёт дорогие безалкогольные коктейли каждую пятницу, по нечётным дням ходит в магазин за продуктами и ежемесячно платит аренду за квартиру. Раз в год у него день рождения и в этот день он закатывает большую вечерину с блэк-джэком и шлюхами:
salary = do
dn <- getMonthDay
t <- getTime
if dn == 23 && t == "18:00"
then do
balancePlus 72000
writeLog $ "Salary, 72000"
else balanceSame
birthday = do
d <- getDayAndMonth
t <- getTime
if d == "07.04" && t == "20:00"
then do
balanceMinus 10000
writeLog "Organised birthday party (10000)"
else balanceSame
bars = do
d <- getToday
t <- getTime
if d == "Friday" && t == "21:00"
then balanceMinus 3000
else balanceSame
supermarkets = do
d <- getMonthDay
t <- getTime
if odd d && t == "20:00"
then balanceMinus 1500
else balanceSame
rent = do
d <- getMonthDay
t <- getTime
if d == 22 && t == "19:00"
then do
balanceMinus 23000
writeLog $ "Paid apartment rent (23000)"
else balanceSame
dullLife :: Investment Double ()
dullLife = do
salary
supermarkets
bars
birthday
rent
Довольно игрушечное описание (зато весёлое!), в котором, ко всему прочему, никак не используются депозиты (учёт вложенных средств) и выводы (учёт выведенных средств, в некотором смысле, прибыль). Все активности при этом происходят только с балансом, суммы фиксированы (например, 1500 на поход в магазин).
Вот результат в GHCi, скучная жизнь Джо со стартовым балансом 20к:
λ> t <- getCurrentTime
t :: UTCTime
λ> ist <- evalInvestmentDays 180 dullLife (makeInvestmentState 0 0 20000 t)
ist :: InvestmentState Double
λ> putStrLn $ readLog ist
Log book created at 2017-01-23 23:08:42.04224 UTC
22.02.2017, Wednesday, 19:00: Paid apartment rent (23000)
23.02.2017, Thursday, 18:00: Salary, 72000
22.03.2017, Wednesday, 19:00: Paid apartment rent (23000)
23.03.2017, Thursday, 18:00: Salary, 72000
07.04.2017, Friday, 20:00: Organised birthday party (10000)
22.04.2017, Saturday, 19:00: Paid apartment rent (23000)
23.04.2017, Sunday, 18:00: Salary, 72000
22.05.2017, Monday, 19:00: Paid apartment rent (23000)
23.05.2017, Tuesday, 18:00: Salary, 72000
22.06.2017, Thursday, 19:00: Paid apartment rent (23000)
23.06.2017, Friday, 18:00: Salary, 72000
22.07.2017, Saturday, 19:00: Paid apartment rent (23000)
it :: ()
λ> balanceSum ist
17500.0
it :: Double
В “финансовом журнале” инвестиции хранятся записи о всех важных событиях (писать в лог с writeLog
), депозиты и выводы средств отмечаются в нём автоматически. Например,
в примере с Джо можно было бы отмечать в журнале просадку, то есть нехватку средств на поход в магазин или невозможность заплатить за квартиру.
Разумеется, в настоящем описании Investment
будет задействован весь потенциал библиотеки. Мы в IO
монаде, а значит запросто можем использовать внешние источники
получения дополнительной информации на каждом шаге: случайность, считывание/анализ данных (например, котировок) с сервера, чтение кофигурационных файлов или БД, ввод-вывод
на терминал или в GUI вопросов о принятии тех или иных дальнейших инвестиционных решений при достижении/наступлении определённых условий и т.д.
Библиотечка доступна на github и будет добавлена в Hackage вскоре после тестирования и (пере)обдумывания экспортируемого API.