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,651 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs'
4
+ require 'csv'
5
+ require 'date'
6
+
7
+ class PortfolioRebalancingSystem
8
+ def initialize(csv_file = 'sample_stock_data.csv')
9
+ @engine = KBS::ReteEngine.new
10
+ @csv_file = csv_file
11
+ @portfolio = {
12
+ cash: 100_000,
13
+ positions: {},
14
+ target_allocations: {
15
+ "Technology" => 0.40, # 40% target
16
+ "Healthcare" => 0.25, # 25% target
17
+ "Finance" => 0.20, # 20% target
18
+ "Consumer" => 0.15 # 15% target
19
+ },
20
+ sector_mappings: {
21
+ "AAPL" => "Technology",
22
+ "GOOGL" => "Technology",
23
+ "MSFT" => "Technology",
24
+ "NVDA" => "Technology",
25
+ "TSLA" => "Consumer",
26
+ "META" => "Technology",
27
+ "JNJ" => "Healthcare",
28
+ "PFE" => "Healthcare",
29
+ "JPM" => "Finance",
30
+ "BAC" => "Finance"
31
+ },
32
+ replacement_candidates: {
33
+ "Technology" => ["AAPL", "GOOGL", "MSFT", "NVDA", "META"],
34
+ "Healthcare" => ["JNJ", "PFE", "UNH", "ABT"],
35
+ "Finance" => ["JPM", "BAC", "WFC", "GS"],
36
+ "Consumer" => ["TSLA", "AMZN", "DIS", "NFLX"]
37
+ },
38
+ trades: [],
39
+ rebalancing_history: []
40
+ }
41
+ @current_prices = {}
42
+ @performance_data = Hash.new { |h, k| h[k] = [] }
43
+ setup_rebalancing_rules
44
+ end
45
+
46
+ def setup_rebalancing_rules
47
+ # Rule 1: Sector Allocation Drift Detection
48
+ allocation_drift_rule = KBS::Rule.new(
49
+ "sector_allocation_drift",
50
+ conditions: [
51
+ KBS::Condition.new(:portfolio_allocation, {
52
+ sector: ->(s) { s && s.length > 0 },
53
+ current_weight: ->(w) { w && w > 0 },
54
+ target_weight: ->(t) { t && t > 0 },
55
+ drift_percentage: ->(d) { d && d.abs > 5 } # >5% drift from target
56
+ })
57
+ ],
58
+ action: lambda do |facts, bindings|
59
+ allocation = facts.find { |f| f.type == :portfolio_allocation }
60
+ sector = allocation[:sector]
61
+ current = allocation[:current_weight]
62
+ target = allocation[:target_weight]
63
+ drift = allocation[:drift_percentage]
64
+
65
+ action = drift > 0 ? "REDUCE" : "INCREASE"
66
+ puts "⚖️ ALLOCATION DRIFT: #{sector}"
67
+ puts " Current: #{(current * 100).round(1)}%"
68
+ puts " Target: #{(target * 100).round(1)}%"
69
+ puts " Drift: #{drift > 0 ? '+' : ''}#{drift.round(1)}%"
70
+ puts " Action: #{action} #{sector} allocation"
71
+
72
+ trigger_sector_rebalancing(sector, target, current)
73
+ end,
74
+ priority: 15
75
+ )
76
+
77
+ # Rule 2: Underperforming Position Replacement
78
+ underperformer_replacement_rule = KBS::Rule.new(
79
+ "replace_underperformer",
80
+ conditions: [
81
+ KBS::Condition.new(:position_performance, {
82
+ symbol: ->(s) { s && s.length > 0 },
83
+ relative_performance: ->(p) { p && p < -10 }, # 10% underperformance
84
+ days_held: ->(d) { d && d > 30 }, # Held for more than 30 days
85
+ sector: ->(s) { s && s.length > 0 }
86
+ }),
87
+ KBS::Condition.new(:replacement_candidate, {
88
+ sector: ->(s) { s && s.length > 0 },
89
+ relative_performance: ->(p) { p && p > 5 }, # 5% outperformance
90
+ momentum_score: ->(m) { m && m > 0.7 }
91
+ })
92
+ ],
93
+ action: lambda do |facts, bindings|
94
+ underperformer = facts.find { |f| f.type == :position_performance }
95
+ candidate = facts.find { |f| f.type == :replacement_candidate }
96
+
97
+ if underperformer[:sector] == candidate[:sector]
98
+ puts "🔄 POSITION REPLACEMENT: #{underperformer[:symbol]} → #{candidate[:symbol]}"
99
+ puts " Sector: #{underperformer[:sector]}"
100
+ puts " Underperformer: #{underperformer[:relative_performance].round(1)}% vs sector"
101
+ puts " Replacement: #{candidate[:relative_performance].round(1)}% vs sector"
102
+ puts " Momentum Score: #{candidate[:momentum_score].round(2)}"
103
+
104
+ execute_position_replacement(underperformer[:symbol], candidate[:symbol])
105
+ end
106
+ end,
107
+ priority: 12
108
+ )
109
+
110
+ # Rule 3: Correlation Risk Reduction
111
+ correlation_replacement_rule = KBS::Rule.new(
112
+ "reduce_correlation_risk",
113
+ conditions: [
114
+ KBS::Condition.new(:correlation_risk, {
115
+ correlation_coefficient: ->(c) { c && c > 0.8 }, # High correlation
116
+ combined_allocation: ->(a) { a && a > 0.25 }, # >25% combined weight
117
+ sector: ->(s) { s && s.length > 0 }
118
+ }),
119
+ KBS::Condition.new(:replacement_candidate, {
120
+ correlation_with_portfolio: ->(c) { c && c < 0.5 }, # Low correlation
121
+ sector: ->(s) { s && s.length > 0 }
122
+ })
123
+ ],
124
+ action: lambda do |facts, bindings|
125
+ risk = facts.find { |f| f.type == :correlation_risk }
126
+ candidate = facts.find { |f| f.type == :replacement_candidate }
127
+
128
+ if risk[:sector] == candidate[:sector]
129
+ puts "📊 CORRELATION RISK REDUCTION"
130
+ puts " High Correlation: #{(risk[:correlation_coefficient] * 100).round(1)}%"
131
+ puts " Combined Weight: #{(risk[:combined_allocation] * 100).round(1)}%"
132
+ puts " Replacement Correlation: #{(candidate[:correlation_with_portfolio] * 100).round(1)}%"
133
+ puts " Action: Replace correlated position"
134
+
135
+ # Replace the weaker performing stock in the correlated pair
136
+ weaker_symbol = identify_weaker_performer(risk[:symbols])
137
+ execute_position_replacement(weaker_symbol, candidate[:symbol])
138
+ end
139
+ end,
140
+ priority: 14
141
+ )
142
+
143
+ # Rule 4: Momentum-Based Rotation
144
+ momentum_rotation_rule = KBS::Rule.new(
145
+ "momentum_rotation",
146
+ conditions: [
147
+ KBS::Condition.new(:sector_momentum, {
148
+ sector: ->(s) { s && s.length > 0 },
149
+ momentum_trend: "declining",
150
+ momentum_score: ->(m) { m && m < 0.3 }, # Weak momentum
151
+ duration_days: ->(d) { d && d > 20 } # Trend persisting
152
+ }),
153
+ KBS::Condition.new(:sector_momentum, {
154
+ sector: ->(s) { s && s.length > 0 },
155
+ momentum_trend: "rising",
156
+ momentum_score: ->(m) { m && m > 0.8 } # Strong momentum
157
+ })
158
+ ],
159
+ action: lambda do |facts, bindings|
160
+ declining = facts.find { |f| f.type == :sector_momentum && f[:momentum_trend] == "declining" }
161
+ rising = facts.find { |f| f.type == :sector_momentum && f[:momentum_trend] == "rising" }
162
+
163
+ if declining && rising && declining[:sector] != rising[:sector]
164
+ puts "🔀 MOMENTUM ROTATION"
165
+ puts " From: #{declining[:sector]} (momentum: #{declining[:momentum_score].round(2)})"
166
+ puts " To: #{rising[:sector]} (momentum: #{rising[:momentum_score].round(2)})"
167
+ puts " Action: Rotate allocation between sectors"
168
+
169
+ execute_sector_rotation(declining[:sector], rising[:sector], 0.10) # 10% rotation
170
+ end
171
+ end,
172
+ priority: 10
173
+ )
174
+
175
+ # Rule 5: Quality Score Replacement
176
+ quality_replacement_rule = KBS::Rule.new(
177
+ "quality_score_replacement",
178
+ conditions: [
179
+ KBS::Condition.new(:position_quality, {
180
+ symbol: ->(s) { s && s.length > 0 },
181
+ quality_score: ->(q) { q && q < 0.4 }, # Low quality score
182
+ sector: ->(s) { s && s.length > 0 }
183
+ }),
184
+ KBS::Condition.new(:replacement_candidate, {
185
+ quality_score: ->(q) { q && q > 0.8 }, # High quality score
186
+ sector: ->(s) { s && s.length > 0 }
187
+ })
188
+ ],
189
+ action: lambda do |facts, bindings|
190
+ low_quality = facts.find { |f| f.type == :position_quality }
191
+ high_quality = facts.find { |f| f.type == :replacement_candidate }
192
+
193
+ if low_quality[:sector] == high_quality[:sector]
194
+ puts "⭐ QUALITY UPGRADE: #{low_quality[:symbol]} → #{high_quality[:symbol]}"
195
+ puts " Current Quality: #{low_quality[:quality_score].round(2)}"
196
+ puts " Replacement Quality: #{high_quality[:quality_score].round(2)}"
197
+ puts " Sector: #{low_quality[:sector]}"
198
+
199
+ execute_position_replacement(low_quality[:symbol], high_quality[:symbol])
200
+ end
201
+ end,
202
+ priority: 8
203
+ )
204
+
205
+ # Rule 6: Risk-Adjusted Return Optimization
206
+ risk_adjusted_optimization_rule = KBS::Rule.new(
207
+ "risk_adjusted_optimization",
208
+ conditions: [
209
+ KBS::Condition.new(:position_metrics, {
210
+ symbol: ->(s) { s && s.length > 0 },
211
+ sharpe_ratio: ->(sr) { sr && sr < 0.5 }, # Low risk-adjusted return
212
+ volatility: ->(v) { v && v > 0.3 } # High volatility
213
+ }),
214
+ KBS::Condition.new(:replacement_candidate, {
215
+ sharpe_ratio: ->(sr) { sr && sr > 1.0 }, # Better risk-adjusted return
216
+ volatility: ->(v) { v && v < 0.2 } # Lower volatility
217
+ })
218
+ ],
219
+ action: lambda do |facts, bindings|
220
+ poor_performer = facts.find { |f| f.type == :position_metrics }
221
+ better_candidate = facts.find { |f| f.type == :replacement_candidate }
222
+
223
+ puts "📈 RISK-ADJUSTED OPTIMIZATION"
224
+ puts " Replace: #{poor_performer[:symbol]}"
225
+ puts " Sharpe Ratio: #{poor_performer[:sharpe_ratio].round(2)}"
226
+ puts " Volatility: #{(poor_performer[:volatility] * 100).round(1)}%"
227
+ puts " With: #{better_candidate[:symbol]}"
228
+ puts " Sharpe Ratio: #{better_candidate[:sharpe_ratio].round(2)}"
229
+ puts " Volatility: #{(better_candidate[:volatility] * 100).round(1)}%"
230
+
231
+ execute_position_replacement(poor_performer[:symbol], better_candidate[:symbol])
232
+ end,
233
+ priority: 11
234
+ )
235
+
236
+ # Rule 7: Quarterly Rebalancing Trigger
237
+ quarterly_rebalancing_rule = KBS::Rule.new(
238
+ "quarterly_rebalancing",
239
+ conditions: [
240
+ KBS::Condition.new(:calendar_event, {
241
+ event_type: "quarter_end",
242
+ days_since_last_rebalance: ->(d) { d && d > 85 } # >85 days
243
+ }),
244
+ KBS::Condition.new(:portfolio_metrics, {
245
+ total_drift: ->(d) { d && d > 3 } # >3% total portfolio drift
246
+ })
247
+ ],
248
+ action: lambda do |facts, bindings|
249
+ event = facts.find { |f| f.type == :calendar_event }
250
+ metrics = facts.find { |f| f.type == :portfolio_metrics }
251
+
252
+ puts "📅 QUARTERLY REBALANCING TRIGGER"
253
+ puts " Days Since Last Rebalance: #{event[:days_since_last_rebalance]}"
254
+ puts " Total Portfolio Drift: #{metrics[:total_drift].round(1)}%"
255
+ puts " Action: Full portfolio rebalancing"
256
+
257
+ execute_full_portfolio_rebalancing
258
+ end,
259
+ priority: 16
260
+ )
261
+
262
+ @engine.add_rule(allocation_drift_rule)
263
+ @engine.add_rule(underperformer_replacement_rule)
264
+ @engine.add_rule(correlation_replacement_rule)
265
+ @engine.add_rule(momentum_rotation_rule)
266
+ @engine.add_rule(quality_replacement_rule)
267
+ @engine.add_rule(risk_adjusted_optimization_rule)
268
+ @engine.add_rule(quarterly_rebalancing_rule)
269
+ end
270
+
271
+ def calculate_sector_allocations
272
+ total_value = calculate_total_portfolio_value
273
+ return {} if total_value <= 0
274
+
275
+ sector_values = Hash.new(0)
276
+
277
+ @portfolio[:positions].each do |symbol, position|
278
+ next unless position[:status] == "open"
279
+ sector = @portfolio[:sector_mappings][symbol] || "Unknown"
280
+ current_value = position[:shares] * @current_prices[symbol] if @current_prices[symbol]
281
+ sector_values[sector] += current_value if current_value
282
+ end
283
+
284
+ sector_allocations = {}
285
+ sector_values.each do |sector, value|
286
+ sector_allocations[sector] = value / total_value.to_f
287
+ end
288
+
289
+ sector_allocations
290
+ end
291
+
292
+ def calculate_total_portfolio_value
293
+ total = @portfolio[:cash]
294
+ @portfolio[:positions].each do |symbol, position|
295
+ if position[:status] == "open" && @current_prices[symbol]
296
+ total += position[:shares] * @current_prices[symbol]
297
+ end
298
+ end
299
+ total
300
+ end
301
+
302
+ def trigger_sector_rebalancing(sector, target_weight, current_weight)
303
+ puts " 🔄 Triggering sector rebalancing for #{sector}"
304
+
305
+ # Calculate the dollar amount to adjust
306
+ total_value = calculate_total_portfolio_value
307
+ target_value = total_value * target_weight
308
+ current_value = total_value * current_weight
309
+ adjustment_needed = target_value - current_value
310
+
311
+ puts " 💰 Adjustment needed: $#{adjustment_needed.round(2)}"
312
+
313
+ if adjustment_needed > 0
314
+ # Need to buy more of this sector
315
+ buy_sector_positions(sector, adjustment_needed)
316
+ else
317
+ # Need to sell some of this sector
318
+ sell_sector_positions(sector, adjustment_needed.abs)
319
+ end
320
+ end
321
+
322
+ def execute_position_replacement(old_symbol, new_symbol)
323
+ old_position = @portfolio[:positions][old_symbol]
324
+ return unless old_position && old_position[:status] == "open"
325
+
326
+ old_price = @current_prices[old_symbol]
327
+ new_price = @current_prices[new_symbol]
328
+ return unless old_price && new_price
329
+
330
+ # Sell old position
331
+ proceeds = old_position[:shares] * old_price
332
+ @portfolio[:cash] += proceeds
333
+ old_position[:status] = "closed"
334
+ old_position[:exit_price] = old_price
335
+
336
+ # Buy new position with proceeds
337
+ new_shares = (proceeds / new_price).to_i
338
+ if new_shares > 0
339
+ cost = new_shares * new_price
340
+ @portfolio[:cash] -= cost
341
+
342
+ @portfolio[:positions][new_symbol] = {
343
+ symbol: new_symbol,
344
+ shares: new_shares,
345
+ entry_price: new_price,
346
+ status: "open"
347
+ }
348
+
349
+ puts " ✅ REPLACEMENT EXECUTED:"
350
+ puts " Sold: #{old_position[:shares]} shares of #{old_symbol} at $#{old_price}"
351
+ puts " Bought: #{new_shares} shares of #{new_symbol} at $#{new_price}"
352
+ puts " Cash: $#{@portfolio[:cash].round(2)}"
353
+ end
354
+ end
355
+
356
+ def execute_sector_rotation(from_sector, to_sector, rotation_percentage)
357
+ total_value = calculate_total_portfolio_value
358
+ rotation_amount = total_value * rotation_percentage
359
+
360
+ puts " 🔀 Rotating $#{rotation_amount.round(2)} from #{from_sector} to #{to_sector}"
361
+
362
+ # Sell positions from declining sector
363
+ sell_sector_positions(from_sector, rotation_amount)
364
+
365
+ # Buy positions in rising sector
366
+ buy_sector_positions(to_sector, rotation_amount)
367
+ end
368
+
369
+ def execute_full_portfolio_rebalancing
370
+ puts " 🔄 Executing full portfolio rebalancing"
371
+
372
+ current_allocations = calculate_sector_allocations
373
+ total_value = calculate_total_portfolio_value
374
+
375
+ @portfolio[:target_allocations].each do |sector, target_weight|
376
+ current_weight = current_allocations[sector] || 0
377
+ drift = ((current_weight - target_weight) / target_weight * 100) rescue 0
378
+
379
+ if drift.abs > 2 # Rebalance if >2% drift
380
+ adjustment = total_value * (target_weight - current_weight)
381
+
382
+ if adjustment > 0
383
+ buy_sector_positions(sector, adjustment)
384
+ else
385
+ sell_sector_positions(sector, adjustment.abs)
386
+ end
387
+ end
388
+ end
389
+
390
+ @portfolio[:rebalancing_history] << {
391
+ date: Date.today,
392
+ type: "full_rebalancing",
393
+ allocations_before: current_allocations.dup,
394
+ allocations_after: calculate_sector_allocations
395
+ }
396
+ end
397
+
398
+ def buy_sector_positions(sector, amount)
399
+ candidates = @portfolio[:replacement_candidates][sector] || []
400
+ return if candidates.empty? || amount <= 0
401
+
402
+ # Simple equal-weight allocation among candidates
403
+ amount_per_stock = amount / candidates.length
404
+
405
+ candidates.each do |symbol|
406
+ price = @current_prices[symbol]
407
+ next unless price
408
+
409
+ shares = (amount_per_stock / price).to_i
410
+ next if shares <= 0
411
+
412
+ cost = shares * price
413
+ if @portfolio[:cash] >= cost
414
+ @portfolio[:cash] -= cost
415
+
416
+ if @portfolio[:positions][symbol] && @portfolio[:positions][symbol][:status] == "open"
417
+ # Add to existing position
418
+ existing = @portfolio[:positions][symbol]
419
+ total_shares = existing[:shares] + shares
420
+ total_cost = (existing[:shares] * existing[:entry_price]) + cost
421
+
422
+ existing[:shares] = total_shares
423
+ existing[:entry_price] = total_cost / total_shares
424
+ else
425
+ # Create new position
426
+ @portfolio[:positions][symbol] = {
427
+ symbol: symbol,
428
+ shares: shares,
429
+ entry_price: price,
430
+ status: "open"
431
+ }
432
+ end
433
+
434
+ puts " ✅ Bought #{shares} shares of #{symbol} at $#{price}"
435
+ end
436
+ end
437
+ end
438
+
439
+ def sell_sector_positions(sector, amount)
440
+ sector_positions = @portfolio[:positions].select do |symbol, position|
441
+ position[:status] == "open" && @portfolio[:sector_mappings][symbol] == sector
442
+ end
443
+
444
+ return if sector_positions.empty? || amount <= 0
445
+
446
+ # Sell proportionally from sector positions
447
+ total_sector_value = sector_positions.sum do |symbol, position|
448
+ position[:shares] * @current_prices[symbol] if @current_prices[symbol]
449
+ end.compact.sum
450
+
451
+ return if total_sector_value <= 0
452
+
453
+ sector_positions.each do |symbol, position|
454
+ position_value = position[:shares] * @current_prices[symbol]
455
+ proportion = position_value / total_sector_value
456
+ shares_to_sell = ((amount * proportion) / @current_prices[symbol]).to_i
457
+
458
+ if shares_to_sell > 0 && shares_to_sell <= position[:shares]
459
+ proceeds = shares_to_sell * @current_prices[symbol]
460
+ @portfolio[:cash] += proceeds
461
+
462
+ position[:shares] -= shares_to_sell
463
+ if position[:shares] <= 0
464
+ position[:status] = "closed"
465
+ position[:exit_price] = @current_prices[symbol]
466
+ end
467
+
468
+ puts " ✅ Sold #{shares_to_sell} shares of #{symbol} at $#{@current_prices[symbol]}"
469
+ end
470
+ end
471
+ end
472
+
473
+ def identify_weaker_performer(symbols)
474
+ performances = symbols.map do |symbol|
475
+ position = @portfolio[:positions][symbol]
476
+ if position && position[:status] == "open" && @current_prices[symbol]
477
+ current_value = position[:shares] * @current_prices[symbol]
478
+ entry_value = position[:shares] * position[:entry_price]
479
+ performance = ((current_value - entry_value) / entry_value) * 100
480
+ [symbol, performance]
481
+ end
482
+ end.compact
483
+
484
+ # Return the symbol with the worst performance
485
+ performances.min_by { |symbol, perf| perf }&.first || symbols.first
486
+ end
487
+
488
+ def simulate_rebalancing_scenarios
489
+ puts "🔄 PORTFOLIO REBALANCING SYSTEM DEMONSTRATION"
490
+ puts "=" * 70
491
+
492
+ # Initialize some positions
493
+ @current_prices = {
494
+ "AAPL" => 185.50, "GOOGL" => 161.90, "MSFT" => 320.00, "NVDA" => 425.80,
495
+ "TSLA" => 238.90, "META" => 298.50, "JNJ" => 165.20, "PFE" => 35.80,
496
+ "JPM" => 145.60, "BAC" => 28.90
497
+ }
498
+
499
+ # Create initial portfolio
500
+ initial_positions = {
501
+ "AAPL" => { symbol: "AAPL", shares: 200, entry_price: 180.00, status: "open" },
502
+ "GOOGL" => { symbol: "GOOGL", shares: 150, entry_price: 160.00, status: "open" },
503
+ "TSLA" => { symbol: "TSLA", shares: 100, entry_price: 240.00, status: "open" },
504
+ "JNJ" => { symbol: "JNJ", shares: 80, entry_price: 170.00, status: "open" }
505
+ }
506
+ @portfolio[:positions] = initial_positions
507
+ @portfolio[:cash] = 25_000
508
+
509
+ puts "\n📊 Initial Portfolio:"
510
+ print_portfolio_status
511
+
512
+ # Scenario 1: Allocation Drift
513
+ puts "\n🎯 SCENARIO 1: Sector Allocation Drift"
514
+ puts "-" * 50
515
+ @engine.working_memory.facts.clear
516
+
517
+ allocations = calculate_sector_allocations
518
+ allocations.each do |sector, current_weight|
519
+ target_weight = @portfolio[:target_allocations][sector] || 0
520
+ drift = ((current_weight - target_weight) / target_weight * 100) rescue 0
521
+
522
+ if drift.abs > 5
523
+ @engine.add_fact(:portfolio_allocation, {
524
+ sector: sector,
525
+ current_weight: current_weight,
526
+ target_weight: target_weight,
527
+ drift_percentage: drift
528
+ })
529
+ end
530
+ end
531
+ @engine.run
532
+
533
+ # Scenario 2: Underperformer Replacement
534
+ puts "\n🎯 SCENARIO 2: Underperformer Replacement"
535
+ puts "-" * 50
536
+ @engine.working_memory.facts.clear
537
+
538
+ @engine.add_fact(:position_performance, {
539
+ symbol: "TSLA",
540
+ relative_performance: -15.2,
541
+ days_held: 45,
542
+ sector: "Consumer"
543
+ })
544
+
545
+ @engine.add_fact(:replacement_candidate, {
546
+ symbol: "AMZN",
547
+ sector: "Consumer",
548
+ relative_performance: 8.3,
549
+ momentum_score: 0.85
550
+ })
551
+ @engine.run
552
+
553
+ # Scenario 3: Correlation Risk
554
+ puts "\n🎯 SCENARIO 3: Correlation Risk Reduction"
555
+ puts "-" * 50
556
+ @engine.working_memory.facts.clear
557
+
558
+ @engine.add_fact(:correlation_risk, {
559
+ symbols: ["AAPL", "GOOGL"],
560
+ correlation_coefficient: 0.87,
561
+ combined_allocation: 0.32,
562
+ sector: "Technology"
563
+ })
564
+
565
+ @engine.add_fact(:replacement_candidate, {
566
+ symbol: "MSFT",
567
+ sector: "Technology",
568
+ correlation_with_portfolio: 0.42
569
+ })
570
+ @engine.run
571
+
572
+ # Scenario 4: Momentum Rotation
573
+ puts "\n🎯 SCENARIO 4: Momentum-Based Rotation"
574
+ puts "-" * 50
575
+ @engine.working_memory.facts.clear
576
+
577
+ @engine.add_fact(:sector_momentum, {
578
+ sector: "Technology",
579
+ momentum_trend: "declining",
580
+ momentum_score: 0.25,
581
+ duration_days: 25
582
+ })
583
+
584
+ @engine.add_fact(:sector_momentum, {
585
+ sector: "Healthcare",
586
+ momentum_trend: "rising",
587
+ momentum_score: 0.85
588
+ })
589
+ @engine.run
590
+
591
+ # Scenario 5: Quality Replacement
592
+ puts "\n🎯 SCENARIO 5: Quality Score Replacement"
593
+ puts "-" * 50
594
+ @engine.working_memory.facts.clear
595
+
596
+ @engine.add_fact(:position_quality, {
597
+ symbol: "TSLA",
598
+ quality_score: 0.35,
599
+ sector: "Consumer"
600
+ })
601
+
602
+ @engine.add_fact(:replacement_candidate, {
603
+ symbol: "AMZN",
604
+ quality_score: 0.88,
605
+ sector: "Consumer"
606
+ })
607
+ @engine.run
608
+
609
+ puts "\n📊 Final Portfolio:"
610
+ print_portfolio_status
611
+
612
+ puts "\n" + "=" * 70
613
+ puts "REBALANCING DEMONSTRATION COMPLETE"
614
+ end
615
+
616
+ def print_portfolio_status
617
+ total_value = calculate_total_portfolio_value
618
+ allocations = calculate_sector_allocations
619
+
620
+ puts "Total Portfolio Value: $#{total_value.round(2)}"
621
+ puts "Cash: $#{@portfolio[:cash].round(2)}"
622
+ puts ""
623
+ puts "Sector Allocations:"
624
+
625
+ @portfolio[:target_allocations].each do |sector, target|
626
+ current = allocations[sector] || 0
627
+ drift = ((current - target) / target * 100) rescue 0
628
+
629
+ puts " #{sector}:"
630
+ puts " Current: #{(current * 100).round(1)}% (Target: #{(target * 100).round(1)}%)"
631
+ puts " Drift: #{drift > 0 ? '+' : ''}#{drift.round(1)}%"
632
+ end
633
+
634
+ puts ""
635
+ puts "Positions:"
636
+ @portfolio[:positions].each do |symbol, position|
637
+ if position[:status] == "open" && @current_prices[symbol]
638
+ current_value = position[:shares] * @current_prices[symbol]
639
+ pnl = current_value - (position[:shares] * position[:entry_price])
640
+ pnl_pct = (pnl / (position[:shares] * position[:entry_price]) * 100)
641
+
642
+ puts " #{symbol}: #{position[:shares]} shares @ $#{@current_prices[symbol]} = $#{current_value.round(2)} (#{pnl_pct > 0 ? '+' : ''}#{pnl_pct.round(1)}%)"
643
+ end
644
+ end
645
+ end
646
+ end
647
+
648
+ if __FILE__ == $0
649
+ system = PortfolioRebalancingSystem.new
650
+ system.simulate_rebalancing_scenarios
651
+ end