1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# Copyright © 2022, 飞麦 <[email protected]>, All rights reserved. # frozen_string_literal: true require 'date' # IRC = Internal Rate of Change, 内部变化率, 资产(含后续投入、抽回)基于货币的时间价值的真实变化率. # IRR = Internal Rate of Return, 内部收益率, 资产(含后续投入、抽回)基于货币的时间价值的真实收益率. # IRR = IRC - 1.0 # 参见: https://www.investopedia.com/terms/i/irr.asp # 现金价值: 保险术语; 净值: 基金术语; 本质均为资产在特定时间的可变现价值. # 日收益率 = 日变化率 - 1.0 # 年收益率 = 年变化率 - 1.0 # 年变化率 = 日变化率**365.2425 # 本模块的 xirr 函数类似于 Excel 的 xirr 函数 # 现金价值盈亏, 试算多少, 日变化率, 日变化率上限, 日变化率下限, 盈利时上限锁定或亏损时下限锁定, 是否退出迭代 MidEx = Struct.new(:cv_cmp, :try_cmp, :daily_rate, :dr_up_limit, :dr_down_limit, :limit_lock, :quit_iter) # 内部日变化率计算模块(日收益率 = 日变化率 - 1.0) module Xirr module_function # 根据不定期不定额资金收(+)支(-)情况,递交各日期之间的天数与收(+)支(-)金额 def gen_inout_series(tday_inout_a) pday = nil tday_inout_a.each do |tday, inout| tday = Date.parse(tday) unless tday.is_a?(Date) day_num = pday ? (tday - pday).round : 0 yield day_num, inout pday = tday end end # 根据假设的日变化率试算现金价值 def try_cash_value(tday_inout_a, daily_rate) try_cv = 0.0 gen_inout_series(tday_inout_a) do |day_num, inout| try_cv *= daily_rate**day_num try_cv -= inout end try_cv end # 根据不定期不定额资金收(+)支(-)情况及现金价值,计算整体是盈利(1)、亏损(-1)还是持平(0) def compare_cash_value(tday_inout_a) static_cv = 0.0 # 忽略现金的时间价值时的结果 tday_inout_a.each { |_tday, inout| static_cv += inout } raise "Invalid static_cv=#{static_cv}" unless static_cv.finite? static_cv <=> 0.0 end # 日变化率调整比率 DAY_RATE_ADJUST = 1.001 # 根据试算比较情况,设定日成长率的上下限 def decide_rate_limit(mid) case mid.cv_cmp when 1 mid.dr_down_limit = 1.0 mid.dr_up_limit = 1.0 * DAY_RATE_ADJUST when -1 mid.dr_down_limit = 1.0 / DAY_RATE_ADJUST mid.dr_up_limit = 1.0 when 0 mid.dr_down_limit = mid.dr_up_limit = 1.0 else raise "Invalid mid.cv_cmp=#{mid.cv_cmp}" end end # 盈利时根据试算结果调整 def win_adjust(mid) case mid.try_cmp when 1 # 试算现金价值偏多 mid.dr_up_limit = mid.daily_rate mid.limit_lock = true # 上限锁定了 when -1 # 试算现金价值偏少 mid.dr_down_limit = mid.daily_rate # 上限未锁定时继续放大 mid.dr_up_limit *= DAY_RATE_ADJUST unless mid.limit_lock when 0 # 试算现金价值正好 mid.quit_iter = true else raise "Invalid mid.try_cmp=#{mid.try_cmp}" end end # 亏损时根据试算结果调整 def loss_adjust(mid) case mid.try_cmp when -1 # 试算现金价值偏少 mid.dr_down_limit = mid.daily_rate mid.limit_lock = true # 下限锁定了 when 1 # 试算现金价值偏多 mid.dr_up_limit = mid.daily_rate # 下限未锁定时继续缩小 mid.dr_down_limit /= DAY_RATE_ADJUST unless mid.limit_lock when 0 # 试算现金价值正好 mid.quit_iter = true else raise "Invalid mid.try_cmp=#{mid.try_cmp}" end end # 根据整体盈亏情况及试算结果, 决定日变化率的上下限 def adjust_limit(mid) case mid.cv_cmp when 1 # 盈利时变化率为正 win_adjust(mid) when -1 # 亏损时变化率为负 loss_adjust(mid) when 0 # 不盈不亏时无需迭代 mid.quit_iter = true else raise "Invalid mid.cv_cmp=#{mid.cv_cmp}" end end # 构造初始值 def gen_mid mid = MidEx.new # 初始为未锁定 mid.limit_lock = false # 初始化日变化率为不变 mid.daily_rate = 1.0 # 初始为未退出迭代 mid.quit_iter = false mid end # 计算并比较试算结果与现金价值 def try_and_compare(mid, tday_inout_a) # 设置 日变化率 = 日变化率上限与下限的几何平均 mid.daily_rate = Math.sqrt(mid.dr_up_limit * mid.dr_down_limit) # 根据假设的日变化率试算现金价值结果 try_cv = try_cash_value(tday_inout_a, mid.daily_rate) # 比较试算结果与现金价值 mid.try_cmp = try_cv <=> 0.0 end end # 内部日变化率计算模块(日收益率 = 日变化率 - 1.0) module Xirr module_function # 显示某行内容 def show(idx, tday, inout) "idx=#{idx} tday=#{tday} inout=#{inout}" end # 检查输入有效性 def check(tday_inout_a) pday = nil tday_inout_a.each_with_index do |(tday, inout), idx| tday = Date.parse(tday) unless tday.is_a?(Date) raise "Invalid tday: #{show(idx, tday, inout)}" unless tday.is_a?(Date) raise "Invalid inout: #{show(idx, tday, inout)}" unless inout.finite? raise "Invalid tday sequence: prev=#{pday}#{show(idx, tday, inout)}" if pday && tday < pday pday = tday end end # 计算不定期不定额资金收(+)支(-)情况下达到现金价值的内部变化率 # tday_inout_a 是个(日期、收支)的数组, 数组的每个元素也是数组, 包含: 日期、收支这两个元素 # 买入资产或追加买入资产为支(-), 抽回资产及最终现金价值(净值)为收(+) def xirc(tday_inout_a) # 检查输入有效性 check(tday_inout_a) # 构造初始值 mid = gen_mid # 根据不定期不定额资金收(+)支(-)情况及现金价值,计算整体是盈利(1)、亏损(-1)还是持平(0) mid.cv_cmp = compare_cash_value(tday_inout_a) # 根据盈亏情况决定日成长率的上下限 decide_rate_limit(mid) # 当日变化率的上下限之间仍然有双精度浮点数时不断迭代 while mid.dr_up_limit > mid.dr_down_limit.next_float # 计算并比较试算结果与现金价值 try_and_compare(mid, tday_inout_a) # 根据试算结果比较情况, 调整日变化率的上下限 adjust_limit(mid) break if mid.quit_iter end mid.daily_rate # 年变化率 = 日变化率**365.2425, 年收益率 = 年变化率 - 1.0 end # 计算不定期不定额资金收(+)支(-)情况下达到现金价值的内部收益率 # tday_inout_a 是个(日期、收支)的数组, 数组的每个元素也是数组, 包含: 日期、收支这两个元素 # 买入资产或追加买入资产为支(-), 抽回资产及最终现金价值(净值)为收(+) def xirr(tday_inout_a) xirc(tday_inout_a) - 1.0 end end |