wads 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +27 -0
- data/Rakefile +4 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/data/NASDAQ.csv +5285 -0
- data/data/Pick4_12_21_2020.txt +9878 -0
- data/lib/wads/app.rb +199 -0
- data/lib/wads/data_structures.rb +251 -0
- data/lib/wads/textinput.rb +87 -0
- data/lib/wads/version.rb +3 -0
- data/lib/wads/widgets.rb +827 -0
- data/lib/wads.rb +7 -0
- data/media/Banner.png +0 -0
- data/run-sample-app +3 -0
- data/sample_app.rb +52 -0
- data/wads.gemspec +37 -0
- metadata +107 -0
data/lib/wads/app.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'gosu'
|
2
|
+
require_relative 'data_structures'
|
3
|
+
require_relative 'widgets'
|
4
|
+
require_relative 'version'
|
5
|
+
|
6
|
+
include Wads
|
7
|
+
|
8
|
+
class WadsSampleApp < Gosu::Window
|
9
|
+
|
10
|
+
STOCKS_DATA_FILE = "./data/NASDAQ.csv"
|
11
|
+
LOTTERY_DATA_FILE = "./data/Pick4_12_21_2020.txt"
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
super(800, 600)
|
15
|
+
self.caption = "Wads Sample App"
|
16
|
+
@font = Gosu::Font.new(32)
|
17
|
+
@title_font = Gosu::Font.new(38)
|
18
|
+
@version_font = Gosu::Font.new(22)
|
19
|
+
@small_font = Gosu::Font.new(22)
|
20
|
+
@banner_image = Gosu::Image.new("./media/Banner.png")
|
21
|
+
@display_widget = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_opts_and_run
|
25
|
+
# Make help the default output if no args are specified
|
26
|
+
if ARGV.length == 0
|
27
|
+
ARGV[0] = "-h"
|
28
|
+
end
|
29
|
+
|
30
|
+
opts = SampleAppCommand.new.parse.run
|
31
|
+
if opts[:stocks]
|
32
|
+
stats = process_stock_data
|
33
|
+
if opts[:gui]
|
34
|
+
@display_widget = SampleStocksDisplay.new(@small_font, stats)
|
35
|
+
show
|
36
|
+
else
|
37
|
+
stats.report(Date::DAYNAMES[1..5])
|
38
|
+
end
|
39
|
+
|
40
|
+
elsif opts[:lottery]
|
41
|
+
process_lottery_data
|
42
|
+
else
|
43
|
+
puts " "
|
44
|
+
puts "Select one of the following sample analysis options"
|
45
|
+
puts "-s Run sample stocks analysis"
|
46
|
+
puts "-l Run sample analysis of lottery numbers"
|
47
|
+
puts " "
|
48
|
+
exit
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
def update
|
54
|
+
# TODO
|
55
|
+
end
|
56
|
+
|
57
|
+
def draw
|
58
|
+
draw_banner
|
59
|
+
@display_widget.draw
|
60
|
+
end
|
61
|
+
|
62
|
+
def draw_banner
|
63
|
+
@banner_image.draw(1,1,1,0.9,0.9)
|
64
|
+
@title_font.draw_text("Wads Sample App", 10, 20, 2, 1, 1, Gosu::Color::WHITE)
|
65
|
+
@version_font.draw_text("Version #{Wads::VERSION}", 13, 54, 2, 1, 1, Gosu::Color::WHITE)
|
66
|
+
end
|
67
|
+
|
68
|
+
def button_down id
|
69
|
+
close if id == Gosu::KbEscape
|
70
|
+
# Delegate button events to the primary display widget
|
71
|
+
result = @display_widget.button_down id, mouse_x, mouse_y
|
72
|
+
if result.close_widget
|
73
|
+
close
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_stock_data
|
78
|
+
# The data file comes from https://finance.yahoo.com
|
79
|
+
# The format of this file is as follows:
|
80
|
+
#
|
81
|
+
# Date,Open,High,Low,Close,Adj Close,Volume
|
82
|
+
# 2000-01-03,4186.189941,4192.189941,3989.709961,4131.149902,4131.149902,1510070000
|
83
|
+
# 2000-01-04,4020.000000,4073.250000,3898.229980,3901.689941,3901.689941,1511840000
|
84
|
+
# ...
|
85
|
+
# 2020-12-30,12906.509766,12924.929688,12857.759766,12870.000000,12870.000000,5292210000
|
86
|
+
# 2020-12-31,12877.089844,12902.070313,12821.230469,12888.280273,12888.280273,4771390000
|
87
|
+
|
88
|
+
stats = Stats.new("NASDAQ")
|
89
|
+
previous_close = nil
|
90
|
+
|
91
|
+
puts "Read the data file #{STOCKS_DATA_FILE}"
|
92
|
+
File.readlines(STOCKS_DATA_FILE).each do |line|
|
93
|
+
line = line.chomp # remove the carriage return
|
94
|
+
|
95
|
+
# Ignore header and any empty lines, process numeric data lines
|
96
|
+
if line.length > 0 and line[0].match(/[0-9]/)
|
97
|
+
values = line.split(",")
|
98
|
+
date = Date.strptime(values[0], "%Y-%m-%d")
|
99
|
+
weekday = Date::DAYNAMES[date.wday]
|
100
|
+
|
101
|
+
open_value = values[1].to_f
|
102
|
+
close_value = values[4].to_f
|
103
|
+
|
104
|
+
if previous_close.nil?
|
105
|
+
# Just use the first day to set the baseline
|
106
|
+
previous_close = close_value
|
107
|
+
else
|
108
|
+
change_from_previous_close = close_value - previous_close
|
109
|
+
change_intraday = close_value - open_value
|
110
|
+
change_overnight = open_value - previous_close
|
111
|
+
|
112
|
+
change_percent = change_from_previous_close / previous_close
|
113
|
+
|
114
|
+
stats.add(weekday, change_percent)
|
115
|
+
stats.add("#{weekday} prev close", change_from_previous_close)
|
116
|
+
stats.add("#{weekday} intraday", change_intraday)
|
117
|
+
stats.add("#{weekday} overnight", change_overnight)
|
118
|
+
|
119
|
+
previous_close = close_value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
stats
|
125
|
+
end
|
126
|
+
|
127
|
+
def process_lottery_data
|
128
|
+
puts "The lottery example has not been implemented yet"
|
129
|
+
puts "however the data is available in the data directory,"
|
130
|
+
puts "and the same pattern used in the stocks example can"
|
131
|
+
puts "be applied here."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class SampleStocksDisplay < Widget
|
136
|
+
attr_accessor :stats
|
137
|
+
|
138
|
+
def initialize(font, stats)
|
139
|
+
super(10, 100, COLOR_HEADER_BRIGHT_BLUE)
|
140
|
+
set_dimensions(780, 500)
|
141
|
+
set_font(font)
|
142
|
+
add_child(Document.new(sample_content, x + 5, y + 5, @width, @height, @font))
|
143
|
+
@exit_button = Button.new("Exit", 380, bottom_edge - 30, @font)
|
144
|
+
add_child(@exit_button)
|
145
|
+
|
146
|
+
@stats = stats
|
147
|
+
@data_table = SingleSelectTable.new(@x + 5, @y + 100, # top left corner
|
148
|
+
770, 200, # width, height
|
149
|
+
["Day", "Min", "Avg", "StdDev", "Max", "p10", "p90"], # column headers
|
150
|
+
@font, COLOR_WHITE) # font and text color
|
151
|
+
@data_table.selected_color = COLOR_LIGHT_GRAY
|
152
|
+
Date::DAYNAMES[1..5].each do |day|
|
153
|
+
min = format_percent(@stats.min(day))
|
154
|
+
avg = format_percent(@stats.average(day))
|
155
|
+
std = format_percent(@stats.std_dev(day))
|
156
|
+
max = format_percent(@stats.max(day))
|
157
|
+
p10 = format_percent(@stats.percentile(day, 0.1))
|
158
|
+
p90 = format_percent(@stats.percentile(day, 0.90))
|
159
|
+
@data_table.add_row([day, min, avg, std, max, p10, p90], COLOR_HEADER_BRIGHT_BLUE)
|
160
|
+
end
|
161
|
+
add_child(@data_table)
|
162
|
+
@selection_text = nil
|
163
|
+
end
|
164
|
+
|
165
|
+
def format_percent(val)
|
166
|
+
"#{(val * 100).round(3)}%"
|
167
|
+
end
|
168
|
+
|
169
|
+
def sample_content
|
170
|
+
<<~HEREDOC
|
171
|
+
This sample stock analysis uses NASDAQ data from https://finance.yahoo.com looking
|
172
|
+
at closing data through the years 2000 to 2020. The percent gain or loss is broken
|
173
|
+
down per day, as shown in the table below.
|
174
|
+
HEREDOC
|
175
|
+
end
|
176
|
+
|
177
|
+
def render
|
178
|
+
if @selection_text
|
179
|
+
@selection_text.draw
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def button_down id, mouse_x, mouse_y
|
184
|
+
if id == Gosu::MsLeft
|
185
|
+
if @exit_button.contains_click(mouse_x, mouse_y)
|
186
|
+
return WidgetResult.new(true)
|
187
|
+
elsif @data_table.contains_click(mouse_x, mouse_y)
|
188
|
+
val = @data_table.set_selected_row(mouse_y, 0)
|
189
|
+
if val.nil?
|
190
|
+
# nothing to do
|
191
|
+
else
|
192
|
+
@selection_text = Text.new("You selected #{val}, a great day!",
|
193
|
+
x + 5, y + 400, @font)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
WidgetResult.new(false)
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Wads
|
4
|
+
|
5
|
+
SPACER = " "
|
6
|
+
VALUE_WIDTH = 10
|
7
|
+
NODE_UNKNOWN = "undefined"
|
8
|
+
|
9
|
+
class HashOfHashes
|
10
|
+
attr_accessor :data
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@data = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(data_set_name, x, y)
|
17
|
+
data_set = @data[x]
|
18
|
+
if data_set.nil?
|
19
|
+
data_set = {}
|
20
|
+
@data[x] = data_set
|
21
|
+
end
|
22
|
+
data_set[x] = y
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(data_set_name, x)
|
26
|
+
data_set = @data[x]
|
27
|
+
if data_set.nil?
|
28
|
+
return nil
|
29
|
+
end
|
30
|
+
data_set[x]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Stats
|
35
|
+
attr_accessor :name
|
36
|
+
attr_accessor :data
|
37
|
+
|
38
|
+
def initialize(name)
|
39
|
+
@name = name
|
40
|
+
@data = {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def add(key, value)
|
44
|
+
data_set = @data[key]
|
45
|
+
if data_set
|
46
|
+
data_set << value
|
47
|
+
else
|
48
|
+
data_set = []
|
49
|
+
data_set << value
|
50
|
+
@data[key] = data_set
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def increment(key)
|
55
|
+
add(key, 1)
|
56
|
+
end
|
57
|
+
|
58
|
+
def count(key)
|
59
|
+
data_set = @data[key]
|
60
|
+
if data_set
|
61
|
+
return data_set.size
|
62
|
+
else
|
63
|
+
return 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def sum(key)
|
68
|
+
data_set = @data[key]
|
69
|
+
if data_set
|
70
|
+
return data_set.inject(0.to_f){|sum,x| sum + x }
|
71
|
+
else
|
72
|
+
return 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def average(key)
|
77
|
+
data_set = @data[key]
|
78
|
+
if data_set
|
79
|
+
return (sum(key) / count(key)).round(5)
|
80
|
+
else
|
81
|
+
return 0
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def min(key)
|
86
|
+
data_set = @data[key]
|
87
|
+
if data_set
|
88
|
+
return data_set.min
|
89
|
+
else
|
90
|
+
return 0
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def max(key)
|
95
|
+
data_set = @data[key]
|
96
|
+
if data_set
|
97
|
+
return data_set.max
|
98
|
+
else
|
99
|
+
return 0
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def sample_variance(key)
|
104
|
+
data_set = @data[key]
|
105
|
+
return 0 unless data_set
|
106
|
+
m = average(key)
|
107
|
+
s = data_set.inject(0.0){|accum, i| accum +(i-m)**2 }
|
108
|
+
s/(data_set.length - 1).to_f
|
109
|
+
end
|
110
|
+
|
111
|
+
def std_dev(key)
|
112
|
+
Math.sqrt(sample_variance(key))
|
113
|
+
end
|
114
|
+
|
115
|
+
def halfway(b, e)
|
116
|
+
d = e - b
|
117
|
+
m = b + (d / 2).to_i
|
118
|
+
m
|
119
|
+
end
|
120
|
+
|
121
|
+
def percentile(key, pct)
|
122
|
+
data_set = @data[key]
|
123
|
+
if data_set
|
124
|
+
sorted_data_set = data_set.sort
|
125
|
+
pct_index = (data_set.length - 1).to_f * pct
|
126
|
+
mod = pct_index.modulo(1.0)
|
127
|
+
adj = pct_index
|
128
|
+
if mod > 0.9
|
129
|
+
adj = pct_index.ceil
|
130
|
+
elsif mod < 0.1
|
131
|
+
adj = pct_index.floor
|
132
|
+
else
|
133
|
+
# We want halfway between the two indices
|
134
|
+
low = pct_index.floor
|
135
|
+
high = pct_index.ceil
|
136
|
+
if low < 0
|
137
|
+
low = 0
|
138
|
+
end
|
139
|
+
if high > data_set.size - 1
|
140
|
+
high = data_set.size - 1
|
141
|
+
end
|
142
|
+
result = halfway(sorted_data_set[low], sorted_data_set[high])
|
143
|
+
return result
|
144
|
+
end
|
145
|
+
|
146
|
+
if adj < 0
|
147
|
+
adj = 0
|
148
|
+
elsif adj > data_set.size - 1
|
149
|
+
adj = data_set.size - 1
|
150
|
+
end
|
151
|
+
result = sorted_data_set[adj]
|
152
|
+
return result
|
153
|
+
else
|
154
|
+
return 0
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def most_common(key)
|
159
|
+
value_counts = {}
|
160
|
+
data_set = @data[key]
|
161
|
+
data_set.each do |data|
|
162
|
+
c = value_counts[data]
|
163
|
+
if c.nil?
|
164
|
+
value_counts[data] = 1
|
165
|
+
else
|
166
|
+
value_counts[data] = value_counts[data] + 1
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
largest_count = 0
|
171
|
+
largest_key = "none"
|
172
|
+
value_counts.keys.each do |key|
|
173
|
+
key_count = value_counts[key]
|
174
|
+
if key_count > largest_count
|
175
|
+
largest_count = key_count
|
176
|
+
largest_key = key
|
177
|
+
end
|
178
|
+
end
|
179
|
+
largest_key
|
180
|
+
end
|
181
|
+
|
182
|
+
def pad(str, size, left_align = false)
|
183
|
+
str = str.to_s
|
184
|
+
if left_align
|
185
|
+
str[0, size].ljust(size, ' ')
|
186
|
+
else
|
187
|
+
str[0, size].rjust(size, ' ')
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def display_counts
|
192
|
+
puts "#{pad(@name, 20)} Value"
|
193
|
+
puts "#{'-' * 20} #{'-' * 10}"
|
194
|
+
@data.keys.each do |key|
|
195
|
+
#data_set = @data[key]
|
196
|
+
puts "#{pad(key, 20)} #{count(key)}"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def keys
|
201
|
+
@data.keys
|
202
|
+
end
|
203
|
+
|
204
|
+
def report(report_keys = keys)
|
205
|
+
puts "#{pad(@name, 10)}#{SPACER}#{pad('Count', 7)}#{SPACER}#{pad('Min', VALUE_WIDTH)}#{SPACER}#{pad('Avg', VALUE_WIDTH)}#{SPACER}#{pad('StdDev', VALUE_WIDTH)}#{SPACER}#{pad('Max', VALUE_WIDTH)}#{SPACER}| #{pad('p1', VALUE_WIDTH)}#{SPACER}#{pad('p10', VALUE_WIDTH)}#{SPACER}#{pad('p50', VALUE_WIDTH)}#{SPACER}#{pad('p90', VALUE_WIDTH)}#{SPACER}#{pad('p99', VALUE_WIDTH)}"
|
206
|
+
puts "#{'-' * 10}#{SPACER}#{'-' * 7}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}| #{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}#{SPACER}#{'-' * VALUE_WIDTH}"
|
207
|
+
if report_keys.nil?
|
208
|
+
report_keys = @data.keys
|
209
|
+
end
|
210
|
+
report_keys.each do |key|
|
211
|
+
data_set = @data[key]
|
212
|
+
m1 = min(key).round(5)
|
213
|
+
a = average(key).round(5)
|
214
|
+
sd = std_dev(key).round(5)
|
215
|
+
m2 = max(key).round(5)
|
216
|
+
p1 = percentile(key, 0.01).round(5)
|
217
|
+
p10 = percentile(key, 0.1).round(5)
|
218
|
+
p50 = percentile(key, 0.5).round(5)
|
219
|
+
p90 = percentile(key, 0.90).round(5)
|
220
|
+
p99 = percentile(key, 0.99).round(5)
|
221
|
+
puts "#{pad(key, 10)}#{SPACER}#{pad(count(key), 7)}#{SPACER}#{pad(m1, VALUE_WIDTH)}#{SPACER}#{pad(a, VALUE_WIDTH)}#{SPACER}#{pad(sd, VALUE_WIDTH)}#{SPACER}#{pad(m2, VALUE_WIDTH)}#{SPACER}| #{pad(p1, VALUE_WIDTH)}#{SPACER}#{pad(p10, VALUE_WIDTH)}#{SPACER}#{pad(p50, VALUE_WIDTH)}#{SPACER}#{pad(p90, VALUE_WIDTH)}#{SPACER}#{pad(p99, VALUE_WIDTH)}"
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class Node
|
228
|
+
attr_accessor :x
|
229
|
+
attr_accessor :y
|
230
|
+
attr_accessor :name
|
231
|
+
attr_accessor :type
|
232
|
+
attr_accessor :inputs
|
233
|
+
attr_accessor :outputs
|
234
|
+
attr_accessor :visited
|
235
|
+
|
236
|
+
def initialize(name, type = NODE_UNKNOWN)
|
237
|
+
@name = name
|
238
|
+
@type = type
|
239
|
+
@inputs = []
|
240
|
+
@outputs = []
|
241
|
+
@visited = false
|
242
|
+
end
|
243
|
+
|
244
|
+
#
|
245
|
+
# TODO Visitor pattern and solution for detecting cyclic graphs
|
246
|
+
#
|
247
|
+
# when you visit, reset all the visited flags
|
248
|
+
# set it to true when you visit the node
|
249
|
+
# first check though if visited already true, if so, you have a cycle
|
250
|
+
end
|
251
|
+
end
|