kbs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +5 -0
  4. data/COMMITS.md +196 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +481 -0
  7. data/Rakefile +8 -0
  8. data/examples/README.md +531 -0
  9. data/examples/advanced_example.rb +270 -0
  10. data/examples/ai_enhanced_kbs.rb +523 -0
  11. data/examples/blackboard_demo.rb +50 -0
  12. data/examples/car_diagnostic.rb +64 -0
  13. data/examples/concurrent_inference_demo.rb +363 -0
  14. data/examples/csv_trading_system.rb +559 -0
  15. data/examples/iot_demo_using_dsl.rb +83 -0
  16. data/examples/portfolio_rebalancing_system.rb +651 -0
  17. data/examples/redis_trading_demo.rb +177 -0
  18. data/examples/sample_stock_data.csv +46 -0
  19. data/examples/stock_trading_advanced.rb +469 -0
  20. data/examples/stock_trading_system.rb.bak +563 -0
  21. data/examples/timestamped_trading.rb +286 -0
  22. data/examples/trading_demo.rb +334 -0
  23. data/examples/working_demo.rb +176 -0
  24. data/lib/kbs/alpha_memory.rb +37 -0
  25. data/lib/kbs/beta_memory.rb +57 -0
  26. data/lib/kbs/blackboard/audit_log.rb +115 -0
  27. data/lib/kbs/blackboard/engine.rb +83 -0
  28. data/lib/kbs/blackboard/fact.rb +65 -0
  29. data/lib/kbs/blackboard/memory.rb +191 -0
  30. data/lib/kbs/blackboard/message_queue.rb +96 -0
  31. data/lib/kbs/blackboard/persistence/hybrid_store.rb +118 -0
  32. data/lib/kbs/blackboard/persistence/redis_store.rb +218 -0
  33. data/lib/kbs/blackboard/persistence/sqlite_store.rb +242 -0
  34. data/lib/kbs/blackboard/persistence/store.rb +55 -0
  35. data/lib/kbs/blackboard/redis_audit_log.rb +107 -0
  36. data/lib/kbs/blackboard/redis_message_queue.rb +111 -0
  37. data/lib/kbs/blackboard.rb +23 -0
  38. data/lib/kbs/condition.rb +26 -0
  39. data/lib/kbs/dsl/condition_helpers.rb +57 -0
  40. data/lib/kbs/dsl/knowledge_base.rb +86 -0
  41. data/lib/kbs/dsl/pattern_evaluator.rb +69 -0
  42. data/lib/kbs/dsl/rule_builder.rb +115 -0
  43. data/lib/kbs/dsl/variable.rb +35 -0
  44. data/lib/kbs/dsl.rb +18 -0
  45. data/lib/kbs/fact.rb +43 -0
  46. data/lib/kbs/join_node.rb +117 -0
  47. data/lib/kbs/negation_node.rb +88 -0
  48. data/lib/kbs/production_node.rb +28 -0
  49. data/lib/kbs/rete_engine.rb +108 -0
  50. data/lib/kbs/rule.rb +46 -0
  51. data/lib/kbs/token.rb +37 -0
  52. data/lib/kbs/version.rb +5 -0
  53. data/lib/kbs/working_memory.rb +32 -0
  54. data/lib/kbs.rb +20 -0
  55. metadata +164 -0
@@ -0,0 +1,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