kbs 0.0.1 → 0.1.0

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