wads 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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