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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- 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)"
|
data/examples/run_all.rb
ADDED
|
@@ -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
|
|
@@ -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
|