-
Notifications
You must be signed in to change notification settings - Fork 292
Expand file tree
/
Copy pathreverse_calculator.rb
More file actions
135 lines (112 loc) · 4.56 KB
/
Copy pathreverse_calculator.rb
File metadata and controls
135 lines (112 loc) · 4.56 KB
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
class Holding::ReverseCalculator
attr_reader :account, :portfolio_snapshot
def initialize(account, portfolio_snapshot:, security_ids: nil)
@account = account
@portfolio_snapshot = portfolio_snapshot
@security_ids = security_ids
end
def calculate
Rails.logger.tagged("Holding::ReverseCalculator") do
precompute_cost_basis
holdings = calculate_holdings
Holding.gapfill(holdings)
end
end
private
# Reverse calculators will use the existing holdings as a source of security ids and prices
# since it is common for a provider to supply "current day" holdings but not all the historical
# trades that make up those holdings.
def portfolio_cache
@portfolio_cache ||= Holding::PortfolioCache.new(account, use_holdings: true, security_ids: @security_ids)
end
def calculate_holdings
# Start with the portfolio snapshot passed in from the materializer
current_portfolio = portfolio_snapshot.to_h
previous_portfolio = {}
holdings = []
Date.current.downto(account.start_date).each do |date|
today_trades = portfolio_cache.get_trades(date: date)
previous_portfolio = transform_portfolio(current_portfolio, today_trades, direction: :reverse)
# If current day, always use holding prices (since that's what Plaid gives us). For historical values, use market data (since Plaid doesn't supply historical prices)
holdings.concat(build_holdings(current_portfolio, date, price_source: date == Date.current ? "holding" : nil))
current_portfolio = previous_portfolio
end
holdings
end
def transform_portfolio(previous_portfolio, trade_entries, direction: :forward)
new_quantities = previous_portfolio.dup
trade_entries.each do |trade_entry|
trade = trade_entry.entryable
security_id = trade.security_id
qty_change = trade.qty
qty_change = qty_change * -1 if direction == :reverse
new_quantities[security_id] = (new_quantities[security_id] || 0) + qty_change
end
new_quantities
end
def build_holdings(portfolio, date, price_source: nil)
portfolio.map do |security_id, qty|
next if @security_ids && !@security_ids.include?(security_id)
price = portfolio_cache.get_price(security_id, date, source: price_source)
if price.nil?
next
end
Holding.new(
account_id: account.id,
security_id: security_id,
date: date,
qty: qty,
price: price.price,
currency: price.currency,
amount: qty * price.price,
cost_basis: cost_basis_for(security_id, date)
)
end.compact
end
def precompute_cost_basis
@cost_basis_snapshots = Hash.new { |h, k| h[k] = [] }
tracker = Hash.new { |h, k| h[k] = { total_cost: BigDecimal("0"), total_qty: BigDecimal("0") } }
invalid_cost_basis = {}
portfolio_cache.get_trades.sort_by(&:date).each do |trade_entry|
trade = trade_entry.entryable
next unless trade.qty > 0
security_id = trade.security_id
next if invalid_cost_basis[security_id]
trade_price = Money.new(trade.price, trade.currency)
begin
converted_price = trade_price.exchange_to(account.currency, date: trade_entry.date, custom_rate: trade.exchange_rate).amount
rescue Money::ConversionError
invalid_cost_basis[security_id] = true
@cost_basis_snapshots[security_id] << [ trade_entry.date, nil ]
Rails.logger.warn(
"Holding::ReverseCalculator: FX conversion failed for trade #{trade.id} " \
"(#{trade.currency}→#{account.currency}, date: #{trade_entry.date}, " \
"custom_rate: #{trade.exchange_rate.inspect}). " \
"Excluding trade from cost basis to avoid mixing currencies."
)
next
end
tracker[security_id][:total_cost] += converted_price * trade.qty
tracker[security_id][:total_qty] += trade.qty
@cost_basis_snapshots[security_id] << [
trade_entry.date,
tracker[security_id][:total_cost] / tracker[security_id][:total_qty]
]
end
end
def cost_basis_for(security_id, date)
snapshots = @cost_basis_snapshots[security_id]
return nil if snapshots.empty?
lo, hi, result = 0, snapshots.size - 1, nil
while lo <= hi
mid = (lo + hi) / 2
if snapshots[mid][0] <= date
result = snapshots[mid][1]
lo = mid + 1
else
hi = mid - 1
end
end
result
end
end