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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# Stock Trading System
|
|
2
|
+
|
|
3
|
+
Complete algorithmic trading system using KBS with market data collection, signal generation, risk management, and order execution.
|
|
4
|
+
|
|
5
|
+
## System Overview
|
|
6
|
+
|
|
7
|
+
This example demonstrates a production-ready trading system with:
|
|
8
|
+
|
|
9
|
+
- **Market Data Agent** - Fetches real-time quotes
|
|
10
|
+
- **Signal Agent** - Generates buy/sell signals using technical indicators
|
|
11
|
+
- **Risk Agent** - Validates trades against risk limits
|
|
12
|
+
- **Execution Agent** - Submits orders to broker
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
Market Data Agent → Blackboard → Signal Agent → Risk Agent → Execution Agent
|
|
18
|
+
↓
|
|
19
|
+
Persistent Storage (SQLite/Redis)
|
|
20
|
+
↓
|
|
21
|
+
Audit Trail
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Complete Implementation
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require 'kbs'
|
|
28
|
+
require 'net/http'
|
|
29
|
+
require 'json'
|
|
30
|
+
|
|
31
|
+
class TradingSystem
|
|
32
|
+
def initialize(db_path: 'trading.db')
|
|
33
|
+
@engine = KBS::Blackboard::Engine.new(db_path: db_path)
|
|
34
|
+
setup_rules
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def setup_rules
|
|
38
|
+
# Rule 1: Generate moving average crossover signals
|
|
39
|
+
signal_rule = KBS::Rule.new("ma_crossover_signal", priority: 100) do |r|
|
|
40
|
+
r.conditions = [
|
|
41
|
+
KBS::Condition.new(:market_data, {
|
|
42
|
+
symbol: :sym?,
|
|
43
|
+
price: :price?,
|
|
44
|
+
ma_short: :ma_short?,
|
|
45
|
+
ma_long: :ma_long?
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
# No existing signal for this symbol
|
|
49
|
+
KBS::Condition.new(:signal, { symbol: :sym? }, negated: true)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
r.action = lambda do |facts, bindings|
|
|
53
|
+
short = bindings[:ma_short?]
|
|
54
|
+
long = bindings[:ma_long?]
|
|
55
|
+
|
|
56
|
+
# Golden cross: short MA crosses above long MA
|
|
57
|
+
if short > long && (short - long) / long > 0.01 # 1% threshold
|
|
58
|
+
@engine.add_fact(:signal, {
|
|
59
|
+
symbol: bindings[:sym?],
|
|
60
|
+
type: "buy",
|
|
61
|
+
price: bindings[:price?],
|
|
62
|
+
confidence: calculate_confidence(short, long),
|
|
63
|
+
timestamp: Time.now
|
|
64
|
+
})
|
|
65
|
+
# Death cross: short MA crosses below long MA
|
|
66
|
+
elsif short < long && (long - short) / long > 0.01
|
|
67
|
+
@engine.add_fact(:signal, {
|
|
68
|
+
symbol: bindings[:sym?],
|
|
69
|
+
type: "sell",
|
|
70
|
+
price: bindings[:price?],
|
|
71
|
+
confidence: calculate_confidence(short, long),
|
|
72
|
+
timestamp: Time.now
|
|
73
|
+
})
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rule 2: Risk check for buy signals
|
|
79
|
+
risk_check_buy = KBS::Rule.new("risk_check_buy", priority: 90) do |r|
|
|
80
|
+
r.conditions = [
|
|
81
|
+
KBS::Condition.new(:signal, {
|
|
82
|
+
symbol: :sym?,
|
|
83
|
+
type: "buy",
|
|
84
|
+
price: :price?
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
KBS::Condition.new(:portfolio, {
|
|
88
|
+
cash: :cash?,
|
|
89
|
+
positions: :positions?
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
# No risk approval yet
|
|
93
|
+
KBS::Condition.new(:risk_approved, { signal_id: :sig_id? }, negated: true)
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
r.action = lambda do |facts, bindings|
|
|
97
|
+
signal = facts.find { |f| f.type == :signal }
|
|
98
|
+
cash = bindings[:cash?]
|
|
99
|
+
positions = bindings[:positions?]
|
|
100
|
+
price = bindings[:price?]
|
|
101
|
+
|
|
102
|
+
# Risk checks
|
|
103
|
+
position_size = calculate_position_size(cash, price)
|
|
104
|
+
max_position_value = cash * 0.10 # Max 10% of cash per position
|
|
105
|
+
|
|
106
|
+
if position_size * price <= max_position_value
|
|
107
|
+
# Check portfolio concentration
|
|
108
|
+
total_positions = positions.size
|
|
109
|
+
|
|
110
|
+
if total_positions < 10 # Max 10 positions
|
|
111
|
+
@engine.add_fact(:risk_approved, {
|
|
112
|
+
signal_id: signal.id,
|
|
113
|
+
symbol: bindings[:sym?],
|
|
114
|
+
quantity: position_size,
|
|
115
|
+
approved_at: Time.now
|
|
116
|
+
})
|
|
117
|
+
else
|
|
118
|
+
@engine.add_fact(:risk_rejected, {
|
|
119
|
+
signal_id: signal.id,
|
|
120
|
+
reason: "Portfolio concentration limit"
|
|
121
|
+
})
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
@engine.add_fact(:risk_rejected, {
|
|
125
|
+
signal_id: signal.id,
|
|
126
|
+
reason: "Position size exceeds limits"
|
|
127
|
+
})
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Rule 3: Execute approved orders
|
|
133
|
+
execution_rule = KBS::Rule.new("execute_approved_orders", priority: 80) do |r|
|
|
134
|
+
r.conditions = [
|
|
135
|
+
KBS::Condition.new(:risk_approved, {
|
|
136
|
+
signal_id: :sig_id?,
|
|
137
|
+
symbol: :sym?,
|
|
138
|
+
quantity: :qty?
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
KBS::Condition.new(:signal, {
|
|
142
|
+
symbol: :sym?,
|
|
143
|
+
type: :type?,
|
|
144
|
+
price: :price?
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
# Not yet executed
|
|
148
|
+
KBS::Condition.new(:order, { signal_id: :sig_id? }, negated: true)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
r.action = lambda do |facts, bindings|
|
|
152
|
+
order_id = execute_order(
|
|
153
|
+
symbol: bindings[:sym?],
|
|
154
|
+
type: bindings[:type?],
|
|
155
|
+
quantity: bindings[:qty?],
|
|
156
|
+
price: bindings[:price?]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@engine.add_fact(:order, {
|
|
160
|
+
signal_id: bindings[:sig_id?],
|
|
161
|
+
order_id: order_id,
|
|
162
|
+
symbol: bindings[:sym?],
|
|
163
|
+
type: bindings[:type?],
|
|
164
|
+
quantity: bindings[:qty?],
|
|
165
|
+
price: bindings[:price?],
|
|
166
|
+
status: "submitted",
|
|
167
|
+
timestamp: Time.now
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# Clean up signal and approval
|
|
171
|
+
signal = facts.find { |f| f.type == :signal }
|
|
172
|
+
approval = facts.find { |f| f.type == :risk_approved }
|
|
173
|
+
@engine.remove_fact(signal)
|
|
174
|
+
@engine.remove_fact(approval)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Rule 4: Stop loss monitoring
|
|
179
|
+
stop_loss_rule = KBS::Rule.new("stop_loss_trigger", priority: 95) do |r|
|
|
180
|
+
r.conditions = [
|
|
181
|
+
KBS::Condition.new(:position, {
|
|
182
|
+
symbol: :sym?,
|
|
183
|
+
entry_price: :entry?,
|
|
184
|
+
quantity: :qty?
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
KBS::Condition.new(:market_data, {
|
|
188
|
+
symbol: :sym?,
|
|
189
|
+
price: :current_price?
|
|
190
|
+
}),
|
|
191
|
+
|
|
192
|
+
KBS::Condition.new(:stop_loss_triggered, { symbol: :sym? }, negated: true)
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
r.action = lambda do |facts, bindings|
|
|
196
|
+
entry = bindings[:entry?]
|
|
197
|
+
current = bindings[:current_price?]
|
|
198
|
+
loss_pct = (entry - current) / entry
|
|
199
|
+
|
|
200
|
+
# 5% stop loss
|
|
201
|
+
if loss_pct > 0.05
|
|
202
|
+
@engine.add_fact(:signal, {
|
|
203
|
+
symbol: bindings[:sym?],
|
|
204
|
+
type: "sell",
|
|
205
|
+
price: current,
|
|
206
|
+
confidence: 1.0,
|
|
207
|
+
reason: "stop_loss",
|
|
208
|
+
timestamp: Time.now
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
@engine.add_fact(:stop_loss_triggered, {
|
|
212
|
+
symbol: bindings[:sym?],
|
|
213
|
+
entry_price: entry,
|
|
214
|
+
exit_price: current,
|
|
215
|
+
loss_pct: loss_pct
|
|
216
|
+
})
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Rule 5: Take profit monitoring
|
|
222
|
+
take_profit_rule = KBS::Rule.new("take_profit_trigger", priority: 95) do |r|
|
|
223
|
+
r.conditions = [
|
|
224
|
+
KBS::Condition.new(:position, {
|
|
225
|
+
symbol: :sym?,
|
|
226
|
+
entry_price: :entry?,
|
|
227
|
+
quantity: :qty?
|
|
228
|
+
}),
|
|
229
|
+
|
|
230
|
+
KBS::Condition.new(:market_data, {
|
|
231
|
+
symbol: :sym?,
|
|
232
|
+
price: :current_price?
|
|
233
|
+
}),
|
|
234
|
+
|
|
235
|
+
KBS::Condition.new(:take_profit_triggered, { symbol: :sym? }, negated: true)
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
r.action = lambda do |facts, bindings|
|
|
239
|
+
entry = bindings[:entry?]
|
|
240
|
+
current = bindings[:current_price?]
|
|
241
|
+
gain_pct = (current - entry) / entry
|
|
242
|
+
|
|
243
|
+
# 15% take profit
|
|
244
|
+
if gain_pct > 0.15
|
|
245
|
+
@engine.add_fact(:signal, {
|
|
246
|
+
symbol: bindings[:sym?],
|
|
247
|
+
type: "sell",
|
|
248
|
+
price: current,
|
|
249
|
+
confidence: 1.0,
|
|
250
|
+
reason: "take_profit",
|
|
251
|
+
timestamp: Time.now
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
@engine.add_fact(:take_profit_triggered, {
|
|
255
|
+
symbol: bindings[:sym?],
|
|
256
|
+
entry_price: entry,
|
|
257
|
+
exit_price: current,
|
|
258
|
+
gain_pct: gain_pct
|
|
259
|
+
})
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@engine.add_rule(signal_rule)
|
|
265
|
+
@engine.add_rule(risk_check_buy)
|
|
266
|
+
@engine.add_rule(execution_rule)
|
|
267
|
+
@engine.add_rule(stop_loss_rule)
|
|
268
|
+
@engine.add_rule(take_profit_rule)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def update_market_data(symbol, price)
|
|
272
|
+
# Calculate moving averages
|
|
273
|
+
history = get_price_history(symbol, days: 50)
|
|
274
|
+
ma_short = calculate_ma(history, period: 10)
|
|
275
|
+
ma_long = calculate_ma(history, period: 50)
|
|
276
|
+
|
|
277
|
+
# Remove old market data
|
|
278
|
+
old = @engine.facts.find { |f| f.type == :market_data && f[:symbol] == symbol }
|
|
279
|
+
@engine.remove_fact(old) if old
|
|
280
|
+
|
|
281
|
+
# Add new market data
|
|
282
|
+
@engine.add_fact(:market_data, {
|
|
283
|
+
symbol: symbol,
|
|
284
|
+
price: price,
|
|
285
|
+
ma_short: ma_short,
|
|
286
|
+
ma_long: ma_long,
|
|
287
|
+
timestamp: Time.now
|
|
288
|
+
})
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def update_portfolio(cash:, positions:)
|
|
292
|
+
old = @engine.facts.find { |f| f.type == :portfolio }
|
|
293
|
+
@engine.remove_fact(old) if old
|
|
294
|
+
|
|
295
|
+
@engine.add_fact(:portfolio, {
|
|
296
|
+
cash: cash,
|
|
297
|
+
positions: positions,
|
|
298
|
+
updated_at: Time.now
|
|
299
|
+
})
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def run_cycle
|
|
303
|
+
@engine.run
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
def calculate_confidence(short_ma, long_ma)
|
|
309
|
+
# Confidence based on divergence
|
|
310
|
+
divergence = ((short_ma - long_ma).abs / long_ma)
|
|
311
|
+
[divergence * 10, 1.0].min
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def calculate_position_size(cash, price)
|
|
315
|
+
# Kelly criterion or fixed percentage
|
|
316
|
+
(cash * 0.05 / price).floor # 5% of cash
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def execute_order(symbol:, type:, quantity:, price:)
|
|
320
|
+
# Submit to broker API
|
|
321
|
+
# Returns order_id
|
|
322
|
+
"ORD-#{Time.now.to_i}-#{symbol}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def get_price_history(symbol, days:)
|
|
326
|
+
# Fetch historical prices
|
|
327
|
+
# Returns array of prices
|
|
328
|
+
[]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def calculate_ma(prices, period:)
|
|
332
|
+
return 0 if prices.size < period
|
|
333
|
+
prices.last(period).sum / period.to_f
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Usage
|
|
338
|
+
trading = TradingSystem.new
|
|
339
|
+
|
|
340
|
+
# Initialize portfolio
|
|
341
|
+
trading.update_portfolio(
|
|
342
|
+
cash: 100000,
|
|
343
|
+
positions: []
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Market data loop
|
|
347
|
+
symbols = ["AAPL", "GOOGL", "MSFT", "TSLA"]
|
|
348
|
+
|
|
349
|
+
loop do
|
|
350
|
+
symbols.each do |symbol|
|
|
351
|
+
# Fetch current price (from API)
|
|
352
|
+
price = fetch_price(symbol)
|
|
353
|
+
|
|
354
|
+
# Update market data
|
|
355
|
+
trading.update_market_data(symbol, price)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Run inference engine
|
|
359
|
+
trading.run_cycle
|
|
360
|
+
|
|
361
|
+
sleep 60 # Run every minute
|
|
362
|
+
end
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Key Features
|
|
366
|
+
|
|
367
|
+
### 1. Moving Average Crossover
|
|
368
|
+
|
|
369
|
+
Generates buy signals when short MA crosses above long MA (golden cross) and sell signals when it crosses below (death cross).
|
|
370
|
+
|
|
371
|
+
### 2. Risk Management
|
|
372
|
+
|
|
373
|
+
- **Position sizing**: Max 10% of cash per position
|
|
374
|
+
- **Portfolio concentration**: Max 10 positions
|
|
375
|
+
- **Stop loss**: Automatic exit at 5% loss
|
|
376
|
+
- **Take profit**: Automatic exit at 15% gain
|
|
377
|
+
|
|
378
|
+
### 3. Order Execution
|
|
379
|
+
|
|
380
|
+
Approved signals become orders submitted to broker.
|
|
381
|
+
|
|
382
|
+
### 4. Audit Trail
|
|
383
|
+
|
|
384
|
+
All decisions logged to database:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
# Query signal history
|
|
388
|
+
signals = trading.engine.facts.select { |f| f.type == :signal }
|
|
389
|
+
|
|
390
|
+
# Query order history
|
|
391
|
+
orders = trading.engine.facts.select { |f| f.type == :order }
|
|
392
|
+
|
|
393
|
+
# Audit trail
|
|
394
|
+
trading.engine.fact_history(signal.id)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Performance Optimization
|
|
398
|
+
|
|
399
|
+
### Use Redis for Real-Time Trading
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
403
|
+
|
|
404
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
405
|
+
url: 'redis://localhost:6379/0'
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
trading = TradingSystem.new(store: store)
|
|
409
|
+
# 100x faster updates
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Hybrid Store for Compliance
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
require 'kbs/blackboard/persistence/hybrid_store'
|
|
416
|
+
|
|
417
|
+
store = KBS::Blackboard::Persistence::HybridStore.new(
|
|
418
|
+
redis_url: 'redis://localhost:6379/0',
|
|
419
|
+
db_path: 'audit.db'
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
trading = TradingSystem.new(store: store)
|
|
423
|
+
# Fast + complete audit trail
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Testing
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
require 'minitest/autorun'
|
|
430
|
+
|
|
431
|
+
class TestTradingSystem < Minitest::Test
|
|
432
|
+
def setup
|
|
433
|
+
@system = TradingSystem.new(db_path: ':memory:')
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def test_golden_cross_generates_buy_signal
|
|
437
|
+
@system.update_portfolio(cash: 10000, positions: [])
|
|
438
|
+
|
|
439
|
+
# Short MA above long MA
|
|
440
|
+
@system.update_market_data("AAPL", 150)
|
|
441
|
+
@system.engine.add_fact(:market_data, {
|
|
442
|
+
symbol: "AAPL",
|
|
443
|
+
price: 150,
|
|
444
|
+
ma_short: 155, # Higher
|
|
445
|
+
ma_long: 145, # Lower
|
|
446
|
+
timestamp: Time.now
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
@system.run_cycle
|
|
450
|
+
|
|
451
|
+
signals = @system.engine.facts.select { |f| f.type == :signal }
|
|
452
|
+
assert_equal 1, signals.size
|
|
453
|
+
assert_equal "buy", signals.first[:type]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def test_stop_loss_triggers_sell
|
|
457
|
+
@system.update_portfolio(cash: 10000, positions: [])
|
|
458
|
+
|
|
459
|
+
# Add position
|
|
460
|
+
@system.engine.add_fact(:position, {
|
|
461
|
+
symbol: "AAPL",
|
|
462
|
+
entry_price: 100,
|
|
463
|
+
quantity: 10
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
# Price drops 6%
|
|
467
|
+
@system.update_market_data("AAPL", 94)
|
|
468
|
+
|
|
469
|
+
@system.run_cycle
|
|
470
|
+
|
|
471
|
+
signals = @system.engine.facts.select { |f|
|
|
472
|
+
f.type == :signal && f[:reason] == "stop_loss"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
assert_equal 1, signals.size
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Next Steps
|
|
481
|
+
|
|
482
|
+
- **[Multi-Agent Example](multi-agent.md)** - Distributed trading with multiple strategies
|
|
483
|
+
- **[Performance Guide](../advanced/performance.md)** - Optimize for high-frequency trading
|
|
484
|
+
- **[Blackboard Memory](../guides/blackboard-memory.md)** - Persistent state management
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
*This trading system demonstrates production-ready algorithmic trading with KBS. Always backtest thoroughly before live trading.*
|