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,563 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/kbs'
|
|
4
|
+
require_relative '../lib/kbs/dsl'
|
|
5
|
+
require_relative '../lib/kbs/blackboard'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
module StockTradingSystem
|
|
9
|
+
class TradingEngine
|
|
10
|
+
include KBS::DSL::ConditionHelpers
|
|
11
|
+
|
|
12
|
+
attr_reader :kb, :portfolio, :market_data, :trades_executed
|
|
13
|
+
|
|
14
|
+
def initialize(initial_capital: 100000)
|
|
15
|
+
@kb = setup_knowledge_base
|
|
16
|
+
@portfolio = {
|
|
17
|
+
cash: initial_capital,
|
|
18
|
+
positions: {},
|
|
19
|
+
total_value: initial_capital,
|
|
20
|
+
initial_capital: initial_capital
|
|
21
|
+
}
|
|
22
|
+
@market_data = {}
|
|
23
|
+
@trades_executed = []
|
|
24
|
+
@current_time = Time.now
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def setup_knowledge_base
|
|
28
|
+
KBS.knowledge_base do
|
|
29
|
+
rule "golden_cross_signal" do
|
|
30
|
+
desc "Buy signal when 50-day MA crosses above 200-day MA"
|
|
31
|
+
priority 12
|
|
32
|
+
|
|
33
|
+
on :technical_indicator, indicator: "moving_average"
|
|
34
|
+
on :stock, volume: greater_than(1000000)
|
|
35
|
+
|
|
36
|
+
without :position, status: "open"
|
|
37
|
+
|
|
38
|
+
perform do |facts, bindings|
|
|
39
|
+
indicator = facts.find { |f| f.type == :technical_indicator }
|
|
40
|
+
stock = facts.find { |f| f.type == :stock }
|
|
41
|
+
|
|
42
|
+
if indicator[:ma_50] > indicator[:ma_200] && indicator[:ma_50_prev] <= indicator[:ma_200_prev]
|
|
43
|
+
puts "š GOLDEN CROSS: #{stock[:symbol]}"
|
|
44
|
+
puts " 50-MA: #{indicator[:ma_50].round(2)}, 200-MA: #{indicator[:ma_200].round(2)}"
|
|
45
|
+
puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
46
|
+
puts " ACTION: BUY SIGNAL GENERATED"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
rule "death_cross_signal" do
|
|
52
|
+
desc "Sell signal when 50-day MA crosses below 200-day MA"
|
|
53
|
+
priority 12
|
|
54
|
+
|
|
55
|
+
when :technical_indicator do
|
|
56
|
+
indicator "moving_average"
|
|
57
|
+
ma_50 satisfies { |v| v }
|
|
58
|
+
ma_200 satisfies { |v| v }
|
|
59
|
+
ma_50_prev satisfies { |v| v }
|
|
60
|
+
ma_200_prev satisfies { |v| v }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
when :position do
|
|
64
|
+
status "open"
|
|
65
|
+
shares greater_than(0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
then do |facts, bindings|
|
|
69
|
+
indicator = facts.find { |f| f.type == :technical_indicator }
|
|
70
|
+
position = facts.find { |f| f.type == :position }
|
|
71
|
+
|
|
72
|
+
if indicator[:ma_50] < indicator[:ma_200] && indicator[:ma_50_prev] >= indicator[:ma_200_prev]
|
|
73
|
+
puts "š DEATH CROSS: #{position[:symbol]}"
|
|
74
|
+
puts " 50-MA: #{indicator[:ma_50].round(2)}, 200-MA: #{indicator[:ma_200].round(2)}"
|
|
75
|
+
puts " Position: #{position[:shares]} shares @ $#{position[:entry_price]}"
|
|
76
|
+
puts " ACTION: SELL SIGNAL GENERATED"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
rule "momentum_breakout" do
|
|
82
|
+
desc "Buy on strong momentum with volume confirmation"
|
|
83
|
+
priority 10
|
|
84
|
+
|
|
85
|
+
when :stock do
|
|
86
|
+
price_change_pct greater_than(3)
|
|
87
|
+
volume_ratio greater_than(1.5)
|
|
88
|
+
rsi range(40, 70)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
when :market, sentiment: one_of("bullish", "neutral")
|
|
92
|
+
|
|
93
|
+
not.when :position do
|
|
94
|
+
symbol satisfies { |s| s }
|
|
95
|
+
status "open"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
then do |facts, bindings|
|
|
99
|
+
stock = facts.find { |f| f.type == :stock }
|
|
100
|
+
puts "š MOMENTUM BREAKOUT: #{stock[:symbol]}"
|
|
101
|
+
puts " Price Change: +#{stock[:price_change_pct]}%"
|
|
102
|
+
puts " Volume Ratio: #{stock[:volume_ratio]}x average"
|
|
103
|
+
puts " RSI: #{stock[:rsi]}"
|
|
104
|
+
puts " ACTION: MOMENTUM BUY"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
rule "oversold_reversal" do
|
|
109
|
+
desc "Buy oversold stocks showing reversal signs"
|
|
110
|
+
priority 9
|
|
111
|
+
|
|
112
|
+
when :stock do
|
|
113
|
+
rsi less_than(30)
|
|
114
|
+
price satisfies { |p| p }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
when :technical_indicator do
|
|
118
|
+
indicator "support_level"
|
|
119
|
+
level satisfies { |l| l }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
when :market_breadth do
|
|
123
|
+
advancing_issues greater_than(1500)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
then do |facts, bindings|
|
|
127
|
+
stock = facts.find { |f| f.type == :stock }
|
|
128
|
+
support = facts.find { |f| f.type == :technical_indicator }
|
|
129
|
+
|
|
130
|
+
if stock[:price] >= support[:level] * 0.98
|
|
131
|
+
puts "š OVERSOLD REVERSAL: #{stock[:symbol]}"
|
|
132
|
+
puts " RSI: #{stock[:rsi]} (oversold)"
|
|
133
|
+
puts " Price: $#{stock[:price]} near support at $#{support[:level]}"
|
|
134
|
+
puts " Market breadth positive"
|
|
135
|
+
puts " ACTION: REVERSAL BUY"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
rule "trailing_stop_loss" do
|
|
141
|
+
desc "Implement trailing stop loss for open positions"
|
|
142
|
+
priority 15
|
|
143
|
+
|
|
144
|
+
when :position do
|
|
145
|
+
status "open"
|
|
146
|
+
profit_pct greater_than(5)
|
|
147
|
+
high_water_mark satisfies { |h| h }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
when :stock do
|
|
151
|
+
price satisfies { |p| p }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
then do |facts, bindings|
|
|
155
|
+
position = facts.find { |f| f.type == :position }
|
|
156
|
+
stock = facts.find { |f| f.type == :stock }
|
|
157
|
+
|
|
158
|
+
trailing_stop = position[:high_water_mark] * 0.95
|
|
159
|
+
|
|
160
|
+
if stock[:price] <= trailing_stop
|
|
161
|
+
puts "š TRAILING STOP HIT: #{position[:symbol]}"
|
|
162
|
+
puts " Current Price: $#{stock[:price]}"
|
|
163
|
+
puts " Stop Price: $#{trailing_stop.round(2)}"
|
|
164
|
+
puts " Profit: #{position[:profit_pct]}%"
|
|
165
|
+
puts " ACTION: SELL TO LOCK PROFITS"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
rule "position_sizing" do
|
|
171
|
+
desc "Calculate position size based on Kelly Criterion"
|
|
172
|
+
priority 8
|
|
173
|
+
|
|
174
|
+
when :trading_signal do
|
|
175
|
+
action "buy"
|
|
176
|
+
confidence greater_than(0.6)
|
|
177
|
+
expected_return satisfies { |r| r }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
when :portfolio do
|
|
181
|
+
cash greater_than(1000)
|
|
182
|
+
risk_tolerance satisfies { |r| r }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
then do |facts, bindings|
|
|
186
|
+
signal = facts.find { |f| f.type == :trading_signal }
|
|
187
|
+
portfolio = facts.find { |f| f.type == :portfolio }
|
|
188
|
+
|
|
189
|
+
win_prob = signal[:confidence]
|
|
190
|
+
win_loss_ratio = signal[:expected_return]
|
|
191
|
+
kelly_pct = (win_prob * win_loss_ratio - (1 - win_prob)) / win_loss_ratio
|
|
192
|
+
adjusted_kelly = kelly_pct * portfolio[:risk_tolerance]
|
|
193
|
+
position_size = portfolio[:cash] * [adjusted_kelly, 0.25].min
|
|
194
|
+
|
|
195
|
+
puts "š POSITION SIZING: #{signal[:symbol]}"
|
|
196
|
+
puts " Kelly %: #{(kelly_pct * 100).round(1)}%"
|
|
197
|
+
puts " Adjusted Size: #{(adjusted_kelly * 100).round(1)}%"
|
|
198
|
+
puts " Dollar Amount: $#{position_size.round(0)}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
rule "sector_rotation" do
|
|
203
|
+
desc "Rotate into outperforming sectors"
|
|
204
|
+
priority 7
|
|
205
|
+
|
|
206
|
+
when :sector_performance do
|
|
207
|
+
sector satisfies { |s| s }
|
|
208
|
+
relative_strength greater_than(1.1)
|
|
209
|
+
trend "upward"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
when :position do
|
|
213
|
+
sector satisfies { |s| s }
|
|
214
|
+
profit_pct less_than(2)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
then do |facts, bindings|
|
|
218
|
+
strong_sector = facts.find { |f| f.type == :sector_performance }
|
|
219
|
+
weak_position = facts.find { |f| f.type == :position }
|
|
220
|
+
|
|
221
|
+
if strong_sector[:sector] != weak_position[:sector]
|
|
222
|
+
puts "š SECTOR ROTATION SIGNAL"
|
|
223
|
+
puts " From: #{weak_position[:sector]} (weak)"
|
|
224
|
+
puts " To: #{strong_sector[:sector]} (RS: #{strong_sector[:relative_strength]})"
|
|
225
|
+
puts " ACTION: ROTATE PORTFOLIO"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
rule "correlation_hedge" do
|
|
231
|
+
desc "Hedge positions with high correlation"
|
|
232
|
+
priority 6
|
|
233
|
+
|
|
234
|
+
when :correlation do
|
|
235
|
+
correlation greater_than(0.8)
|
|
236
|
+
symbol1 satisfies { |s| s }
|
|
237
|
+
symbol2 satisfies { |s| s }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
when :position do
|
|
241
|
+
symbol satisfies { |s| s }
|
|
242
|
+
value greater_than(10000)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
then do |facts, bindings|
|
|
246
|
+
correlation = facts.find { |f| f.type == :correlation }
|
|
247
|
+
position = facts.find { |f| f.type == :position }
|
|
248
|
+
|
|
249
|
+
if [correlation[:symbol1], correlation[:symbol2]].include?(position[:symbol])
|
|
250
|
+
puts "ā ļø HIGH CORRELATION WARNING"
|
|
251
|
+
puts " Symbols: #{correlation[:symbol1]} <-> #{correlation[:symbol2]}"
|
|
252
|
+
puts " Correlation: #{correlation[:correlation]}"
|
|
253
|
+
puts " ACTION: CONSIDER HEDGING OR DIVERSIFYING"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
rule "earnings_play" do
|
|
259
|
+
desc "Trade around earnings announcements"
|
|
260
|
+
priority 11
|
|
261
|
+
|
|
262
|
+
when :earnings_calendar do
|
|
263
|
+
symbol satisfies { |s| s }
|
|
264
|
+
days_until range(1, 5)
|
|
265
|
+
expected_move greater_than(5)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
when :options do
|
|
269
|
+
symbol satisfies { |s| s }
|
|
270
|
+
implied_volatility greater_than(30)
|
|
271
|
+
iv_rank greater_than(50)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
then do |facts, bindings|
|
|
275
|
+
earnings = facts.find { |f| f.type == :earnings_calendar }
|
|
276
|
+
options = facts.find { |f| f.type == :options }
|
|
277
|
+
|
|
278
|
+
puts "š° EARNINGS PLAY: #{earnings[:symbol]}"
|
|
279
|
+
puts " Days to Earnings: #{earnings[:days_until]}"
|
|
280
|
+
puts " Expected Move: ±#{earnings[:expected_move]}%"
|
|
281
|
+
puts " IV: #{options[:implied_volatility]}% (Rank: #{options[:iv_rank]})"
|
|
282
|
+
puts " ACTION: CONSIDER VOLATILITY STRATEGY"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
rule "risk_concentration" do
|
|
287
|
+
desc "Alert on concentrated risk exposure"
|
|
288
|
+
priority 14
|
|
289
|
+
|
|
290
|
+
when :portfolio_metrics do
|
|
291
|
+
concentration_ratio greater_than(0.3)
|
|
292
|
+
top_holding satisfies { |h| h }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
when :market, volatility: greater_than(25)
|
|
296
|
+
|
|
297
|
+
then do |facts, bindings|
|
|
298
|
+
metrics = facts.find { |f| f.type == :portfolio_metrics }
|
|
299
|
+
|
|
300
|
+
puts "ā ļø CONCENTRATION RISK ALERT"
|
|
301
|
+
puts " Top Holding: #{metrics[:top_holding]}"
|
|
302
|
+
puts " Concentration: #{(metrics[:concentration_ratio] * 100).round(1)}%"
|
|
303
|
+
puts " Market Volatility Elevated"
|
|
304
|
+
puts " ACTION: REDUCE POSITION SIZE"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
rule "gap_fade" do
|
|
309
|
+
desc "Fade large opening gaps"
|
|
310
|
+
priority 8
|
|
311
|
+
|
|
312
|
+
when :market_open do
|
|
313
|
+
gap_percentage greater_than(2)
|
|
314
|
+
direction satisfies { |d| d }
|
|
315
|
+
volume satisfies { |v| v }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
when :stock do
|
|
319
|
+
symbol satisfies { |s| s }
|
|
320
|
+
average_true_range satisfies { |atr| atr }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
then do |facts, bindings|
|
|
324
|
+
gap = facts.find { |f| f.type == :market_open }
|
|
325
|
+
stock = facts.find { |f| f.type == :stock }
|
|
326
|
+
|
|
327
|
+
if gap[:gap_percentage] > 2 * (stock[:average_true_range] / stock[:price] * 100)
|
|
328
|
+
direction = gap[:direction] == "up" ? "SHORT" : "LONG"
|
|
329
|
+
puts "š GAP FADE OPPORTUNITY: #{stock[:symbol]}"
|
|
330
|
+
puts " Gap: #{gap[:direction]} #{gap[:gap_percentage]}%"
|
|
331
|
+
puts " ATR Multiple: #{(gap[:gap_percentage] / (stock[:average_true_range] / stock[:price] * 100)).round(1)}x"
|
|
332
|
+
puts " ACTION: #{direction} FADE TRADE"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
rule "vwap_reversion" do
|
|
338
|
+
desc "Trade VWAP mean reversion"
|
|
339
|
+
priority 7
|
|
340
|
+
|
|
341
|
+
when :intraday do
|
|
342
|
+
symbol satisfies { |s| s }
|
|
343
|
+
price satisfies { |p| p }
|
|
344
|
+
vwap satisfies { |v| v }
|
|
345
|
+
distance_from_vwap satisfies { |d| d.abs > 2 }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
when :volume_profile do
|
|
349
|
+
symbol satisfies { |s| s }
|
|
350
|
+
poc satisfies { |p| p }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
then do |facts, bindings|
|
|
354
|
+
intraday = facts.find { |f| f.type == :intraday }
|
|
355
|
+
profile = facts.find { |f| f.type == :volume_profile }
|
|
356
|
+
|
|
357
|
+
direction = intraday[:distance_from_vwap] > 0 ? "OVERBOUGHT" : "OVERSOLD"
|
|
358
|
+
target = intraday[:vwap]
|
|
359
|
+
|
|
360
|
+
puts "š VWAP REVERSION: #{intraday[:symbol]}"
|
|
361
|
+
puts " Status: #{direction}"
|
|
362
|
+
puts " Current: $#{intraday[:price]}"
|
|
363
|
+
puts " VWAP: $#{intraday[:vwap].round(2)}"
|
|
364
|
+
puts " POC: $#{profile[:poc].round(2)}"
|
|
365
|
+
puts " ACTION: MEAN REVERSION TRADE TO $#{target.round(2)}"
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
rule "news_sentiment" do
|
|
370
|
+
desc "React to news sentiment changes"
|
|
371
|
+
priority 13
|
|
372
|
+
|
|
373
|
+
when :news do
|
|
374
|
+
symbol satisfies { |s| s }
|
|
375
|
+
sentiment_score satisfies { |s| s.abs > 0.7 }
|
|
376
|
+
volume greater_than(10)
|
|
377
|
+
recency less_than(60)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
when :stock do
|
|
381
|
+
symbol satisfies { |s| s }
|
|
382
|
+
price_change_pct range(-2, 2)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
then do |facts, bindings|
|
|
386
|
+
news = facts.find { |f| f.type == :news }
|
|
387
|
+
stock = facts.find { |f| f.type == :stock }
|
|
388
|
+
|
|
389
|
+
sentiment = news[:sentiment_score] > 0 ? "POSITIVE" : "NEGATIVE"
|
|
390
|
+
action = news[:sentiment_score] > 0 ? "BUY" : "SELL"
|
|
391
|
+
|
|
392
|
+
puts "š° NEWS SENTIMENT: #{news[:symbol]}"
|
|
393
|
+
puts " Sentiment: #{sentiment} (#{news[:sentiment_score]})"
|
|
394
|
+
puts " News Volume: #{news[:volume]} articles"
|
|
395
|
+
puts " Price Reaction: #{stock[:price_change_pct]}%"
|
|
396
|
+
puts " ACTION: #{action} ON SENTIMENT"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def simulate_market_data(symbol, base_price = 100)
|
|
403
|
+
volatility = 0.02
|
|
404
|
+
trend = rand(-0.001..0.001)
|
|
405
|
+
|
|
406
|
+
@market_data[symbol] ||= {
|
|
407
|
+
price: base_price,
|
|
408
|
+
ma_50: base_price,
|
|
409
|
+
ma_200: base_price,
|
|
410
|
+
volume: 1000000 + rand(500000),
|
|
411
|
+
rsi: 50
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
data = @market_data[symbol]
|
|
415
|
+
|
|
416
|
+
price_change = data[:price] * (trend + volatility * (rand - 0.5))
|
|
417
|
+
data[:price] = (data[:price] + price_change).round(2)
|
|
418
|
+
data[:ma_50] = (data[:ma_50] * 0.98 + data[:price] * 0.02).round(2)
|
|
419
|
+
data[:ma_200] = (data[:ma_200] * 0.995 + data[:price] * 0.005).round(2)
|
|
420
|
+
data[:volume] = (data[:volume] * (0.8 + rand * 0.4)).to_i
|
|
421
|
+
|
|
422
|
+
rsi_change = (data[:price] > base_price) ? 1 : -1
|
|
423
|
+
data[:rsi] = [[data[:rsi] + rsi_change * rand(5), 0].max, 100].min
|
|
424
|
+
|
|
425
|
+
data
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def run_simulation(symbols: ["AAPL", "GOOGL", "MSFT", "AMZN"], iterations: 10)
|
|
429
|
+
puts "\n" + "=" * 80
|
|
430
|
+
puts "STOCK TRADING SYSTEM SIMULATION"
|
|
431
|
+
puts "=" * 80
|
|
432
|
+
puts "Initial Capital: $#{@portfolio[:initial_capital].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
433
|
+
puts "Trading Symbols: #{symbols.join(', ')}"
|
|
434
|
+
puts "=" * 80
|
|
435
|
+
|
|
436
|
+
iterations.times do |i|
|
|
437
|
+
puts "\nā° MARKET TICK #{i + 1} - #{(@current_time + i * 60).strftime('%H:%M:%S')}"
|
|
438
|
+
puts "-" * 60
|
|
439
|
+
|
|
440
|
+
@kb.reset
|
|
441
|
+
|
|
442
|
+
@kb.fact :market, sentiment: ["bullish", "neutral", "bearish"].sample, volatility: rand(15..35)
|
|
443
|
+
@kb.fact :market_breadth, advancing_issues: rand(1000..2500), declining_issues: rand(500..2000)
|
|
444
|
+
|
|
445
|
+
symbols.each do |symbol|
|
|
446
|
+
data = simulate_market_data(symbol, 100 + rand(50))
|
|
447
|
+
|
|
448
|
+
@kb.fact :stock, {
|
|
449
|
+
symbol: symbol,
|
|
450
|
+
price: data[:price],
|
|
451
|
+
volume: data[:volume],
|
|
452
|
+
rsi: data[:rsi],
|
|
453
|
+
price_change_pct: ((data[:price] - (data[:price] - rand(-5..5))) / data[:price] * 100).round(2),
|
|
454
|
+
volume_ratio: (data[:volume] / 1000000.0).round(2),
|
|
455
|
+
average_true_range: rand(1.0..3.0).round(2)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
@kb.fact :technical_indicator, {
|
|
459
|
+
symbol: symbol,
|
|
460
|
+
indicator: "moving_average",
|
|
461
|
+
ma_50: data[:ma_50],
|
|
462
|
+
ma_200: data[:ma_200],
|
|
463
|
+
ma_50_prev: data[:ma_50] - rand(-1..1),
|
|
464
|
+
ma_200_prev: data[:ma_200] - rand(-0.5..0.5)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@kb.fact :technical_indicator, {
|
|
468
|
+
symbol: symbol,
|
|
469
|
+
indicator: "support_level",
|
|
470
|
+
level: data[:price] * 0.95
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if rand > 0.7
|
|
474
|
+
@kb.fact :news, {
|
|
475
|
+
symbol: symbol,
|
|
476
|
+
sentiment_score: rand(-1.0..1.0).round(2),
|
|
477
|
+
volume: rand(5..50),
|
|
478
|
+
recency: rand(10..120)
|
|
479
|
+
}
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
if rand > 0.8
|
|
483
|
+
@kb.fact :earnings_calendar, {
|
|
484
|
+
symbol: symbol,
|
|
485
|
+
days_until: rand(1..30),
|
|
486
|
+
expected_move: rand(3..15).round(1)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@kb.fact :options, {
|
|
490
|
+
symbol: symbol,
|
|
491
|
+
implied_volatility: rand(20..80),
|
|
492
|
+
iv_rank: rand(0..100)
|
|
493
|
+
}
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
if rand > 0.5
|
|
498
|
+
@kb.fact :correlation, {
|
|
499
|
+
symbol1: symbols.sample,
|
|
500
|
+
symbol2: symbols.sample,
|
|
501
|
+
correlation: rand(0.5..0.95).round(2)
|
|
502
|
+
}
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
if @portfolio[:positions].any?
|
|
506
|
+
position = @portfolio[:positions].values.sample
|
|
507
|
+
@kb.fact :position, position if position
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
@kb.fact :portfolio, {
|
|
511
|
+
cash: @portfolio[:cash],
|
|
512
|
+
risk_tolerance: 0.5
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@kb.fact :portfolio_metrics, {
|
|
516
|
+
concentration_ratio: @portfolio[:positions].any? ?
|
|
517
|
+
@portfolio[:positions].values.map { |p| p[:value] }.max.to_f / @portfolio[:total_value] : 0,
|
|
518
|
+
top_holding: @portfolio[:positions].any? ?
|
|
519
|
+
@portfolio[:positions].max_by { |_, p| p[:value] }&.first : "None"
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@kb.run
|
|
523
|
+
|
|
524
|
+
sleep(0.5) if i < iterations - 1
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
puts "\n" + "=" * 80
|
|
528
|
+
puts "SIMULATION COMPLETE"
|
|
529
|
+
puts "=" * 80
|
|
530
|
+
print_portfolio_summary
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def print_portfolio_summary
|
|
534
|
+
puts "\nš PORTFOLIO SUMMARY"
|
|
535
|
+
puts "-" * 40
|
|
536
|
+
puts "Cash: $#{@portfolio[:cash].round(2).to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
537
|
+
|
|
538
|
+
if @portfolio[:positions].any?
|
|
539
|
+
puts "\nOpen Positions:"
|
|
540
|
+
@portfolio[:positions].each do |symbol, position|
|
|
541
|
+
puts " #{symbol}: #{position[:shares]} shares @ $#{position[:entry_price]}"
|
|
542
|
+
end
|
|
543
|
+
else
|
|
544
|
+
puts "No open positions"
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
total_value = @portfolio[:cash] + @portfolio[:positions].values.sum { |p| p[:value] || 0 }
|
|
548
|
+
pnl = total_value - @portfolio[:initial_capital]
|
|
549
|
+
pnl_pct = (pnl / @portfolio[:initial_capital] * 100).round(2)
|
|
550
|
+
|
|
551
|
+
puts "\nTotal Portfolio Value: $#{total_value.round(2).to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
|
|
552
|
+
puts "P&L: $#{pnl.round(2)} (#{pnl_pct}%)"
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
if __FILE__ == $0
|
|
558
|
+
engine = StockTradingSystem::TradingEngine.new(initial_capital: 100000)
|
|
559
|
+
engine.run_simulation(
|
|
560
|
+
symbols: ["AAPL", "GOOGL", "MSFT", "NVDA", "TSLA", "META"],
|
|
561
|
+
iterations: 15
|
|
562
|
+
)
|
|
563
|
+
end
|