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.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +481 -0
- data/Rakefile +8 -0
- data/examples/README.md +531 -0
- data/examples/advanced_example.rb +270 -0
- data/examples/ai_enhanced_kbs.rb +523 -0
- data/examples/blackboard_demo.rb +50 -0
- data/examples/car_diagnostic.rb +64 -0
- data/examples/concurrent_inference_demo.rb +363 -0
- data/examples/csv_trading_system.rb +559 -0
- data/examples/iot_demo_using_dsl.rb +83 -0
- data/examples/portfolio_rebalancing_system.rb +651 -0
- data/examples/redis_trading_demo.rb +177 -0
- data/examples/sample_stock_data.csv +46 -0
- data/examples/stock_trading_advanced.rb +469 -0
- data/examples/stock_trading_system.rb.bak +563 -0
- data/examples/timestamped_trading.rb +286 -0
- data/examples/trading_demo.rb +334 -0
- data/examples/working_demo.rb +176 -0
- data/lib/kbs/alpha_memory.rb +37 -0
- data/lib/kbs/beta_memory.rb +57 -0
- data/lib/kbs/blackboard/audit_log.rb +115 -0
- data/lib/kbs/blackboard/engine.rb +83 -0
- data/lib/kbs/blackboard/fact.rb +65 -0
- data/lib/kbs/blackboard/memory.rb +191 -0
- data/lib/kbs/blackboard/message_queue.rb +96 -0
- data/lib/kbs/blackboard/persistence/hybrid_store.rb +118 -0
- data/lib/kbs/blackboard/persistence/redis_store.rb +218 -0
- data/lib/kbs/blackboard/persistence/sqlite_store.rb +242 -0
- data/lib/kbs/blackboard/persistence/store.rb +55 -0
- data/lib/kbs/blackboard/redis_audit_log.rb +107 -0
- data/lib/kbs/blackboard/redis_message_queue.rb +111 -0
- data/lib/kbs/blackboard.rb +23 -0
- data/lib/kbs/condition.rb +26 -0
- data/lib/kbs/dsl/condition_helpers.rb +57 -0
- data/lib/kbs/dsl/knowledge_base.rb +86 -0
- data/lib/kbs/dsl/pattern_evaluator.rb +69 -0
- data/lib/kbs/dsl/rule_builder.rb +115 -0
- data/lib/kbs/dsl/variable.rb +35 -0
- data/lib/kbs/dsl.rb +18 -0
- data/lib/kbs/fact.rb +43 -0
- data/lib/kbs/join_node.rb +117 -0
- data/lib/kbs/negation_node.rb +88 -0
- data/lib/kbs/production_node.rb +28 -0
- data/lib/kbs/rete_engine.rb +108 -0
- data/lib/kbs/rule.rb +46 -0
- data/lib/kbs/token.rb +37 -0
- data/lib/kbs/version.rb +5 -0
- data/lib/kbs/working_memory.rb +32 -0
- data/lib/kbs.rb +20 -0
- metadata +164 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/kbs'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
class TimestampedTradingSystem
|
|
7
|
+
def initialize
|
|
8
|
+
@engine = KBS::ReteEngine.new
|
|
9
|
+
@market_open = Time.parse("09:30:00")
|
|
10
|
+
@market_close = Time.parse("16:00:00")
|
|
11
|
+
setup_time_aware_rules
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def setup_time_aware_rules
|
|
15
|
+
# Rule 1: Fresh data only
|
|
16
|
+
fresh_momentum_rule = KBS::Rule.new(
|
|
17
|
+
"fresh_momentum",
|
|
18
|
+
conditions: [
|
|
19
|
+
KBS::Condition.new(:stock, {
|
|
20
|
+
price_change: ->(p) { p && p > 2 },
|
|
21
|
+
timestamp: ->(t) { t && (Time.now - t) < 60 }, # Within 1 minute
|
|
22
|
+
market_session: "regular"
|
|
23
|
+
})
|
|
24
|
+
],
|
|
25
|
+
action: lambda do |facts, bindings|
|
|
26
|
+
stock = facts.find { |f| f.type == :stock }
|
|
27
|
+
age = Time.now - stock[:timestamp]
|
|
28
|
+
puts "🚀 FRESH MOMENTUM: #{stock[:symbol]}"
|
|
29
|
+
puts " Price Change: +#{stock[:price_change]}%"
|
|
30
|
+
puts " Data Age: #{age.round(1)} seconds"
|
|
31
|
+
puts " Market Time: #{stock[:market_time]}"
|
|
32
|
+
puts " Recommendation: BUY (fresh signal)"
|
|
33
|
+
end,
|
|
34
|
+
priority: 15
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Rule 2: Stale data warning
|
|
38
|
+
stale_data_rule = KBS::Rule.new(
|
|
39
|
+
"stale_data_warning",
|
|
40
|
+
conditions: [
|
|
41
|
+
KBS::Condition.new(:stock, {
|
|
42
|
+
timestamp: ->(t) { t && (Time.now - t) > 300 } # Older than 5 minutes
|
|
43
|
+
})
|
|
44
|
+
],
|
|
45
|
+
action: lambda do |facts, bindings|
|
|
46
|
+
stock = facts.find { |f| f.type == :stock }
|
|
47
|
+
age = Time.now - stock[:timestamp]
|
|
48
|
+
puts "⚠️ STALE DATA: #{stock[:symbol]}"
|
|
49
|
+
puts " Data Age: #{(age / 60).round(1)} minutes"
|
|
50
|
+
puts " Last Update: #{stock[:timestamp]}"
|
|
51
|
+
puts " ACTION: IGNORE - Data too old"
|
|
52
|
+
end,
|
|
53
|
+
priority: 20
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Rule 3: Market hours validation
|
|
57
|
+
market_hours_rule = KBS::Rule.new(
|
|
58
|
+
"market_hours_check",
|
|
59
|
+
conditions: [
|
|
60
|
+
KBS::Condition.new(:stock, {
|
|
61
|
+
market_session: "after_hours",
|
|
62
|
+
volume: ->(v) { v && v > 100_000 }
|
|
63
|
+
})
|
|
64
|
+
],
|
|
65
|
+
action: lambda do |facts, bindings|
|
|
66
|
+
stock = facts.find { |f| f.type == :stock }
|
|
67
|
+
puts "🌙 AFTER HOURS ACTIVITY: #{stock[:symbol]}"
|
|
68
|
+
puts " Volume: #{format_volume(stock[:volume])}"
|
|
69
|
+
puts " Time: #{stock[:market_time]}"
|
|
70
|
+
puts " ACTION: Monitor for gap potential"
|
|
71
|
+
end
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Rule 4: Rapid price movement detection
|
|
75
|
+
rapid_movement_rule = KBS::Rule.new(
|
|
76
|
+
"rapid_movement",
|
|
77
|
+
conditions: [
|
|
78
|
+
KBS::Condition.new(:price_tick, {
|
|
79
|
+
time_delta: ->(d) { d && d < 5 }, # Within 5 seconds
|
|
80
|
+
price_delta: ->(d) { d && d.abs > 0.50 } # More than $0.50 move
|
|
81
|
+
})
|
|
82
|
+
],
|
|
83
|
+
action: lambda do |facts, bindings|
|
|
84
|
+
tick = facts.find { |f| f.type == :price_tick }
|
|
85
|
+
direction = tick[:price_delta] > 0 ? "📈 UP" : "📉 DOWN"
|
|
86
|
+
puts "⚡ RAPID MOVEMENT: #{tick[:symbol]} #{direction}"
|
|
87
|
+
puts " Price Change: #{tick[:price_delta] > 0 ? '+' : ''}$#{tick[:price_delta]}"
|
|
88
|
+
puts " Time Frame: #{tick[:time_delta]} seconds"
|
|
89
|
+
puts " ACTION: Check for news catalyst"
|
|
90
|
+
end
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Rule 5: Time-based strategy
|
|
94
|
+
opening_gap_rule = KBS::Rule.new(
|
|
95
|
+
"opening_gap",
|
|
96
|
+
conditions: [
|
|
97
|
+
KBS::Condition.new(:stock, {
|
|
98
|
+
market_time: ->(t) {
|
|
99
|
+
t && (t.hour == 9 && t.min >= 30 && t.min <= 35) # First 5 minutes
|
|
100
|
+
},
|
|
101
|
+
gap_percentage: ->(g) { g && g.abs > 1 }
|
|
102
|
+
})
|
|
103
|
+
],
|
|
104
|
+
action: lambda do |facts, bindings|
|
|
105
|
+
stock = facts.find { |f| f.type == :stock }
|
|
106
|
+
direction = stock[:gap_percentage] > 0 ? "GAP UP" : "GAP DOWN"
|
|
107
|
+
puts "🔔 OPENING #{direction}: #{stock[:symbol]}"
|
|
108
|
+
puts " Gap: #{stock[:gap_percentage] > 0 ? '+' : ''}#{stock[:gap_percentage]}%"
|
|
109
|
+
puts " Time: #{stock[:market_time]}"
|
|
110
|
+
puts " Volume: #{format_volume(stock[:volume])}"
|
|
111
|
+
puts " ACTION: Monitor for gap fill or continuation"
|
|
112
|
+
end
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Rule 6: End of day positioning
|
|
116
|
+
eod_rule = KBS::Rule.new(
|
|
117
|
+
"end_of_day",
|
|
118
|
+
conditions: [
|
|
119
|
+
KBS::Condition.new(:stock, {
|
|
120
|
+
market_time: ->(t) {
|
|
121
|
+
t && (t.hour == 15 && t.min >= 45) # Last 15 minutes
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
KBS::Condition.new(:position, { status: "open" })
|
|
125
|
+
],
|
|
126
|
+
action: lambda do |facts, bindings|
|
|
127
|
+
stock = facts.find { |f| f.type == :stock }
|
|
128
|
+
position = facts.find { |f| f.type == :position }
|
|
129
|
+
puts "🕐 END OF DAY: #{position[:symbol]}"
|
|
130
|
+
puts " Current Time: #{stock[:market_time]}"
|
|
131
|
+
puts " Position P&L: #{position[:unrealized_pnl]}"
|
|
132
|
+
puts " ACTION: Consider closing before market close"
|
|
133
|
+
end
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@engine.add_rule(fresh_momentum_rule)
|
|
137
|
+
@engine.add_rule(stale_data_rule)
|
|
138
|
+
@engine.add_rule(market_hours_rule)
|
|
139
|
+
@engine.add_rule(rapid_movement_rule)
|
|
140
|
+
@engine.add_rule(opening_gap_rule)
|
|
141
|
+
@engine.add_rule(eod_rule)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def format_volume(volume)
|
|
145
|
+
if volume >= 1_000_000
|
|
146
|
+
"#{(volume / 1_000_000.0).round(1)}M"
|
|
147
|
+
elsif volume >= 1_000
|
|
148
|
+
"#{(volume / 1_000.0).round(1)}K"
|
|
149
|
+
else
|
|
150
|
+
volume.to_s
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def determine_market_session(time)
|
|
155
|
+
hour_min = time.hour * 100 + time.min
|
|
156
|
+
|
|
157
|
+
case hour_min
|
|
158
|
+
when 400..929 then "pre_market"
|
|
159
|
+
when 930..1559 then "regular"
|
|
160
|
+
when 1600..2000 then "after_hours"
|
|
161
|
+
else "closed"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def add_timestamped_stock_fact(symbol, price, volume, options = {})
|
|
166
|
+
timestamp = options[:timestamp] || Time.now
|
|
167
|
+
market_time = options[:market_time] || timestamp
|
|
168
|
+
|
|
169
|
+
@engine.add_fact(:stock, {
|
|
170
|
+
symbol: symbol,
|
|
171
|
+
price: price,
|
|
172
|
+
volume: volume,
|
|
173
|
+
timestamp: timestamp,
|
|
174
|
+
market_time: market_time,
|
|
175
|
+
market_session: determine_market_session(market_time),
|
|
176
|
+
price_change: options[:price_change] || 0,
|
|
177
|
+
gap_percentage: options[:gap_percentage] || 0
|
|
178
|
+
})
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def add_price_tick(symbol, old_price, new_price, old_time, new_time)
|
|
182
|
+
price_delta = new_price - old_price
|
|
183
|
+
time_delta = new_time - old_time
|
|
184
|
+
|
|
185
|
+
@engine.add_fact(:price_tick, {
|
|
186
|
+
symbol: symbol,
|
|
187
|
+
old_price: old_price,
|
|
188
|
+
new_price: new_price,
|
|
189
|
+
price_delta: price_delta,
|
|
190
|
+
time_delta: time_delta,
|
|
191
|
+
timestamp: new_time
|
|
192
|
+
})
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def simulate_trading_day
|
|
196
|
+
puts "🏦 TIMESTAMPED STOCK TRADING SYSTEM"
|
|
197
|
+
puts "=" * 60
|
|
198
|
+
|
|
199
|
+
base_time = Time.parse("2024-08-15 09:30:00")
|
|
200
|
+
|
|
201
|
+
# Scenario 1: Market Open with Gap
|
|
202
|
+
puts "\n📊 SCENARIO 1: Market Open Gap Up"
|
|
203
|
+
puts "-" * 40
|
|
204
|
+
@engine.working_memory.facts.clear
|
|
205
|
+
|
|
206
|
+
add_timestamped_stock_fact("AAPL", 185.50, 2_500_000, {
|
|
207
|
+
market_time: base_time + 2*60, # 9:32 AM
|
|
208
|
+
price_change: 2.1,
|
|
209
|
+
gap_percentage: 2.3
|
|
210
|
+
})
|
|
211
|
+
@engine.run
|
|
212
|
+
|
|
213
|
+
# Scenario 2: Fresh Momentum Signal
|
|
214
|
+
puts "\n📊 SCENARIO 2: Fresh Momentum (Recent Data)"
|
|
215
|
+
puts "-" * 40
|
|
216
|
+
@engine.working_memory.facts.clear
|
|
217
|
+
|
|
218
|
+
add_timestamped_stock_fact("GOOGL", 142.80, 1_200_000, {
|
|
219
|
+
timestamp: Time.now - 30, # 30 seconds ago
|
|
220
|
+
market_time: Time.now - 30,
|
|
221
|
+
price_change: 3.2
|
|
222
|
+
})
|
|
223
|
+
@engine.run
|
|
224
|
+
|
|
225
|
+
# Scenario 3: Stale Data
|
|
226
|
+
puts "\n📊 SCENARIO 3: Stale Data Warning"
|
|
227
|
+
puts "-" * 40
|
|
228
|
+
@engine.working_memory.facts.clear
|
|
229
|
+
|
|
230
|
+
add_timestamped_stock_fact("TSLA", 195.40, 800_000, {
|
|
231
|
+
timestamp: Time.now - 600, # 10 minutes ago
|
|
232
|
+
market_time: Time.now - 600,
|
|
233
|
+
price_change: 1.5
|
|
234
|
+
})
|
|
235
|
+
@engine.run
|
|
236
|
+
|
|
237
|
+
# Scenario 4: After Hours Activity
|
|
238
|
+
puts "\n📊 SCENARIO 4: After Hours Trading"
|
|
239
|
+
puts "-" * 40
|
|
240
|
+
@engine.working_memory.facts.clear
|
|
241
|
+
|
|
242
|
+
after_hours_time = Time.parse("2024-08-15 17:30:00")
|
|
243
|
+
add_timestamped_stock_fact("NVDA", 425.80, 150_000, {
|
|
244
|
+
market_time: after_hours_time,
|
|
245
|
+
timestamp: after_hours_time
|
|
246
|
+
})
|
|
247
|
+
@engine.run
|
|
248
|
+
|
|
249
|
+
# Scenario 5: Rapid Price Movement
|
|
250
|
+
puts "\n📊 SCENARIO 5: Rapid Price Tick"
|
|
251
|
+
puts "-" * 40
|
|
252
|
+
@engine.working_memory.facts.clear
|
|
253
|
+
|
|
254
|
+
tick_time = Time.now
|
|
255
|
+
add_price_tick("META", 298.50, 299.25, tick_time - 3, tick_time)
|
|
256
|
+
@engine.run
|
|
257
|
+
|
|
258
|
+
# Scenario 6: End of Day
|
|
259
|
+
puts "\n📊 SCENARIO 6: End of Trading Day"
|
|
260
|
+
puts "-" * 40
|
|
261
|
+
@engine.working_memory.facts.clear
|
|
262
|
+
|
|
263
|
+
eod_time = Time.parse("2024-08-15 15:50:00")
|
|
264
|
+
add_timestamped_stock_fact("AMZN", 142.30, 900_000, {
|
|
265
|
+
market_time: eod_time,
|
|
266
|
+
timestamp: eod_time
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
@engine.add_fact(:position, {
|
|
270
|
+
symbol: "AMZN",
|
|
271
|
+
status: "open",
|
|
272
|
+
shares: 100,
|
|
273
|
+
entry_price: 140.00,
|
|
274
|
+
unrealized_pnl: 230.00
|
|
275
|
+
})
|
|
276
|
+
@engine.run
|
|
277
|
+
|
|
278
|
+
puts "\n" + "=" * 60
|
|
279
|
+
puts "TIMESTAMPED TRADING SIMULATION COMPLETE"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
if __FILE__ == $0
|
|
284
|
+
system = TimestampedTradingSystem.new
|
|
285
|
+
system.simulate_trading_day
|
|
286
|
+
end
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/kbs'
|
|
4
|
+
|
|
5
|
+
class TradingDemo
|
|
6
|
+
def initialize
|
|
7
|
+
@engine = KBS::ReteEngine.new
|
|
8
|
+
setup_trading_rules
|
|
9
|
+
puts "🏦 STOCK TRADING EXPERT SYSTEM LOADED"
|
|
10
|
+
puts "📊 #{@engine.rules.size} trading strategies active"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def setup_trading_rules
|
|
14
|
+
golden_cross = KBS::Rule.new(
|
|
15
|
+
"golden_cross_buy",
|
|
16
|
+
conditions: [
|
|
17
|
+
KBS::Condition.new(:ma_signal, { type: "golden_cross" }),
|
|
18
|
+
KBS::Condition.new(:stock, { volume: ->(v) { v && v > 500000 } })
|
|
19
|
+
],
|
|
20
|
+
action: lambda do |facts, bindings|
|
|
21
|
+
stock = facts.find { |f| f.type == :stock }
|
|
22
|
+
signal = facts.find { |f| f.type == :ma_signal }
|
|
23
|
+
puts "📈 GOLDEN CROSS SIGNAL: #{stock[:symbol]}"
|
|
24
|
+
puts " 50-MA crossed above 200-MA"
|
|
25
|
+
puts " Volume: #{format_volume(stock[:volume])}"
|
|
26
|
+
puts " Price: $#{stock[:price]}"
|
|
27
|
+
puts " Recommendation: STRONG BUY"
|
|
28
|
+
end
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
momentum_buy = KBS::Rule.new(
|
|
32
|
+
"momentum_breakout",
|
|
33
|
+
conditions: [
|
|
34
|
+
KBS::Condition.new(:stock, {
|
|
35
|
+
price_change: ->(p) { p > 2 },
|
|
36
|
+
rsi: ->(r) { r.between?(50, 75) }
|
|
37
|
+
})
|
|
38
|
+
],
|
|
39
|
+
action: lambda do |facts, bindings|
|
|
40
|
+
stock = facts.find { |f| f.type == :stock }
|
|
41
|
+
puts "🚀 MOMENTUM BREAKOUT: #{stock[:symbol]}"
|
|
42
|
+
puts " Price Change: +#{stock[:price_change].round(1)}%"
|
|
43
|
+
puts " RSI: #{stock[:rsi].round(1)} (strong but not overbought)"
|
|
44
|
+
puts " Recommendation: BUY"
|
|
45
|
+
end
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
oversold_buy = KBS::Rule.new(
|
|
49
|
+
"oversold_reversal",
|
|
50
|
+
conditions: [
|
|
51
|
+
KBS::Condition.new(:stock, { rsi: ->(r) { r < 35 } }),
|
|
52
|
+
KBS::Condition.new(:market, { trend: "bullish" })
|
|
53
|
+
],
|
|
54
|
+
action: lambda do |facts, bindings|
|
|
55
|
+
stock = facts.find { |f| f.type == :stock }
|
|
56
|
+
puts "🔄 OVERSOLD REVERSAL: #{stock[:symbol]}"
|
|
57
|
+
puts " RSI: #{stock[:rsi].round(1)} (oversold)"
|
|
58
|
+
puts " Market: Bullish environment"
|
|
59
|
+
puts " Recommendation: CONTRARIAN BUY"
|
|
60
|
+
end
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
earnings_volatility = KBS::Rule.new(
|
|
64
|
+
"earnings_play",
|
|
65
|
+
conditions: [
|
|
66
|
+
KBS::Condition.new(:earnings, { days_until: ->(d) { d <= 3 } }),
|
|
67
|
+
KBS::Condition.new(:options, { iv: ->(v) { v > 40 } })
|
|
68
|
+
],
|
|
69
|
+
action: lambda do |facts, bindings|
|
|
70
|
+
earnings = facts.find { |f| f.type == :earnings }
|
|
71
|
+
options = facts.find { |f| f.type == :options }
|
|
72
|
+
puts "💰 EARNINGS PLAY: #{earnings[:symbol]}"
|
|
73
|
+
puts " Earnings in #{earnings[:days_until]} day(s)"
|
|
74
|
+
puts " Implied Volatility: #{options[:iv].round(1)}%"
|
|
75
|
+
puts " Recommendation: VOLATILITY STRATEGY"
|
|
76
|
+
end
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
stop_loss = KBS::Rule.new(
|
|
80
|
+
"stop_loss_alert",
|
|
81
|
+
conditions: [
|
|
82
|
+
KBS::Condition.new(:position, {
|
|
83
|
+
status: "open",
|
|
84
|
+
loss_pct: ->(l) { l > 8 }
|
|
85
|
+
})
|
|
86
|
+
],
|
|
87
|
+
action: lambda do |facts, bindings|
|
|
88
|
+
position = facts.find { |f| f.type == :position }
|
|
89
|
+
puts "🛑 STOP LOSS TRIGGERED: #{position[:symbol]}"
|
|
90
|
+
puts " Loss: #{position[:loss_pct].round(1)}%"
|
|
91
|
+
puts " Entry: $#{position[:entry_price]}"
|
|
92
|
+
puts " Current: $#{position[:current_price]}"
|
|
93
|
+
puts " Recommendation: SELL IMMEDIATELY"
|
|
94
|
+
end
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
risk_warning = KBS::Rule.new(
|
|
98
|
+
"concentration_risk",
|
|
99
|
+
conditions: [
|
|
100
|
+
KBS::Condition.new(:portfolio, {
|
|
101
|
+
concentration: ->(c) { c > 25 }
|
|
102
|
+
})
|
|
103
|
+
],
|
|
104
|
+
action: lambda do |facts, bindings|
|
|
105
|
+
portfolio = facts.find { |f| f.type == :portfolio }
|
|
106
|
+
puts "⚠️ CONCENTRATION RISK: #{portfolio[:top_holding]}"
|
|
107
|
+
puts " Position Size: #{portfolio[:concentration].round(1)}% of portfolio"
|
|
108
|
+
puts " Recommendation: DIVERSIFY HOLDINGS"
|
|
109
|
+
end
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
news_catalyst = KBS::Rule.new(
|
|
113
|
+
"news_sentiment",
|
|
114
|
+
conditions: [
|
|
115
|
+
KBS::Condition.new(:news, {
|
|
116
|
+
sentiment: ->(s) { s.abs > 0.6 },
|
|
117
|
+
impact: "high"
|
|
118
|
+
})
|
|
119
|
+
],
|
|
120
|
+
action: lambda do |facts, bindings|
|
|
121
|
+
news = facts.find { |f| f.type == :news }
|
|
122
|
+
sentiment = news[:sentiment] > 0 ? "POSITIVE" : "NEGATIVE"
|
|
123
|
+
action = news[:sentiment] > 0 ? "BUY" : "SELL"
|
|
124
|
+
puts "📰 NEWS CATALYST: #{news[:symbol]}"
|
|
125
|
+
puts " Sentiment: #{sentiment} (#{news[:sentiment].round(2)})"
|
|
126
|
+
puts " Impact: HIGH"
|
|
127
|
+
puts " Recommendation: #{action} ON NEWS"
|
|
128
|
+
end
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
sector_rotation = KBS::Rule.new(
|
|
132
|
+
"sector_strength",
|
|
133
|
+
conditions: [
|
|
134
|
+
KBS::Condition.new(:sector, {
|
|
135
|
+
performance: ->(p) { p > 5 },
|
|
136
|
+
trend: "accelerating"
|
|
137
|
+
})
|
|
138
|
+
],
|
|
139
|
+
action: lambda do |facts, bindings|
|
|
140
|
+
sector = facts.find { |f| f.type == :sector }
|
|
141
|
+
puts "🔄 SECTOR ROTATION: #{sector[:name]}"
|
|
142
|
+
puts " Performance: +#{sector[:performance].round(1)}%"
|
|
143
|
+
puts " Trend: Accelerating"
|
|
144
|
+
puts " Recommendation: INCREASE ALLOCATION"
|
|
145
|
+
end
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@engine.add_rule(golden_cross)
|
|
149
|
+
@engine.add_rule(momentum_buy)
|
|
150
|
+
@engine.add_rule(oversold_buy)
|
|
151
|
+
@engine.add_rule(earnings_volatility)
|
|
152
|
+
@engine.add_rule(stop_loss)
|
|
153
|
+
@engine.add_rule(risk_warning)
|
|
154
|
+
@engine.add_rule(news_catalyst)
|
|
155
|
+
@engine.add_rule(sector_rotation)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def format_volume(volume)
|
|
159
|
+
if volume >= 1_000_000
|
|
160
|
+
"#{(volume / 1_000_000.0).round(1)}M"
|
|
161
|
+
elsif volume >= 1_000
|
|
162
|
+
"#{(volume / 1_000.0).round(1)}K"
|
|
163
|
+
else
|
|
164
|
+
volume.to_s
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def generate_scenario(name, &block)
|
|
169
|
+
puts "\n" + "="*60
|
|
170
|
+
puts "SCENARIO: #{name}"
|
|
171
|
+
puts "="*60
|
|
172
|
+
|
|
173
|
+
@engine.working_memory.facts.clear
|
|
174
|
+
|
|
175
|
+
yield
|
|
176
|
+
|
|
177
|
+
puts "\nFacts in working memory:"
|
|
178
|
+
@engine.working_memory.facts.each do |fact|
|
|
179
|
+
puts " #{fact}"
|
|
180
|
+
end
|
|
181
|
+
puts ""
|
|
182
|
+
|
|
183
|
+
@engine.run
|
|
184
|
+
|
|
185
|
+
puts "-"*60
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def demo_scenarios
|
|
189
|
+
generate_scenario("Bull Market with Golden Cross") do
|
|
190
|
+
@engine.add_fact(:stock, {
|
|
191
|
+
symbol: "AAPL",
|
|
192
|
+
price: 185.50,
|
|
193
|
+
volume: 1_250_000,
|
|
194
|
+
price_change: 1.2,
|
|
195
|
+
rsi: 65
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
@engine.add_fact(:ma_signal, {
|
|
199
|
+
symbol: "AAPL",
|
|
200
|
+
type: "golden_cross"
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
@engine.add_fact(:market, { trend: "bullish" })
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
generate_scenario("Momentum Breakout") do
|
|
207
|
+
@engine.add_fact(:stock, {
|
|
208
|
+
symbol: "NVDA",
|
|
209
|
+
price: 425.80,
|
|
210
|
+
volume: 980_000,
|
|
211
|
+
price_change: 4.7,
|
|
212
|
+
rsi: 68
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
@engine.add_fact(:market, { trend: "bullish" })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
generate_scenario("Oversold Bounce Opportunity") do
|
|
219
|
+
@engine.add_fact(:stock, {
|
|
220
|
+
symbol: "TSLA",
|
|
221
|
+
price: 178.90,
|
|
222
|
+
volume: 750_000,
|
|
223
|
+
price_change: -2.1,
|
|
224
|
+
rsi: 28
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
@engine.add_fact(:market, { trend: "bullish" })
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
generate_scenario("Earnings Volatility Play") do
|
|
231
|
+
@engine.add_fact(:earnings, {
|
|
232
|
+
symbol: "GOOGL",
|
|
233
|
+
days_until: 2,
|
|
234
|
+
expected_move: 8.5
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
@engine.add_fact(:options, {
|
|
238
|
+
symbol: "GOOGL",
|
|
239
|
+
iv: 45.2,
|
|
240
|
+
iv_rank: 75
|
|
241
|
+
})
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
generate_scenario("Stop Loss Alert") do
|
|
245
|
+
@engine.add_fact(:position, {
|
|
246
|
+
symbol: "META",
|
|
247
|
+
status: "open",
|
|
248
|
+
entry_price: 320.00,
|
|
249
|
+
current_price: 285.40,
|
|
250
|
+
loss_pct: 10.8,
|
|
251
|
+
shares: 100
|
|
252
|
+
})
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
generate_scenario("Portfolio Risk Warning") do
|
|
256
|
+
@engine.add_fact(:portfolio, {
|
|
257
|
+
total_value: 250_000,
|
|
258
|
+
top_holding: "AAPL",
|
|
259
|
+
concentration: 32.5,
|
|
260
|
+
cash_pct: 5
|
|
261
|
+
})
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
generate_scenario("News-Driven Trade") do
|
|
265
|
+
@engine.add_fact(:news, {
|
|
266
|
+
symbol: "MSFT",
|
|
267
|
+
sentiment: -0.75,
|
|
268
|
+
impact: "high",
|
|
269
|
+
headlines: "Major cloud outage affects services"
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
@engine.add_fact(:stock, {
|
|
273
|
+
symbol: "MSFT",
|
|
274
|
+
price: 395.20,
|
|
275
|
+
price_change: -1.2,
|
|
276
|
+
volume: 890_000
|
|
277
|
+
})
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
generate_scenario("Sector Rotation Signal") do
|
|
281
|
+
@engine.add_fact(:sector, {
|
|
282
|
+
name: "Technology",
|
|
283
|
+
performance: 7.3,
|
|
284
|
+
trend: "accelerating",
|
|
285
|
+
leaders: ["AAPL", "MSFT", "GOOGL"]
|
|
286
|
+
})
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
generate_scenario("Complex Multi-Signal Day") do
|
|
290
|
+
@engine.add_fact(:stock, {
|
|
291
|
+
symbol: "AMZN",
|
|
292
|
+
price: 142.50,
|
|
293
|
+
volume: 1_800_000,
|
|
294
|
+
price_change: 3.8,
|
|
295
|
+
rsi: 72
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
@engine.add_fact(:ma_signal, {
|
|
299
|
+
symbol: "AMZN",
|
|
300
|
+
type: "golden_cross"
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
@engine.add_fact(:news, {
|
|
304
|
+
symbol: "AMZN",
|
|
305
|
+
sentiment: 0.8,
|
|
306
|
+
impact: "high",
|
|
307
|
+
headlines: "AWS wins major government contract"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
@engine.add_fact(:earnings, {
|
|
311
|
+
symbol: "AMZN",
|
|
312
|
+
days_until: 1,
|
|
313
|
+
expected_move: 6.2
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
@engine.add_fact(:options, {
|
|
317
|
+
symbol: "AMZN",
|
|
318
|
+
iv: 52.1,
|
|
319
|
+
iv_rank: 85
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
@engine.add_fact(:market, { trend: "bullish" })
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
if __FILE__ == $0
|
|
328
|
+
demo = TradingDemo.new
|
|
329
|
+
demo.demo_scenarios
|
|
330
|
+
|
|
331
|
+
puts "\n" + "="*60
|
|
332
|
+
puts "TRADING SYSTEM DEMONSTRATION COMPLETE"
|
|
333
|
+
puts "="*60
|
|
334
|
+
end
|