kbs 0.0.1 → 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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. data/examples/stock_trading_system.rb.bak +0 -563
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs/blackboard'
4
+
5
+ puts "Redis-Backed High-Frequency Trading System"
6
+ puts "=" * 70
7
+
8
+ # Demo 1: Pure Redis Store (fast, in-memory)
9
+ puts "\n=== Pure Redis Store Demo ==="
10
+ puts "Fast in-memory fact storage with Redis"
11
+
12
+ begin
13
+ redis_store = KBS::Blackboard::Persistence::RedisStore.new(
14
+ url: 'redis://localhost:6379/1' # Use DB 1 for demo
15
+ )
16
+
17
+ # Clear previous demo data
18
+ redis_store.redis.flushdb
19
+ puts "(Cleared previous Redis data)"
20
+
21
+ engine = KBS::Blackboard::Engine.new(store: redis_store)
22
+
23
+ # Add trading rules (raw API since Blackboard::Engine doesn't use DSL)
24
+ spread_rule = KBS::Rule.new('tight_spread_opportunity') do |r|
25
+ r.conditions << KBS::Condition.new(:market_price, { symbol: 'AAPL' })
26
+ r.conditions << KBS::Condition.new(:order, { symbol: 'AAPL', type: 'BUY' })
27
+ r.action = ->(facts) {
28
+ price = facts.find { |f| f.type == :market_price }
29
+ order = facts.find { |f| f.type == :order }
30
+ spread = 0.02 # Tight spread
31
+ puts "\n💹 TRADING SIGNAL: Tight spread opportunity for #{price[:symbol]}"
32
+ puts " Price: $#{price[:price]}, Spread: $#{spread}"
33
+ puts " Order: #{order[:type]} #{order[:quantity]} @ $#{order[:limit]}"
34
+ }
35
+ end
36
+
37
+ high_volume_rule = KBS::Rule.new('high_volume_alert') do |r|
38
+ r.conditions << KBS::Condition.new(:market_price, { volume: ->(v) { v > 800 } })
39
+ r.action = ->(facts) {
40
+ price = facts.first
41
+ puts "\n📊 HIGH VOLUME: #{price[:symbol]} trading at #{price[:volume]} shares"
42
+ }
43
+ end
44
+
45
+ engine.add_rule(spread_rule)
46
+ engine.add_rule(high_volume_rule)
47
+
48
+ puts "\nAdding high-frequency market data..."
49
+ price1 = engine.add_fact(:market_price, { symbol: "AAPL", price: 150.25, volume: 1000 })
50
+ price2 = engine.add_fact(:market_price, { symbol: "GOOGL", price: 2800.50, volume: 500 })
51
+ order = engine.add_fact(:order, { symbol: "AAPL", type: "BUY", quantity: 100, limit: 150.00 })
52
+
53
+ puts "Facts added with UUIDs:"
54
+ puts " AAPL Price: #{price1.uuid}"
55
+ puts " GOOGL Price: #{price2.uuid}"
56
+ puts " Order: #{order.uuid}"
57
+
58
+ puts "\nRunning inference engine..."
59
+ engine.run
60
+
61
+ puts "\nPosting trading messages..."
62
+ engine.post_message("MarketDataFeed", "prices", { symbol: "AAPL", bid: 150.24, ask: 150.26 }, priority: 10)
63
+ engine.post_message("OrderManager", "orders", { action: "fill", order_id: order.uuid }, priority: 5)
64
+
65
+ puts "\nConsuming highest priority message..."
66
+ message = engine.consume_message("prices", "TradingStrategy")
67
+ puts " Received: #{message[:content]}" if message
68
+
69
+ puts "\nQuerying market prices..."
70
+ prices = engine.blackboard.get_facts(:market_price)
71
+ puts " Found #{prices.size} price(s):"
72
+ prices.each { |p| puts " - #{p}" }
73
+
74
+ puts "\nRedis Statistics:"
75
+ stats = engine.stats
76
+ stats.each do |key, value|
77
+ puts " #{key.to_s.gsub('_', ' ').capitalize}: #{value}"
78
+ end
79
+
80
+ engine.blackboard.close
81
+
82
+ rescue Redis::CannotConnectError => e
83
+ puts "\n⚠️ Redis not available: #{e.message}"
84
+ puts " Please start Redis: redis-server"
85
+ puts " Or install Redis: brew install redis (macOS)"
86
+ end
87
+
88
+ # Demo 2: Hybrid Store (Redis + SQLite)
89
+ puts "\n\n=== Hybrid Store Demo ==="
90
+ puts "Redis for fast fact access + SQLite for durable audit trail"
91
+
92
+ begin
93
+ hybrid_store = KBS::Blackboard::Persistence::HybridStore.new(
94
+ redis_url: 'redis://localhost:6379/2', # Use DB 2 for demo
95
+ db_path: ':memory:' # In-memory SQLite for demo
96
+ )
97
+
98
+ # Clear previous demo data from Redis
99
+ hybrid_store.redis_store.redis.flushdb
100
+ puts "(Cleared previous Redis data)"
101
+
102
+ engine = KBS::Blackboard::Engine.new(store: hybrid_store)
103
+
104
+ # Add monitoring rules (raw API since Blackboard::Engine doesn't use DSL)
105
+ temp_alert = KBS::Rule.new('temperature_alert') do |r|
106
+ r.conditions << KBS::Condition.new(:sensor, {
107
+ type: 'temperature',
108
+ value: ->(v) { v > 25 }
109
+ })
110
+ r.action = ->(facts) {
111
+ sensor = facts.first
112
+ puts "\n🌡️ TEMPERATURE ALERT: #{sensor[:location]} at #{sensor[:value]}°C (threshold: 25°C)"
113
+ }
114
+ end
115
+
116
+ cpu_warning = KBS::Rule.new('cpu_warning') do |r|
117
+ r.conditions << KBS::Condition.new(:sensor, {
118
+ type: 'cpu_usage',
119
+ value: ->(v) { v > 40 }
120
+ })
121
+ r.action = ->(facts) {
122
+ sensor = facts.first
123
+ puts "\n⚙️ CPU WARNING: #{sensor[:location]} at #{sensor[:value]}% (threshold: 40%)"
124
+ }
125
+ end
126
+
127
+ engine.add_rule(temp_alert)
128
+ engine.add_rule(cpu_warning)
129
+
130
+ puts "\nAdding facts (stored in Redis)..."
131
+ sensor1 = engine.add_fact(:sensor, { location: "trading_floor", type: "temperature", value: 22 })
132
+ sensor2 = engine.add_fact(:sensor, { location: "data_center", type: "cpu_usage", value: 45 })
133
+
134
+ puts "Facts added:"
135
+ puts " Sensor 1: #{sensor1.uuid}"
136
+ puts " Sensor 2: #{sensor2.uuid}"
137
+
138
+ puts "\nRunning inference engine..."
139
+ engine.run
140
+
141
+ puts "\nUpdating sensor value (audit logged to SQLite)..."
142
+ sensor1[:value] = 28
143
+
144
+ puts "\nRunning inference again after update..."
145
+ engine.run
146
+
147
+ puts "\nFact History from SQLite Audit Log:"
148
+ history = engine.blackboard.get_history(limit: 5)
149
+ history.each do |entry|
150
+ puts " [#{entry[:timestamp].strftime('%H:%M:%S')}] #{entry[:action]}: #{entry[:fact_type]}(#{entry[:attributes]})"
151
+ end
152
+
153
+ puts "\nHybrid Store Benefits:"
154
+ puts " ✓ Facts in Redis (fast reads/writes)"
155
+ puts " ✓ Messages in Redis (real-time messaging)"
156
+ puts " ✓ Audit trail in SQLite (durable, queryable)"
157
+ puts " ✓ Best of both worlds for production systems"
158
+
159
+ puts "\nStatistics:"
160
+ stats = engine.stats
161
+ stats.each do |key, value|
162
+ puts " #{key.to_s.gsub('_', ' ').capitalize}: #{value}"
163
+ end
164
+
165
+ engine.blackboard.close
166
+
167
+ rescue Redis::CannotConnectError => e
168
+ puts "\n⚠️ Redis not available: #{e.message}"
169
+ puts " Hybrid store requires Redis for fact storage"
170
+ end
171
+
172
+ puts "\n" + "=" * 70
173
+ puts "Demo complete!"
174
+ puts "\nComparison:"
175
+ puts " SQLite Store: Durable, transactional, embedded (no server needed)"
176
+ puts " Redis Store: Fast (100x), distributed, requires Redis server"
177
+ puts " Hybrid Store: Fast facts + durable audit (production recommended)"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run all example files in the examples directory
5
+
6
+ require 'pathname'
7
+
8
+ # Get the directory where this script is located
9
+ examples_dir = Pathname.new(__FILE__).dirname
10
+
11
+ # Find all Ruby files except this one
12
+ example_files = Dir.glob(examples_dir.join('*.rb'))
13
+ .reject { |f| File.basename(f).start_with? 'run_all' }
14
+ .reject { |f| File.basename(f).end_with? '_dsl.rb' }
15
+ .sort
16
+
17
+ puts
18
+ puts "Running #{example_files.size} examples from #{examples_dir}"
19
+ puts
20
+ puts
21
+
22
+ example_files.each_with_index do |file, index|
23
+ filename = File.basename(file)
24
+ filename_size = filename.size + 6
25
+
26
+ STDERR.puts "Running example #{index + 1}/#{example_files.size}: #{filename} ..."
27
+
28
+ puts
29
+ puts "=" * filename_size
30
+ puts "## #{filename} ##"
31
+ puts "=" * filename_size
32
+ puts
33
+
34
+ # Run the example
35
+ system("ruby", file)
36
+
37
+ exit_status = $?.exitstatus
38
+
39
+ if exit_status != 0
40
+ puts
41
+ puts "⚠️ Example #{filename} exited with status #{exit_status}"
42
+ end
43
+
44
+ puts
45
+ end
46
+
47
+ puts
48
+ puts
49
+ puts "Completed running all #{example_files.size} examples"
50
+ puts
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run all example DSL files in the examples directory
5
+
6
+ require 'pathname'
7
+
8
+ # Get the directory where this script is located
9
+ examples_dir = Pathname.new(__FILE__).dirname
10
+
11
+ # Find all Ruby files except this one
12
+ example_files = Dir.glob(examples_dir.join('*_dsl.rb'))
13
+ .reject { |f| File.basename(f).start_with? 'run_all' }
14
+ .sort
15
+
16
+ puts
17
+ puts "Running #{example_files.size} examples from #{examples_dir}"
18
+ puts
19
+ puts
20
+
21
+ example_files.each_with_index do |file, index|
22
+ filename = File.basename(file)
23
+ filename_size = filename.size + 6
24
+
25
+ STDERR.puts "Running example #{index + 1}/#{example_files.size}: #{filename} ..."
26
+
27
+ puts
28
+ puts "=" * filename_size
29
+ puts "## #{filename} ##"
30
+ puts "=" * filename_size
31
+ puts
32
+
33
+ # Run the example
34
+ system("ruby", file)
35
+
36
+ exit_status = $?.exitstatus
37
+
38
+ if exit_status != 0
39
+ puts
40
+ puts "⚠️ Example #{filename} exited with status #{exit_status}"
41
+ end
42
+
43
+ puts
44
+ end
45
+
46
+ puts
47
+ puts
48
+ puts "Completed running all #{example_files.size} examples"
49
+ puts
@@ -4,7 +4,7 @@ require_relative '../lib/kbs'
4
4
 
5
5
  class AdvancedStockTradingSystem
6
6
  def initialize
7
- @engine = KBS::ReteEngine.new
7
+ @engine = KBS::Engine.new
8
8
  @portfolio = {
9
9
  cash: 100000,
10
10
  positions: {},
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs/dsl'
4
+
5
+ class AdvancedStockTradingSystem
6
+ include KBS::DSL::ConditionHelpers
7
+
8
+ def initialize
9
+ @kb = nil
10
+ @portfolio = {
11
+ cash: 100000,
12
+ positions: {},
13
+ total_value: 100000
14
+ }
15
+ @market_data = {}
16
+ setup_rules
17
+ end
18
+
19
+ def setup_rules
20
+ @kb = KBS.knowledge_base do
21
+ rule "golden_cross" do
22
+ priority 15
23
+ on :technical,
24
+ indicator: "ma_crossover",
25
+ ma50: satisfies { |v| v > 0 },
26
+ ma200: satisfies { |v| v > 0 }
27
+ on :stock, volume: satisfies { |v| v > 1000000 }
28
+ without.on :position, status: "open"
29
+
30
+ perform do |facts|
31
+ tech = facts.find { |f| f.type == :technical }
32
+ stock = facts.find { |f| f.type == :stock }
33
+ if tech && stock && tech[:ma50] > tech[:ma200] && tech[:ma50_prev] <= tech[:ma200_prev]
34
+ puts "📈 GOLDEN CROSS: #{stock[:symbol]}"
35
+ puts " 50-MA: $#{tech[:ma50].round(2)}, 200-MA: $#{tech[:ma200].round(2)}"
36
+ puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
37
+ puts " ACTION: Strong BUY signal"
38
+ end
39
+ end
40
+ end
41
+
42
+ rule "momentum_breakout" do
43
+ priority 12
44
+ on :stock,
45
+ price_change: satisfies { |v| v > 3 },
46
+ volume_ratio: satisfies { |v| v > 1.5 },
47
+ rsi: satisfies { |v| v.between?(40, 70) }
48
+ on :market, sentiment: satisfies { |s| ["bullish", "neutral"].include?(s) }
49
+
50
+ perform do |facts|
51
+ stock = facts.find { |f| f.type == :stock }
52
+ puts "🚀 MOMENTUM BREAKOUT: #{stock[:symbol]}"
53
+ puts " Price Change: +#{stock[:price_change]}%"
54
+ puts " Volume Spike: #{stock[:volume_ratio]}x average"
55
+ puts " RSI: #{stock[:rsi]}"
56
+ puts " ACTION: Momentum BUY"
57
+ end
58
+ end
59
+
60
+ rule "oversold_bounce" do
61
+ priority 10
62
+ on :stock,
63
+ rsi: satisfies { |v| v < 30 },
64
+ price: satisfies { |p| p > 0 }
65
+ on :support, level: satisfies { |l| l > 0 }
66
+
67
+ perform do |facts|
68
+ stock = facts.find { |f| f.type == :stock }
69
+ support = facts.find { |f| f.type == :support }
70
+ if stock && support && stock[:price] >= support[:level] * 0.98
71
+ puts "🔄 OVERSOLD REVERSAL: #{stock[:symbol]}"
72
+ puts " RSI: #{stock[:rsi]} (oversold)"
73
+ puts " Price: $#{stock[:price]} near support $#{support[:level].round(2)}"
74
+ puts " ACTION: Reversal BUY opportunity"
75
+ end
76
+ end
77
+ end
78
+
79
+ rule "trailing_stop" do
80
+ priority 18
81
+ on :position,
82
+ status: "open",
83
+ profit_pct: satisfies { |p| p > 5 }
84
+ on :stock, price: satisfies { |p| p > 0 }
85
+
86
+ perform do |facts|
87
+ position = facts.find { |f| f.type == :position }
88
+ stock = facts.find { |f| f.type == :stock }
89
+ if position && stock
90
+ trailing_stop = position[:high_water] * 0.95
91
+ if stock[:price] <= trailing_stop
92
+ puts "🛑 TRAILING STOP: #{position[:symbol]}"
93
+ puts " Entry: $#{position[:entry_price]}"
94
+ puts " Current: $#{stock[:price]}"
95
+ puts " Stop: $#{trailing_stop.round(2)}"
96
+ puts " Profit: #{position[:profit_pct].round(1)}%"
97
+ puts " ACTION: SELL to lock profits"
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ rule "position_sizing" do
104
+ priority 8
105
+ on :signal,
106
+ action: "buy",
107
+ confidence: satisfies { |c| c > 0.6 }
108
+ on :portfolio, cash: satisfies { |c| c > 1000 }
109
+
110
+ perform do |facts|
111
+ signal = facts.find { |f| f.type == :signal }
112
+ portfolio = facts.find { |f| f.type == :portfolio }
113
+ if signal && portfolio
114
+ kelly = (signal[:confidence] * signal[:expected_return] - (1 - signal[:confidence])) / signal[:expected_return]
115
+ position_size = portfolio[:cash] * [kelly * 0.25, 0.1].min
116
+ puts "📊 POSITION SIZING: #{signal[:symbol]}"
117
+ puts " Confidence: #{(signal[:confidence] * 100).round}%"
118
+ puts " Kelly %: #{(kelly * 100).round(1)}%"
119
+ puts " Suggested Size: $#{position_size.round(0)}"
120
+ end
121
+ end
122
+ end
123
+
124
+ rule "earnings_play" do
125
+ priority 11
126
+ on :earnings,
127
+ days_until: satisfies { |d| d.between?(1, 5) },
128
+ expected_move: satisfies { |m| m > 5 }
129
+ on :options,
130
+ iv: satisfies { |v| v > 30 },
131
+ iv_rank: satisfies { |r| r > 50 }
132
+
133
+ perform do |facts|
134
+ earnings = facts.find { |f| f.type == :earnings }
135
+ options = facts.find { |f| f.type == :options }
136
+ puts "💰 EARNINGS PLAY: #{earnings[:symbol]}"
137
+ puts " Days to Earnings: #{earnings[:days_until]}"
138
+ puts " Expected Move: ±#{earnings[:expected_move]}%"
139
+ puts " IV: #{options[:iv]}% (Rank: #{options[:iv_rank]})"
140
+ puts " ACTION: Consider volatility strategy"
141
+ end
142
+ end
143
+
144
+ rule "sector_rotation" do
145
+ priority 7
146
+ on :sector,
147
+ performance: satisfies { |p| p > 1.1 },
148
+ trend: "up"
149
+ on :position,
150
+ sector: satisfies { |s| s != nil },
151
+ profit_pct: satisfies { |p| p < 2 }
152
+
153
+ perform do |facts|
154
+ strong_sector = facts.find { |f| f.type == :sector }
155
+ weak_position = facts.find { |f| f.type == :position }
156
+ if strong_sector && weak_position && strong_sector[:name] != weak_position[:sector]
157
+ puts "🔄 SECTOR ROTATION"
158
+ puts " From: #{weak_position[:sector]} (underperforming)"
159
+ puts " To: #{strong_sector[:name]} (RS: #{strong_sector[:performance]})"
160
+ puts " ACTION: Rotate portfolio allocation"
161
+ end
162
+ end
163
+ end
164
+
165
+ rule "risk_concentration" do
166
+ priority 16
167
+ on :portfolio_metrics, concentration: satisfies { |c| c > 0.3 }
168
+ on :market, volatility: satisfies { |v| v > 25 }
169
+
170
+ perform do |facts|
171
+ metrics = facts.find { |f| f.type == :portfolio_metrics }
172
+ puts "⚠️ RISK CONCENTRATION ALERT"
173
+ puts " Top Position: #{(metrics[:concentration] * 100).round}% of portfolio"
174
+ puts " Market Volatility: Elevated"
175
+ puts " ACTION: Reduce position sizes for risk management"
176
+ end
177
+ end
178
+
179
+ rule "vwap_reversion" do
180
+ priority 9
181
+ on :intraday, distance_from_vwap: satisfies { |d| d.abs > 2 }
182
+
183
+ perform do |facts|
184
+ intraday = facts.find { |f| f.type == :intraday }
185
+ direction = intraday[:distance_from_vwap] > 0 ? "OVERBOUGHT" : "OVERSOLD"
186
+ puts "📊 VWAP REVERSION: #{intraday[:symbol]}"
187
+ puts " Status: #{direction}"
188
+ puts " Distance: #{intraday[:distance_from_vwap].round(1)} std devs"
189
+ puts " Current: $#{intraday[:price]}"
190
+ puts " VWAP: $#{intraday[:vwap].round(2)}"
191
+ puts " ACTION: Mean reversion trade"
192
+ end
193
+ end
194
+
195
+ rule "news_sentiment" do
196
+ priority 13
197
+ on :news,
198
+ sentiment: satisfies { |s| s.abs > 0.7 },
199
+ volume: satisfies { |v| v > 10 }
200
+ on :stock, price_change: satisfies { |p| p.abs < 2 }
201
+
202
+ perform do |facts|
203
+ news = facts.find { |f| f.type == :news }
204
+ stock = facts.find { |f| f.type == :stock }
205
+ sentiment = news[:sentiment] > 0 ? "POSITIVE" : "NEGATIVE"
206
+ action = news[:sentiment] > 0 ? "BUY" : "SELL"
207
+ puts "📰 NEWS CATALYST: #{stock[:symbol]}"
208
+ puts " Sentiment: #{sentiment} (#{news[:sentiment].round(2)})"
209
+ puts " News Volume: #{news[:volume]} articles"
210
+ puts " Price Reaction: #{stock[:price_change].round(1)}% (lagging)"
211
+ puts " ACTION: #{action} on sentiment divergence"
212
+ end
213
+ end
214
+
215
+ rule "correlation_warning" do
216
+ priority 6
217
+ on :correlation, value: satisfies { |v| v > 0.8 }
218
+
219
+ perform do |facts|
220
+ corr = facts.find { |f| f.type == :correlation }
221
+ puts "⚠️ HIGH CORRELATION"
222
+ puts " Pairs: #{corr[:symbol1]} <-> #{corr[:symbol2]}"
223
+ puts " Correlation: #{corr[:value].round(2)}"
224
+ puts " ACTION: Diversify or hedge positions"
225
+ end
226
+ end
227
+
228
+ rule "gap_fade" do
229
+ priority 8
230
+ on :gap, size: satisfies { |s| s.abs > 2 }
231
+ on :stock, atr: satisfies { |a| a > 0 }
232
+
233
+ perform do |facts|
234
+ gap = facts.find { |f| f.type == :gap }
235
+ stock = facts.find { |f| f.type == :stock }
236
+ if gap && stock
237
+ gap_multiple = gap[:size].abs / (stock[:atr] / stock[:price] * 100)
238
+ if gap_multiple > 2
239
+ direction = gap[:size] > 0 ? "SHORT" : "LONG"
240
+ puts "📉 GAP FADE: #{stock[:symbol]}"
241
+ puts " Gap: #{gap[:size].round(1)}%"
242
+ puts " ATR Multiple: #{gap_multiple.round(1)}x"
243
+ puts " ACTION: #{direction} fade trade"
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def simulate_market_tick(symbols)
252
+ symbols.each do |symbol|
253
+ @market_data[symbol] ||= {
254
+ price: 100 + rand(50),
255
+ ma50: 100,
256
+ ma200: 100,
257
+ volume: 1000000,
258
+ rsi: 50,
259
+ high_water: 100
260
+ }
261
+
262
+ data = @market_data[symbol]
263
+
264
+ price_change = rand(-5.0..5.0)
265
+ data[:price] = (data[:price] * (1 + price_change / 100)).round(2)
266
+ data[:ma50] = (data[:ma50] * 0.98 + data[:price] * 0.02).round(2)
267
+ data[:ma200] = (data[:ma200] * 0.995 + data[:price] * 0.005).round(2)
268
+ data[:volume] = (1000000 * (0.5 + rand * 2)).to_i
269
+ data[:rsi] = [[data[:rsi] + rand(-10..10), 0].max, 100].min
270
+ data[:high_water] = [data[:high_water], data[:price]].max
271
+
272
+ @kb.fact :stock, {
273
+ symbol: symbol,
274
+ price: data[:price],
275
+ volume: data[:volume],
276
+ rsi: data[:rsi],
277
+ price_change: price_change,
278
+ volume_ratio: data[:volume] / 1000000.0,
279
+ atr: rand(1.0..3.0)
280
+ }
281
+
282
+ @kb.fact :technical, {
283
+ symbol: symbol,
284
+ indicator: "ma_crossover",
285
+ ma50: data[:ma50],
286
+ ma200: data[:ma200],
287
+ ma50_prev: data[:ma50] - rand(-1.0..1.0),
288
+ ma200_prev: data[:ma200] - rand(-0.5..0.5)
289
+ }
290
+
291
+ @kb.fact :support, {
292
+ symbol: symbol,
293
+ level: data[:price] * 0.95
294
+ }
295
+
296
+ if rand > 0.7
297
+ @kb.fact :intraday, {
298
+ symbol: symbol,
299
+ price: data[:price],
300
+ vwap: data[:price] + rand(-2.0..2.0),
301
+ distance_from_vwap: rand(-3.0..3.0)
302
+ }
303
+ end
304
+
305
+ if rand > 0.8
306
+ @kb.fact :news, {
307
+ symbol: symbol,
308
+ sentiment: rand(-1.0..1.0),
309
+ volume: rand(5..50)
310
+ }
311
+ end
312
+
313
+ if rand > 0.85
314
+ @kb.fact :earnings, {
315
+ symbol: symbol,
316
+ days_until: rand(1..10),
317
+ expected_move: rand(3.0..15.0)
318
+ }
319
+
320
+ @kb.fact :options, {
321
+ symbol: symbol,
322
+ iv: rand(20..80),
323
+ iv_rank: rand(0..100)
324
+ }
325
+ end
326
+
327
+ if rand > 0.9
328
+ @kb.fact :gap, {
329
+ symbol: symbol,
330
+ size: rand(-5.0..5.0)
331
+ }
332
+ end
333
+ end
334
+
335
+ @kb.fact :market, {
336
+ sentiment: ["bullish", "neutral", "bearish"].sample,
337
+ volatility: rand(10..40)
338
+ }
339
+
340
+ if @portfolio[:positions].any?
341
+ total_value = @portfolio[:cash] + @portfolio[:positions].values.sum { |p| p[:value] }
342
+ max_position = @portfolio[:positions].values.map { |p| p[:value] }.max || 0
343
+ @kb.fact :portfolio_metrics, {
344
+ concentration: max_position.to_f / total_value
345
+ }
346
+ end
347
+
348
+ @kb.fact :portfolio, {
349
+ cash: @portfolio[:cash],
350
+ risk_tolerance: 0.5
351
+ }
352
+
353
+ if rand > 0.85
354
+ symbols_pair = symbols.sample(2)
355
+ @kb.fact :correlation, {
356
+ symbol1: symbols_pair[0],
357
+ symbol2: symbols_pair[1],
358
+ value: rand(0.5..0.95)
359
+ }
360
+ end
361
+
362
+ sectors = ["Technology", "Healthcare", "Finance", "Energy", "Consumer"]
363
+ @kb.fact :sector, {
364
+ name: sectors.sample,
365
+ performance: rand(0.8..1.3),
366
+ trend: ["up", "down", "sideways"].sample
367
+ }
368
+ end
369
+
370
+ def run_simulation(symbols: ["AAPL", "GOOGL", "MSFT", "NVDA"], iterations: 10)
371
+ puts "\n" + "=" * 80
372
+ puts "ADVANCED STOCK TRADING SYSTEM"
373
+ puts "=" * 80
374
+ puts "Initial Capital: $100,000"
375
+ puts "Symbols: #{symbols.join(', ')}"
376
+ puts "Rules: #{@kb.engine.rules.size} active trading strategies"
377
+ puts "=" * 80
378
+
379
+ iterations.times do |i|
380
+ puts "\n⏰ MARKET TICK #{i + 1} - #{Time.now.strftime('%H:%M:%S')}"
381
+ puts "-" * 60
382
+
383
+ @kb.reset
384
+
385
+ simulate_market_tick(symbols)
386
+
387
+ @kb.run
388
+
389
+ sleep(0.3) if i < iterations - 1
390
+ end
391
+
392
+ puts "\n" + "=" * 80
393
+ puts "SIMULATION COMPLETE"
394
+ puts "=" * 80
395
+ end
396
+ end
397
+
398
+ if __FILE__ == $0
399
+ system = AdvancedStockTradingSystem.new
400
+ system.run_simulation(
401
+ symbols: ["AAPL", "GOOGL", "MSFT", "NVDA", "TSLA", "META", "AMZN"],
402
+ iterations: 12
403
+ )
404
+ end