kbs 0.0.1

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/COMMITS.md +196 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/Rakefile +8 -0
  8. data/examples/README.md +531 -0
  9. data/examples/advanced_example.rb +270 -0
  10. data/examples/ai_enhanced_kbs.rb +523 -0
  11. data/examples/blackboard_demo.rb +50 -0
  12. data/examples/car_diagnostic.rb +64 -0
  13. data/examples/concurrent_inference_demo.rb +363 -0
  14. data/examples/csv_trading_system.rb +559 -0
  15. data/examples/iot_demo_using_dsl.rb +83 -0
  16. data/examples/portfolio_rebalancing_system.rb +651 -0
  17. data/examples/redis_trading_demo.rb +177 -0
  18. data/examples/sample_stock_data.csv +46 -0
  19. data/examples/stock_trading_advanced.rb +469 -0
  20. data/examples/stock_trading_system.rb.bak +563 -0
  21. data/examples/timestamped_trading.rb +286 -0
  22. data/examples/trading_demo.rb +334 -0
  23. data/examples/working_demo.rb +176 -0
  24. data/lib/kbs/alpha_memory.rb +37 -0
  25. data/lib/kbs/beta_memory.rb +57 -0
  26. data/lib/kbs/blackboard/audit_log.rb +115 -0
  27. data/lib/kbs/blackboard/engine.rb +83 -0
  28. data/lib/kbs/blackboard/fact.rb +65 -0
  29. data/lib/kbs/blackboard/memory.rb +191 -0
  30. data/lib/kbs/blackboard/message_queue.rb +96 -0
  31. data/lib/kbs/blackboard/persistence/hybrid_store.rb +118 -0
  32. data/lib/kbs/blackboard/persistence/redis_store.rb +218 -0
  33. data/lib/kbs/blackboard/persistence/sqlite_store.rb +242 -0
  34. data/lib/kbs/blackboard/persistence/store.rb +55 -0
  35. data/lib/kbs/blackboard/redis_audit_log.rb +107 -0
  36. data/lib/kbs/blackboard/redis_message_queue.rb +111 -0
  37. data/lib/kbs/blackboard.rb +23 -0
  38. data/lib/kbs/condition.rb +26 -0
  39. data/lib/kbs/dsl/condition_helpers.rb +57 -0
  40. data/lib/kbs/dsl/knowledge_base.rb +86 -0
  41. data/lib/kbs/dsl/pattern_evaluator.rb +69 -0
  42. data/lib/kbs/dsl/rule_builder.rb +115 -0
  43. data/lib/kbs/dsl/variable.rb +35 -0
  44. data/lib/kbs/dsl.rb +18 -0
  45. data/lib/kbs/fact.rb +43 -0
  46. data/lib/kbs/join_node.rb +117 -0
  47. data/lib/kbs/negation_node.rb +88 -0
  48. data/lib/kbs/production_node.rb +28 -0
  49. data/lib/kbs/rete_engine.rb +108 -0
  50. data/lib/kbs/rule.rb +46 -0
  51. data/lib/kbs/token.rb +37 -0
  52. data/lib/kbs/version.rb +5 -0
  53. data/lib/kbs/working_memory.rb +32 -0
  54. data/lib/kbs.rb +20 -0
  55. metadata +164 -0
@@ -0,0 +1,559 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs'
4
+ require 'csv'
5
+ require 'date'
6
+
7
+ class CSVTradingSystem
8
+ def initialize(csv_file = 'sample_stock_data.csv')
9
+ @engine = KBS::ReteEngine.new
10
+ @csv_file = csv_file
11
+ @portfolio = {
12
+ cash: 100_000,
13
+ positions: {},
14
+ trades: [],
15
+ daily_values: []
16
+ }
17
+ @current_prices = {}
18
+ @price_history = Hash.new { |h, k| h[k] = [] }
19
+ setup_trading_rules
20
+ end
21
+
22
+ def setup_trading_rules
23
+ # Rule 1: Moving Average Crossover (Golden Cross)
24
+ ma_crossover_rule = KBS::Rule.new(
25
+ "moving_average_crossover",
26
+ conditions: [
27
+ KBS::Condition.new(:technical_indicator, {
28
+ indicator: "ma_crossover",
29
+ ma_20: ->(ma20) { ma20 && ma20 > 0 },
30
+ ma_50: ->(ma50) { ma50 && ma50 > 0 },
31
+ signal: "golden_cross"
32
+ }),
33
+ KBS::Condition.new(:stock_price, {
34
+ symbol: ->(s) { s && s.length > 0 }
35
+ }),
36
+ KBS::Condition.new(:position, {
37
+ symbol: ->(s) { s && s.length > 0 },
38
+ status: "closed"
39
+ }, negated: true)
40
+ ],
41
+ action: lambda do |facts, bindings|
42
+ indicator = facts.find { |f| f.type == :technical_indicator }
43
+ price = facts.find { |f| f.type == :stock_price }
44
+
45
+ if indicator && price && indicator[:symbol] == price[:symbol]
46
+ symbol = price[:symbol]
47
+ current_price = price[:close]
48
+
49
+ puts "📈 GOLDEN CROSS: #{symbol}"
50
+ puts " 20-MA: $#{indicator[:ma_20].round(2)}"
51
+ puts " 50-MA: $#{indicator[:ma_50].round(2)}"
52
+ puts " Current Price: $#{current_price}"
53
+ puts " Signal: Strong BUY"
54
+
55
+ # Execute buy order
56
+ if @portfolio[:cash] >= current_price * 100
57
+ execute_trade(symbol, "BUY", 100, current_price, price[:date])
58
+ end
59
+ end
60
+ end,
61
+ priority: 15
62
+ )
63
+
64
+ # Rule 2: RSI Oversold Bounce
65
+ rsi_oversold_rule = KBS::Rule.new(
66
+ "rsi_oversold",
67
+ conditions: [
68
+ KBS::Condition.new(:technical_indicator, {
69
+ indicator: "rsi",
70
+ rsi_value: ->(rsi) { rsi && rsi < 30 }
71
+ }),
72
+ KBS::Condition.new(:stock_price, {
73
+ price_change: ->(change) { change && change > 1 } # Bouncing up
74
+ })
75
+ ],
76
+ action: lambda do |facts, bindings|
77
+ indicator = facts.find { |f| f.type == :technical_indicator }
78
+ price = facts.find { |f| f.type == :stock_price }
79
+
80
+ if indicator && price && indicator[:symbol] == price[:symbol]
81
+ symbol = price[:symbol]
82
+ puts "🔄 RSI OVERSOLD BOUNCE: #{symbol}"
83
+ puts " RSI: #{indicator[:rsi_value].round(1)}"
84
+ puts " Price Change: +#{price[:price_change].round(2)}%"
85
+ puts " Signal: BUY (oversold reversal)"
86
+
87
+ if @portfolio[:cash] >= price[:close] * 50
88
+ execute_trade(symbol, "BUY", 50, price[:close], price[:date])
89
+ end
90
+ end
91
+ end,
92
+ priority: 12
93
+ )
94
+
95
+ # Rule 3: Take Profit
96
+ take_profit_rule = KBS::Rule.new(
97
+ "take_profit",
98
+ conditions: [
99
+ KBS::Condition.new(:position, {
100
+ status: "open",
101
+ profit_percent: ->(profit) { profit && profit > 10 }
102
+ })
103
+ ],
104
+ action: lambda do |facts, bindings|
105
+ position = facts.find { |f| f.type == :position }
106
+ symbol = position[:symbol]
107
+ current_price = @current_prices[symbol]
108
+
109
+ if current_price
110
+ puts "💰 TAKE PROFIT: #{symbol}"
111
+ puts " Entry: $#{position[:entry_price]}"
112
+ puts " Current: $#{current_price}"
113
+ puts " Profit: #{position[:profit_percent].round(1)}%"
114
+ puts " Signal: SELL (take profit)"
115
+
116
+ execute_trade(symbol, "SELL", position[:shares], current_price, position[:current_date])
117
+ end
118
+ end,
119
+ priority: 18
120
+ )
121
+
122
+ # Rule 4: Stop Loss
123
+ stop_loss_rule = KBS::Rule.new(
124
+ "stop_loss",
125
+ conditions: [
126
+ KBS::Condition.new(:position, {
127
+ status: "open",
128
+ loss_percent: ->(loss) { loss && loss > 8 }
129
+ })
130
+ ],
131
+ action: lambda do |facts, bindings|
132
+ position = facts.find { |f| f.type == :position }
133
+ symbol = position[:symbol]
134
+ current_price = @current_prices[symbol]
135
+
136
+ if current_price
137
+ puts "🛑 STOP LOSS: #{symbol}"
138
+ puts " Entry: $#{position[:entry_price]}"
139
+ puts " Current: $#{current_price}"
140
+ puts " Loss: #{position[:loss_percent].round(1)}%"
141
+ puts " Signal: SELL (stop loss)"
142
+
143
+ execute_trade(symbol, "SELL", position[:shares], current_price, position[:current_date])
144
+ end
145
+ end,
146
+ priority: 20
147
+ )
148
+
149
+ # Rule 5: Breakout Pattern
150
+ breakout_rule = KBS::Rule.new(
151
+ "price_breakout",
152
+ conditions: [
153
+ KBS::Condition.new(:technical_indicator, {
154
+ indicator: "breakout",
155
+ resistance_break: true,
156
+ volume_confirmation: true
157
+ })
158
+ ],
159
+ action: lambda do |facts, bindings|
160
+ indicator = facts.find { |f| f.type == :technical_indicator }
161
+ symbol = indicator[:symbol]
162
+
163
+ puts "🚀 BREAKOUT: #{symbol}"
164
+ puts " Resistance Level: $#{indicator[:resistance_level]}"
165
+ puts " Current Price: $#{indicator[:current_price]}"
166
+ puts " Volume Spike: #{indicator[:volume_ratio]}x"
167
+ puts " Signal: BUY (momentum breakout)"
168
+
169
+ if @portfolio[:cash] >= indicator[:current_price] * 75
170
+ execute_trade(symbol, "BUY", 75, indicator[:current_price], indicator[:date])
171
+ end
172
+ end,
173
+ priority: 13
174
+ )
175
+
176
+ # Rule 6: Trend Following
177
+ trend_following_rule = KBS::Rule.new(
178
+ "trend_following",
179
+ conditions: [
180
+ KBS::Condition.new(:technical_indicator, {
181
+ indicator: "trend",
182
+ trend_direction: "up",
183
+ trend_strength: ->(strength) { strength && strength > 0.7 }
184
+ }),
185
+ KBS::Condition.new(:stock_price, {
186
+ volume: ->(vol) { vol && vol > 50_000_000 } # High volume confirmation
187
+ })
188
+ ],
189
+ action: lambda do |facts, bindings|
190
+ indicator = facts.find { |f| f.type == :technical_indicator }
191
+ price = facts.find { |f| f.type == :stock_price }
192
+
193
+ if indicator && price && indicator[:symbol] == price[:symbol]
194
+ symbol = price[:symbol]
195
+ puts "📊 TREND FOLLOWING: #{symbol}"
196
+ puts " Trend Strength: #{(indicator[:trend_strength] * 100).round(1)}%"
197
+ puts " Volume: #{(price[:volume] / 1_000_000.0).round(1)}M"
198
+ puts " Signal: BUY (strong uptrend)"
199
+
200
+ if @portfolio[:cash] >= price[:close] * 60
201
+ execute_trade(symbol, "BUY", 60, price[:close], price[:date])
202
+ end
203
+ end
204
+ end,
205
+ priority: 10
206
+ )
207
+
208
+ @engine.add_rule(ma_crossover_rule)
209
+ @engine.add_rule(rsi_oversold_rule)
210
+ @engine.add_rule(take_profit_rule)
211
+ @engine.add_rule(stop_loss_rule)
212
+ @engine.add_rule(breakout_rule)
213
+ @engine.add_rule(trend_following_rule)
214
+ end
215
+
216
+ def calculate_moving_average(prices, period)
217
+ return nil if prices.length < period
218
+ prices.last(period).sum / period.to_f
219
+ end
220
+
221
+ def calculate_rsi(prices, period = 14)
222
+ return 50 if prices.length < period + 1
223
+
224
+ gains = []
225
+ losses = []
226
+
227
+ (1...prices.length).each do |i|
228
+ change = prices[i] - prices[i-1]
229
+ if change > 0
230
+ gains << change
231
+ losses << 0
232
+ else
233
+ gains << 0
234
+ losses << change.abs
235
+ end
236
+ end
237
+
238
+ return 50 if gains.length < period
239
+
240
+ avg_gain = gains.last(period).sum / period.to_f
241
+ avg_loss = losses.last(period).sum / period.to_f
242
+
243
+ return 100 if avg_loss == 0
244
+
245
+ rs = avg_gain / avg_loss
246
+ 100 - (100 / (1 + rs))
247
+ end
248
+
249
+ def detect_breakout(symbol, current_price, prices, volume, avg_volume)
250
+ return false if prices.length < 20
251
+
252
+ # Calculate resistance level (highest high of last 20 days)
253
+ resistance = prices.last(20).max
254
+
255
+ # Check if price breaks above resistance with volume confirmation
256
+ price_break = current_price > resistance * 1.01 # 1% above resistance
257
+ volume_confirmation = volume > avg_volume * 1.5 # 50% above average volume
258
+
259
+ {
260
+ resistance_break: price_break,
261
+ volume_confirmation: volume_confirmation,
262
+ resistance_level: resistance,
263
+ current_price: current_price,
264
+ volume_ratio: volume / avg_volume.to_f
265
+ }
266
+ end
267
+
268
+ def calculate_trend_strength(prices)
269
+ return 0.5 if prices.length < 10
270
+
271
+ # Simple trend strength based on price momentum
272
+ recent_avg = prices.last(5).sum / 5.0
273
+ older_avg = prices[-10..-6].sum / 5.0
274
+
275
+ strength = (recent_avg - older_avg) / older_avg
276
+ [[strength, 0].max, 1].min # Clamp between 0 and 1
277
+ end
278
+
279
+ def execute_trade(symbol, action, shares, price, date)
280
+ if action == "BUY"
281
+ cost = shares * price
282
+ if @portfolio[:cash] >= cost
283
+ @portfolio[:cash] -= cost
284
+ @portfolio[:positions][symbol] = {
285
+ symbol: symbol,
286
+ shares: shares,
287
+ entry_price: price,
288
+ entry_date: date,
289
+ status: "open"
290
+ }
291
+
292
+ trade = {
293
+ symbol: symbol,
294
+ action: action,
295
+ shares: shares,
296
+ price: price,
297
+ date: date,
298
+ value: cost
299
+ }
300
+ @portfolio[:trades] << trade
301
+
302
+ puts " ✅ EXECUTED: #{action} #{shares} shares of #{symbol} at $#{price}"
303
+ puts " 💰 Cash Remaining: $#{@portfolio[:cash].round(2)}"
304
+ end
305
+ elsif action == "SELL"
306
+ if @portfolio[:positions][symbol] && @portfolio[:positions][symbol][:status] == "open"
307
+ proceeds = shares * price
308
+ @portfolio[:cash] += proceeds
309
+
310
+ position = @portfolio[:positions][symbol]
311
+ profit = proceeds - (position[:shares] * position[:entry_price])
312
+
313
+ @portfolio[:positions][symbol][:status] = "closed"
314
+ @portfolio[:positions][symbol][:exit_price] = price
315
+ @portfolio[:positions][symbol][:exit_date] = date
316
+ @portfolio[:positions][symbol][:profit] = profit
317
+
318
+ trade = {
319
+ symbol: symbol,
320
+ action: action,
321
+ shares: shares,
322
+ price: price,
323
+ date: date,
324
+ value: proceeds,
325
+ profit: profit
326
+ }
327
+ @portfolio[:trades] << trade
328
+
329
+ puts " ✅ EXECUTED: #{action} #{shares} shares of #{symbol} at $#{price}"
330
+ puts " 💰 Profit/Loss: $#{profit.round(2)}"
331
+ puts " 💰 Cash: $#{@portfolio[:cash].round(2)}"
332
+ end
333
+ end
334
+ end
335
+
336
+ def update_positions(date)
337
+ @portfolio[:positions].each do |symbol, position|
338
+ if position[:status] == "open" && @current_prices[symbol]
339
+ current_price = @current_prices[symbol]
340
+ entry_price = position[:entry_price]
341
+ shares = position[:shares]
342
+
343
+ current_value = shares * current_price
344
+ entry_value = shares * entry_price
345
+
346
+ profit_loss = current_value - entry_value
347
+ profit_percent = (profit_loss / entry_value) * 100
348
+ loss_percent = profit_percent < 0 ? profit_percent.abs : 0
349
+
350
+ position[:current_price] = current_price
351
+ position[:current_value] = current_value
352
+ position[:unrealized_pnl] = profit_loss
353
+ position[:profit_percent] = profit_percent > 0 ? profit_percent : 0
354
+ position[:loss_percent] = loss_percent
355
+ position[:current_date] = date
356
+
357
+ # Add updated position as fact for rules to evaluate
358
+ @engine.add_fact(:position, position)
359
+ end
360
+ end
361
+ end
362
+
363
+ def process_csv_data
364
+ puts "🏦 CSV TRADING SYSTEM - Historical Backtesting"
365
+ puts "=" * 70
366
+ puts "Initial Capital: $#{@portfolio[:cash].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
367
+ puts "=" * 70
368
+
369
+ CSV.foreach(@csv_file, headers: true) do |row|
370
+ date = Date.parse(row['Date'])
371
+ symbol = row['Symbol']
372
+ open_price = row['Open'].to_f
373
+ high = row['High'].to_f
374
+ low = row['Low'].to_f
375
+ close = row['Close'].to_f
376
+ volume = row['Volume'].to_i
377
+
378
+ # Store price history
379
+ @price_history[symbol] << close
380
+ @current_prices[symbol] = close
381
+
382
+ # Calculate price change from previous day
383
+ price_change = 0
384
+ if @price_history[symbol].length > 1
385
+ prev_close = @price_history[symbol][-2]
386
+ price_change = ((close - prev_close) / prev_close) * 100
387
+ end
388
+
389
+ puts "\n📅 #{date} - Processing #{symbol}"
390
+ puts " OHLC: $#{open_price} / $#{high} / $#{low} / $#{close}"
391
+ puts " Volume: #{(volume / 1_000_000.0).round(1)}M"
392
+ puts " Change: #{price_change >= 0 ? '+' : ''}#{price_change.round(2)}%"
393
+
394
+ # Clear previous facts
395
+ @engine.working_memory.facts.clear
396
+
397
+ # Add current price fact
398
+ @engine.add_fact(:stock_price, {
399
+ symbol: symbol,
400
+ date: date,
401
+ open: open_price,
402
+ high: high,
403
+ low: low,
404
+ close: close,
405
+ volume: volume,
406
+ price_change: price_change
407
+ })
408
+
409
+ # Calculate and add technical indicators
410
+ if @price_history[symbol].length >= 50
411
+ ma_20 = calculate_moving_average(@price_history[symbol], 20)
412
+ ma_50 = calculate_moving_average(@price_history[symbol], 50)
413
+
414
+ # Golden Cross detection
415
+ if ma_20 && ma_50 && ma_20 > ma_50
416
+ prev_ma_20 = calculate_moving_average(@price_history[symbol][0..-2], 20)
417
+ prev_ma_50 = calculate_moving_average(@price_history[symbol][0..-2], 50)
418
+
419
+ if prev_ma_20 && prev_ma_50 && prev_ma_20 <= prev_ma_50
420
+ @engine.add_fact(:technical_indicator, {
421
+ symbol: symbol,
422
+ indicator: "ma_crossover",
423
+ ma_20: ma_20,
424
+ ma_50: ma_50,
425
+ signal: "golden_cross",
426
+ date: date
427
+ })
428
+ end
429
+ end
430
+ end
431
+
432
+ # RSI calculation
433
+ if @price_history[symbol].length >= 15
434
+ rsi = calculate_rsi(@price_history[symbol])
435
+ @engine.add_fact(:technical_indicator, {
436
+ symbol: symbol,
437
+ indicator: "rsi",
438
+ rsi_value: rsi,
439
+ date: date
440
+ })
441
+ end
442
+
443
+ # Breakout detection
444
+ if @price_history[symbol].length >= 20
445
+ avg_volume = @price_history[symbol].length >= 20 ?
446
+ @price_history[symbol].last(20).sum / 20.0 * 45_000_000 : 45_000_000
447
+
448
+ breakout_data = detect_breakout(symbol, close, @price_history[symbol], volume, avg_volume)
449
+
450
+ if breakout_data[:resistance_break] && breakout_data[:volume_confirmation]
451
+ @engine.add_fact(:technical_indicator, {
452
+ symbol: symbol,
453
+ indicator: "breakout",
454
+ resistance_break: true,
455
+ volume_confirmation: true,
456
+ resistance_level: breakout_data[:resistance_level],
457
+ current_price: close,
458
+ volume_ratio: breakout_data[:volume_ratio],
459
+ date: date
460
+ })
461
+ end
462
+ end
463
+
464
+ # Trend analysis
465
+ if @price_history[symbol].length >= 10
466
+ trend_strength = calculate_trend_strength(@price_history[symbol])
467
+
468
+ @engine.add_fact(:technical_indicator, {
469
+ symbol: symbol,
470
+ indicator: "trend",
471
+ trend_direction: trend_strength > 0.6 ? "up" : "down",
472
+ trend_strength: trend_strength,
473
+ date: date
474
+ })
475
+ end
476
+
477
+ # Update existing positions
478
+ update_positions(date)
479
+
480
+ # Run the inference engine
481
+ @engine.run
482
+
483
+ # Calculate portfolio value
484
+ portfolio_value = @portfolio[:cash]
485
+ @portfolio[:positions].each do |sym, pos|
486
+ if pos[:status] == "open"
487
+ portfolio_value += pos[:shares] * @current_prices[sym]
488
+ end
489
+ end
490
+
491
+ @portfolio[:daily_values] << {
492
+ date: date,
493
+ portfolio_value: portfolio_value,
494
+ cash: @portfolio[:cash],
495
+ positions_value: portfolio_value - @portfolio[:cash]
496
+ }
497
+
498
+ puts " 📊 Portfolio Value: $#{portfolio_value.round(2)}"
499
+ end
500
+
501
+ print_final_results
502
+ end
503
+
504
+ def print_final_results
505
+ puts "\n" + "=" * 70
506
+ puts "FINAL TRADING RESULTS"
507
+ puts "=" * 70
508
+
509
+ # Calculate final portfolio value
510
+ final_value = @portfolio[:cash]
511
+ @portfolio[:positions].each do |symbol, position|
512
+ if position[:status] == "open"
513
+ final_value += position[:shares] * @current_prices[symbol]
514
+ end
515
+ end
516
+
517
+ initial_value = 100_000
518
+ total_return = final_value - initial_value
519
+ return_pct = (total_return / initial_value) * 100
520
+
521
+ puts "Initial Capital: $#{initial_value.to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
522
+ puts "Final Value: $#{final_value.round(2).to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
523
+ puts "Total Return: $#{total_return.round(2)} (#{return_pct.round(2)}%)"
524
+ puts "Cash: $#{@portfolio[:cash].round(2)}"
525
+
526
+ puts "\nOpen Positions:"
527
+ @portfolio[:positions].each do |symbol, position|
528
+ if position[:status] == "open"
529
+ current_value = position[:shares] * @current_prices[symbol]
530
+ puts " #{symbol}: #{position[:shares]} shares @ $#{@current_prices[symbol]} = $#{current_value.round(2)}"
531
+ end
532
+ end
533
+
534
+ puts "\nTrade Summary:"
535
+ puts "Total Trades: #{@portfolio[:trades].length}"
536
+
537
+ buy_trades = @portfolio[:trades].select { |t| t[:action] == "BUY" }
538
+ sell_trades = @portfolio[:trades].select { |t| t[:action] == "SELL" }
539
+
540
+ puts "Buy Orders: #{buy_trades.length}"
541
+ puts "Sell Orders: #{sell_trades.length}"
542
+
543
+ if sell_trades.any?
544
+ profitable_trades = sell_trades.select { |t| t[:profit] > 0 }
545
+ win_rate = (profitable_trades.length.to_f / sell_trades.length) * 100
546
+ puts "Win Rate: #{win_rate.round(1)}%"
547
+
548
+ total_profit = sell_trades.sum { |t| t[:profit] }
549
+ puts "Realized P&L: $#{total_profit.round(2)}"
550
+ end
551
+
552
+ puts "\n" + "=" * 70
553
+ end
554
+ end
555
+
556
+ if __FILE__ == $0
557
+ system = CSVTradingSystem.new('sample_stock_data.csv')
558
+ system.process_csv_data
559
+ end
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs/dsl'
4
+ include KBS::DSL::ConditionHelpers
5
+
6
+ kb = KBS.knowledge_base do
7
+ rule "high_temperature_alert" do
8
+ desc "Alert when temperature exceeds safe limits"
9
+ priority 10
10
+
11
+ on :sensor, type: "temperature", location: "reactor"
12
+ on :reading do
13
+ value greater_than(100)
14
+ unit "celsius"
15
+ end
16
+ without.on :alarm, type: "temperature", active: true
17
+
18
+ perform do |facts, bindings|
19
+ sensor = facts.find { |f| f.type == :sensor }
20
+ reading = facts.find { |f| f.type == :reading }
21
+ puts "🚨 HIGH TEMPERATURE ALERT!"
22
+ puts " Location: #{sensor[:location]}"
23
+ puts " Temperature: #{reading[:value]}°#{reading[:unit]}"
24
+ puts " Action: Activating cooling system"
25
+ end
26
+ end
27
+
28
+ rule "low_inventory" do
29
+ desc "Check for items that need reordering"
30
+ priority 5
31
+
32
+ on :item do
33
+ quantity less_than(10)
34
+ category "essential"
35
+ end
36
+ absent :order, status: "pending"
37
+
38
+ perform do |facts, bindings|
39
+ item = facts.find { |f| f.type == :item }
40
+ puts "📦 LOW INVENTORY WARNING"
41
+ puts " Item: #{item[:name]}"
42
+ puts " Quantity: #{item[:quantity]}"
43
+ puts " Action: Creating purchase order"
44
+ end
45
+ end
46
+
47
+ rule "customer_vip_upgrade" do
48
+ desc "Upgrade customers to VIP status"
49
+
50
+ on :customer do
51
+ total_purchases greater_than(10000)
52
+ member_since satisfies { |date| date && date < Time.now - (365 * 24 * 60 * 60) }
53
+ end
54
+ without.on :customer, status: "vip"
55
+
56
+ perform do |facts, bindings|
57
+ customer = facts.find { |f| f.type == :customer }
58
+ puts "⭐ VIP UPGRADE"
59
+ puts " Customer: #{customer[:name]}"
60
+ puts " Total Purchases: $#{customer[:total_purchases]}"
61
+ puts " Action: Upgrading to VIP status"
62
+ end
63
+ end
64
+ end
65
+
66
+ puts "Expert System with DSL"
67
+ puts "=" * 60
68
+
69
+ kb.print_rules
70
+
71
+ puts "\nAdding facts..."
72
+ kb.fact :sensor, type: "temperature", location: "reactor", id: 1
73
+ kb.fact :reading, value: 105, unit: "celsius", sensor_id: 1
74
+ kb.fact :item, name: "Safety Valve", quantity: 5, category: "essential"
75
+ kb.fact :customer, name: "John Doe", total_purchases: 15000, member_since: Time.now - (400 * 24 * 60 * 60)
76
+
77
+ puts "\nRunning inference engine..."
78
+ puts "-" * 60
79
+ kb.run
80
+
81
+ puts "\n" + "=" * 60
82
+ puts "Current Facts in Working Memory:"
83
+ kb.print_facts