Можно просто посмотреть пример.

Краткое описание

Накатал забавную 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 параметризуется неким типом: он описывает концепт ‘средств’, с которыми работает данная инвестиция – конкретную реализацию денег, каких-то контрактов или иных ценностей.

Внутри инвестиции предусмотрено три счёта:

  1. Для учёта количества (внешних, абстрактных) денег, вложенных в инвестицию.
  2. Для учёта количества денег, выведенных (во вне) из инвестиции.
  3. Для учёта текущего баланса инвестиции.

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

Средства в инвестиции обладают следующими свойствами:

-- | 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.