-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathassessor.rb
140 lines (117 loc) · 4.55 KB
/
assessor.rb
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
class Assessor
attr_accessor :buying_plan
attr_accessor :selling_plan
attr_accessor :history_requirement
attr_accessor :holding
attr_accessor :results
DELISTING_DEADBAND = 14.days
def buy_when(history: 2, &b)
@buying_plan = b
@history_requirement = history
end
def sell_when(&b)
@selling_plan = b
end
def buy?(ticker)
buying_plan[ticker]
end
def sell?(ticker, original)
selling_plan[ticker, original]
end
def assess_buys(tickers, opts={})
tids = tickers.map {|t| t.id }
debut = Time.parse(opts[:after].to_s || '1 march 1900')
fin = Time.parse(opts[:before].to_s || Date.today.to_s)
bars = Bar.where(:date => debut..fin, :ticker_id => tids)
.order(:ticker_id, Sequel.asc(:date))
.all
groups = bars.group_by {|b| b.ticker_id }
@holding = []
# create groups of size `@history_requirement`, and then
# pass that history to the checker
# most recent bar is at -1, oldest bar is at 0
@holding = groups.map do |ticker_id, bars|
# assume the history is
histories = bars.each_cons history_requirement
histories.filter do |history|
# verify that the history is consecutive
day_deltas = history.each_cons(2).map {|a, b| b.date - a.date }
if day_deltas.any? {|v| v > 4.days }
false
else
buy? history
end
end.map {|history| history.last }
end.flatten
# `@holding` currently references the days that a decision to buy is made
# (using the day's closing price), but we don't *actually* buy until the
# next morning. So we need to replace these stocks with the next day's
# stock.
#
# This is key because the `Bar#change_from` method operates on the opening
# price of the earlier day.
#
# If `bars[index + 1]` is nil because we're dealing with some HOT OF THE
# PRESS stock recommendations, then... I don't really have a plan for that
# yet. Then the stock doesn't exist, so just present the stock itself.
# It'll stay until the time period is recalculated, which happens often.
#
# From here on out, we're dealing with *simulation*.
#
# TODO get rid of the #index call; we already know that `bars[0]` is `stock`
@holding = @holding.map do |stock|
bars = Bar.where(:ticker => stock.ticker,
:date => stock.date..(stock.date + 7.days))
.order(Sequel.asc(:date))
.all
index = bars.index stock
bars[index + 1] || stock
end
end
def assess_sells(partial: false)
# assumes `@holding` and `@results` are accurately mapped
if partial
verified = @results.filter {|h| h[:sell] }
unverified_stocks = @results.filter {|h| h[:sell].nil? }
.map {|h| h[:buy] }
else
unverified_stocks, verified = @holding, []
end
# Stocks can be delisted, at which point stocks held will be no longer
# valid, but then a *new* ticker can start and can *reuse* the old name.
# And since any stocks held from the previous incarnation won't be valid
# for the new incarnation of the symbol, we need to separate those
# instances. We do this by looking for a stretch of 7 days (using the date,
# not the trading days, since trading days is calculated based on the
# availability of bar information for that specific stock) during which the
# stock is not traded (stocks can go intermittently inactive for short
# periods of time, but that doesn't imply delistment).
unverified = unverified_stocks.map do |stock|
bars = Bar.where(:ticker => stock.ticker) { date >= stock.date }
.order(Sequel.asc(:date))
.all
periods = bars.slice_when do |before, after|
after.date - before.date >= DELISTING_DEADBAND
end
# this is the only period that starts from the holding bar
range = periods.first
sell_bar = range.find {|day| sell? stock, day }
delisted = if sell_bar
false
else
Time.now - range.last.date >= DELISTING_DEADBAND
end
{:buy => stock,
:sell => sell_bar,
:hold => sell_bar ? sell_bar.trading_days_from(stock) : nil,
:ROI => sell_bar ? sell_bar.change_from(stock) : -1,
:delisted => delisted
}
end
@results = (unverified + verified).sort_by {|h| h[:buy].date }
end
def assess(tickers, opts={})
assess_buys tickers, opts
assess_sells :partial => opts[:partial]
end
end