ollama-client 0.2.4 → 0.2.6

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +560 -106
  4. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  5. data/docs/GETTING_STARTED.md +361 -0
  6. data/docs/INTEGRATION_TESTING.md +170 -0
  7. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  8. data/docs/PERSONAS.md +383 -0
  9. data/docs/QUICK_START.md +195 -0
  10. data/docs/README.md +2 -3
  11. data/docs/RELEASE_GUIDE.md +376 -0
  12. data/docs/TESTING.md +392 -170
  13. data/docs/TEST_CHECKLIST.md +450 -0
  14. data/docs/ruby_guide.md +6232 -0
  15. data/examples/README.md +51 -66
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/tool_calling_parsing.rb +59 -0
  19. data/exe/ollama-client +128 -1
  20. data/lib/ollama/agent/planner.rb +7 -2
  21. data/lib/ollama/chat_session.rb +101 -0
  22. data/lib/ollama/client.rb +43 -21
  23. data/lib/ollama/config.rb +4 -1
  24. data/lib/ollama/document_loader.rb +163 -0
  25. data/lib/ollama/embeddings.rb +42 -13
  26. data/lib/ollama/errors.rb +1 -0
  27. data/lib/ollama/personas.rb +287 -0
  28. data/lib/ollama/version.rb +1 -1
  29. data/lib/ollama_client.rb +8 -0
  30. metadata +31 -53
  31. data/docs/GEM_RELEASE_GUIDE.md +0 -794
  32. data/docs/GET_RUBYGEMS_SECRET.md +0 -151
  33. data/docs/QUICK_OTP_SETUP.md +0 -80
  34. data/docs/QUICK_RELEASE.md +0 -106
  35. data/docs/RUBYGEMS_OTP_SETUP.md +0 -199
  36. data/examples/advanced_complex_schemas.rb +0 -366
  37. data/examples/advanced_edge_cases.rb +0 -241
  38. data/examples/advanced_error_handling.rb +0 -200
  39. data/examples/advanced_multi_step_agent.rb +0 -341
  40. data/examples/advanced_performance_testing.rb +0 -186
  41. data/examples/chat_console.rb +0 -143
  42. data/examples/complete_workflow.rb +0 -245
  43. data/examples/dhan_console.rb +0 -843
  44. data/examples/dhanhq/README.md +0 -236
  45. data/examples/dhanhq/agents/base_agent.rb +0 -74
  46. data/examples/dhanhq/agents/data_agent.rb +0 -66
  47. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  48. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  49. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  50. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  51. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  52. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  53. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  54. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  55. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  56. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  57. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  58. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  59. data/examples/dhanhq/services/base_service.rb +0 -46
  60. data/examples/dhanhq/services/data_service.rb +0 -118
  61. data/examples/dhanhq/services/trading_service.rb +0 -59
  62. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  63. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  64. data/examples/dhanhq/test_tool_calling.rb +0 -538
  65. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  66. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  67. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  68. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  69. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  70. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  71. data/examples/dhanhq_agent.rb +0 -964
  72. data/examples/dhanhq_tools.rb +0 -1663
  73. data/examples/multi_step_agent_with_external_data.rb +0 -368
  74. data/examples/structured_outputs_chat.rb +0 -72
  75. data/examples/structured_tools.rb +0 -89
  76. data/examples/test_dhanhq_tool_calling.rb +0 -375
  77. data/examples/test_tool_calling.rb +0 -160
  78. data/examples/tool_calling_direct.rb +0 -124
  79. data/examples/tool_calling_pattern.rb +0 -269
  80. data/exe/dhan_console +0 -4
@@ -1,252 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base_agent"
4
- require_relative "../schemas/agent_schemas"
5
- require_relative "../analysis/trend_analyzer"
6
- require_relative "../services/data_service"
7
-
8
- module DhanHQ
9
- module Agents
10
- # Agent for technical analysis and trading recommendations
11
- class TechnicalAnalysisAgent < BaseAgent
12
- def initialize(ollama_client:)
13
- super(ollama_client: ollama_client, schema: build_analysis_schema)
14
- @data_service = Services::DataService.new
15
- end
16
-
17
- def analyze_symbol(symbol:, exchange_segment:)
18
- # Fetch historical data
19
- result = @data_service.execute(
20
- action: "get_historical_data",
21
- params: {
22
- "symbol" => symbol,
23
- "exchange_segment" => exchange_segment,
24
- "from_date" => (Date.today - 60).strftime("%Y-%m-%d"),
25
- "to_date" => Date.today.strftime("%Y-%m-%d")
26
- }
27
- )
28
-
29
- return { error: "Failed to fetch data: #{result[:error] || 'Unknown error'}" } if result[:error]
30
- return { error: "No result data returned" } unless result[:result]
31
-
32
- # Convert to OHLC format
33
- ohlc_data = convert_to_ohlc(result)
34
-
35
- if ohlc_data.nil? || ohlc_data.empty?
36
- return { error: "Failed to convert data to OHLC format (empty or invalid data)" }
37
- end
38
-
39
- # Perform technical analysis
40
- analysis = Analysis::TrendAnalyzer.analyze(ohlc_data)
41
-
42
- return { error: "Analysis returned empty result" } if analysis.nil? || analysis.empty?
43
-
44
- # Get LLM interpretation
45
- interpretation = interpret_analysis(symbol, analysis)
46
-
47
- {
48
- symbol: symbol,
49
- exchange_segment: exchange_segment,
50
- analysis: analysis,
51
- interpretation: interpretation
52
- }
53
- end
54
-
55
- def generate_recommendation(analysis_result, trading_style: :swing)
56
- market_context = build_market_context(analysis_result)
57
-
58
- prompt = build_recommendation_prompt(market_context, trading_style)
59
-
60
- begin
61
- @ollama_client.generate(
62
- prompt: prompt,
63
- schema: build_recommendation_schema(trading_style)
64
- )
65
- rescue Ollama::Error => e
66
- { error: e.message }
67
- end
68
- end
69
-
70
- protected
71
-
72
- def build_analysis_prompt(*)
73
- ""
74
- end
75
-
76
- private
77
-
78
- def convert_to_ohlc(historical_data)
79
- return [] unless historical_data.is_a?(Hash)
80
-
81
- data = extract_data_payload(historical_data)
82
- return [] unless data
83
-
84
- return ohlc_from_hash(data) if data.is_a?(Hash)
85
- return ohlc_from_array(data) if data.is_a?(Array)
86
-
87
- []
88
- end
89
-
90
- def extract_data_payload(historical_data)
91
- outer_result = historical_data[:result] || historical_data["result"]
92
- return nil unless outer_result.is_a?(Hash)
93
-
94
- outer_result[:data] || outer_result["data"]
95
- end
96
-
97
- def ohlc_from_hash(data)
98
- series = extract_series(data)
99
- return [] if series[:closes].nil? || series[:closes].empty?
100
-
101
- max_length = series_lengths(series).max
102
- return [] if max_length.zero?
103
-
104
- build_ohlc_rows(series, max_length)
105
- end
106
-
107
- def extract_series(data)
108
- {
109
- opens: data[:open] || data["open"] || [],
110
- highs: data[:high] || data["high"] || [],
111
- lows: data[:low] || data["low"] || [],
112
- closes: data[:close] || data["close"] || [],
113
- volumes: data[:volume] || data["volume"] || []
114
- }
115
- end
116
-
117
- def series_lengths(series)
118
- [series[:opens].length, series[:highs].length, series[:lows].length, series[:closes].length]
119
- end
120
-
121
- def build_ohlc_rows(series, max_length)
122
- (0...max_length).map do |index|
123
- {
124
- open: series[:opens][index] || series[:closes][index] || 0,
125
- high: series[:highs][index] || series[:closes][index] || 0,
126
- low: series[:lows][index] || series[:closes][index] || 0,
127
- close: series[:closes][index] || 0,
128
- volume: series[:volumes][index] || 0
129
- }
130
- end
131
- end
132
-
133
- def ohlc_from_array(data)
134
- data.filter_map { |bar| normalize_bar(bar) }
135
- end
136
-
137
- def normalize_bar(bar)
138
- return nil unless bar.is_a?(Hash)
139
-
140
- {
141
- open: bar["open"] || bar[:open],
142
- high: bar["high"] || bar[:high],
143
- low: bar["low"] || bar[:low],
144
- close: bar["close"] || bar[:close],
145
- volume: bar["volume"] || bar[:volume]
146
- }
147
- end
148
-
149
- def interpret_analysis(symbol, analysis)
150
- context = <<~CONTEXT
151
- Technical Analysis for #{symbol}:
152
-
153
- Trend: #{analysis[:trend][:trend]} (Strength: #{analysis[:trend][:strength]}%)
154
- RSI: #{analysis[:indicators][:rsi]&.round(2) || 'N/A'}
155
- MACD: #{analysis[:indicators][:macd]&.round(2) || 'N/A'}
156
- Current Price: #{analysis[:current_price]}
157
-
158
- Patterns: #{analysis[:patterns][:candlestick].length} candlestick patterns detected
159
- Structure Break: #{analysis[:structure_break][:broken] ? 'Yes' : 'No'}
160
- CONTEXT
161
-
162
- begin
163
- @ollama_client.generate(
164
- prompt: "Interpret this technical analysis: #{context}",
165
- schema: {
166
- "type" => "object",
167
- "properties" => {
168
- "summary" => { "type" => "string" },
169
- "sentiment" => { "type" => "string", "enum" => ["bullish", "bearish", "neutral"] },
170
- "key_levels" => { "type" => "array", "items" => { "type" => "number" } }
171
- }
172
- }
173
- )
174
- rescue Ollama::Error
175
- { summary: "Analysis completed", sentiment: "neutral" }
176
- end
177
- end
178
-
179
- def build_market_context(analysis_result)
180
- analysis = analysis_result[:analysis]
181
- <<~CONTEXT
182
- Symbol: #{analysis_result[:symbol]}
183
- Current Price: #{analysis[:current_price]}
184
- Trend: #{analysis[:trend][:trend]} (#{analysis[:trend][:strength]}% strength)
185
- RSI: #{analysis[:indicators][:rsi]&.round(2) || 'N/A'}
186
- MACD: #{analysis[:indicators][:macd]&.round(2) || 'N/A'} (Signal: #{analysis[:indicators][:macd_signal]&.round(2) || 'N/A'})
187
- Structure Break: #{analysis[:structure_break][:broken] ? 'Yes' : 'No'}
188
- Patterns: #{analysis[:patterns][:candlestick].length} recent patterns
189
- CONTEXT
190
- end
191
-
192
- def build_recommendation_prompt(market_context, trading_style)
193
- style_instructions = case trading_style
194
- when :intraday
195
- "Focus on intraday moves, entry/exit within same day, use tight stops"
196
- when :swing
197
- "Focus on swing trades, 2-7 day holds, use wider stops"
198
- when :options
199
- "Focus on options buying, identify high probability setups, consider IV"
200
- else
201
- "General trading recommendations"
202
- end
203
-
204
- <<~PROMPT
205
- Analyze the following technical analysis and provide trading recommendations:
206
-
207
- #{market_context}
208
-
209
- Trading Style: #{style_instructions}
210
-
211
- Provide:
212
- - Entry strategy
213
- - Stop loss levels
214
- - Target levels
215
- - Risk/reward ratio
216
- - Confidence level
217
- PROMPT
218
- end
219
-
220
- def build_analysis_schema
221
- {
222
- "type" => "object",
223
- "properties" => {
224
- "action" => { "type" => "string" },
225
- "reasoning" => { "type" => "string" }
226
- }
227
- }
228
- end
229
-
230
- def build_recommendation_schema(_trading_style)
231
- {
232
- "type" => "object",
233
- "properties" => {
234
- "recommendation" => { "type" => "string", "enum" => ["buy", "sell", "hold", "avoid"] },
235
- "entry_price" => { "type" => "number" },
236
- "stop_loss" => { "type" => "number" },
237
- "target_price" => { "type" => "number" },
238
- "risk_reward_ratio" => { "type" => "number" },
239
- "confidence" => {
240
- "type" => "number",
241
- "minimum" => 0,
242
- "maximum" => 1,
243
- "description" => "Confidence (0.0 to 1.0)"
244
- },
245
- "reasoning" => { "type" => "string" },
246
- "timeframe" => { "type" => "string" }
247
- }
248
- }
249
- end
250
- end
251
- end
252
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base_agent"
4
- require_relative "../schemas/agent_schemas"
5
- require_relative "../services/trading_service"
6
- require_relative "../utils/trading_parameter_normalizer"
7
-
8
- module DhanHQ
9
- module Agents
10
- # Agent for trading order decisions using LLM
11
- class TradingAgent < BaseAgent
12
- def initialize(ollama_client:)
13
- super(ollama_client: ollama_client, schema: DhanHQ::Schemas::AgentSchemas::TRADING_AGENT_SCHEMA)
14
- @trading_service = DhanHQ::Services::TradingService.new
15
- end
16
-
17
- def execute_decision(decision)
18
- action = decision["action"]
19
- raw_params = decision["parameters"] || {}
20
- params = DhanHQ::Utils::TradingParameterNormalizer.normalize(raw_params)
21
-
22
- # Warn if price seems suspiciously low
23
- if params["price"] && params["price"] < 10
24
- puts "⚠️ Warning: Price value (#{params['price']}) seems unusually low. Please verify."
25
- end
26
-
27
- @trading_service.execute(action: action, params: params)
28
- end
29
-
30
- protected
31
-
32
- def build_analysis_prompt(market_context:)
33
- <<~PROMPT
34
- Analyze the following market situation and decide the best trading action:
35
-
36
- Market Context:
37
- #{market_context}
38
-
39
- Available Actions (TRADING ONLY):
40
- - place_order: Build order parameters (requires: security_id as string, quantity, price, transaction_type, exchange_segment)
41
- - place_super_order: Build super order parameters with SL/TP (requires: security_id as string, quantity, price, target_price, stop_loss_price, exchange_segment)
42
- - cancel_order: Build cancel parameters (requires: order_id)
43
- - no_action: Take no action if market conditions are unclear or risky
44
-
45
- CRITICAL PARAMETER EXTRACTION RULES:
46
- 1. security_id must be a STRING (e.g., "13" not 13)
47
- 2. price, target_price, stop_loss_price must be NUMERIC values (numbers, not strings)
48
- 3. When extracting prices from context:
49
- - "2,850" means 2850 (remove commas, convert to number)
50
- - "1,483.2" means 1483.2 (remove commas, keep decimal)
51
- - Use the EXACT numeric value from context, do NOT approximate
52
- - If context says "Entry price: 2,850", use 2850 (not 2, not 285, not any approximation)
53
- 4. quantity must be a positive integer
54
- 5. Valid exchange_segment values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
55
-
56
- CRITICAL: The parameters object must contain ONLY valid parameter values (strings, numbers, etc.).
57
- DO NOT include comments, instructions, explanations, or descriptions in the parameters object.
58
- Parameters should be clean JSON values only.
59
-
60
- Example of CORRECT parameter extraction:
61
- Context: "Entry price: 2,850, Quantity: 100, security_id='1333'"
62
- Correct parameters: {"security_id": "1333", "quantity": 100, "price": 2850}
63
- WRONG: {"price": 2, "description": "approximation"} ❌
64
- WRONG: {"price": "2850"} ❌ (should be number, not string)
65
-
66
- Decision Criteria:
67
- - Only take actions with confidence > 0.6
68
- - Consider risk management (use super orders for risky trades)
69
- - Ensure all required parameters are provided with EXACT values from context
70
- - Be conservative - prefer no_action if uncertain
71
-
72
- Respond with a JSON object containing:
73
- - action: one of the available trading actions
74
- - reasoning: why this action was chosen (put explanations here, NOT in parameters)
75
- - confidence: your confidence level (0-1)
76
- - parameters: object with ONLY required parameter values (no comments, no explanations, exact numeric values)
77
- PROMPT
78
- end
79
- end
80
- end
81
- end
@@ -1,138 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../indicators/technical_indicators"
4
-
5
- module DhanHQ
6
- module Analysis
7
- # Market Structure Analysis (SMC - Smart Money Concepts)
8
- class MarketStructure
9
- def self.analyze_trend(highs, lows, closes)
10
- return { trend: :unknown, strength: 0 } if closes.nil? || closes.length < 3
11
-
12
- trend = infer_trend(highs, lows)
13
- strength = moving_average_strength(closes)
14
-
15
- { trend: trend, strength: strength.round(2) }
16
- end
17
-
18
- # Smart Money Concepts: Order Blocks
19
- def self.find_order_blocks(highs, lows, closes, volumes)
20
- return [] if closes.nil? || closes.length < 5
21
-
22
- order_blocks = []
23
-
24
- (4...closes.length).each do |i|
25
- # Bullish order block: strong move up after consolidation
26
- if closes[i] > closes[i - 1] && closes[i - 1] > closes[i - 2] &&
27
- volumes[i] > volumes[i - 1] * 1.5
28
- order_blocks << {
29
- type: :bullish,
30
- price_range: [lows[i - 2], highs[i]],
31
- timestamp: i,
32
- volume: volumes[i]
33
- }
34
- end
35
-
36
- # Bearish order block: strong move down after consolidation
37
- next unless closes[i] < closes[i - 1] && closes[i - 1] < closes[i - 2] &&
38
- volumes[i] > volumes[i - 1] * 1.5
39
-
40
- order_blocks << {
41
- type: :bearish,
42
- price_range: [lows[i], highs[i - 2]],
43
- timestamp: i,
44
- volume: volumes[i]
45
- }
46
- end
47
-
48
- order_blocks
49
- end
50
-
51
- # Liquidity Zones (SMC)
52
- def self.find_liquidity_zones(highs, lows, closes, lookback = 50)
53
- return { buy_side: [], sell_side: [] } if closes.nil? || closes.length < lookback
54
-
55
- # Buy-side liquidity: areas where stops are likely (below support)
56
- # Sell-side liquidity: areas where stops are likely (above resistance)
57
-
58
- support_resistance = DhanHQ::Indicators::TechnicalIndicators.support_resistance(
59
- highs, lows, closes, lookback
60
- )
61
-
62
- current_price = closes.last
63
- buy_side = support_resistance[:support].select { |s| s[:price] < current_price }
64
- sell_side = support_resistance[:resistance].select { |r| r[:price] > current_price }
65
-
66
- { buy_side: buy_side, sell_side: sell_side }
67
- end
68
-
69
- # Market Structure Break (Change in trend)
70
- def self.detect_structure_break(highs, lows, closes)
71
- return { broken: false, direction: nil } if closes.nil? || closes.length < 10
72
-
73
- trend = analyze_trend(highs, lows, closes)
74
-
75
- # Structure break: previous high/low is broken
76
- recent_high = highs ? highs.last(5).max : closes.last(5).max
77
- recent_low = lows ? lows.last(5).min : closes.last(5).min
78
- previous_high = highs ? highs[-10..-6].max : closes[-10..-6].max
79
- previous_low = lows ? lows[-10..-6].min : closes[-10..-6].min
80
-
81
- broken = false
82
- direction = nil
83
-
84
- if recent_high > previous_high && trend[:trend] != :uptrend
85
- broken = true
86
- direction = :bullish_break
87
- elsif recent_low < previous_low && trend[:trend] != :downtrend
88
- broken = true
89
- direction = :bearish_break
90
- end
91
-
92
- { broken: broken, direction: direction, current_trend: trend[:trend] }
93
- end
94
-
95
- def self.infer_trend(highs, lows)
96
- recent_highs = extract_recent(highs)
97
- recent_lows = extract_recent(lows)
98
-
99
- higher_highs = non_decreasing?(recent_highs)
100
- higher_lows = non_decreasing?(recent_lows)
101
- lower_highs = non_increasing?(recent_highs)
102
- lower_lows = non_increasing?(recent_lows)
103
-
104
- return :uptrend if higher_highs && higher_lows
105
- return :downtrend if lower_highs && lower_lows
106
-
107
- :sideways
108
- end
109
-
110
- def self.moving_average_strength(closes)
111
- short_average = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 20)
112
- long_average = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 50)
113
-
114
- return 0 unless short_average.last && long_average.last
115
-
116
- ((short_average.last - long_average.last) / long_average.last * 100).abs
117
- end
118
-
119
- def self.extract_recent(series, lookback = 10)
120
- return nil unless series
121
-
122
- series.last(lookback)
123
- end
124
-
125
- def self.non_decreasing?(series)
126
- return false unless series
127
-
128
- series.each_cons(2).all? { |first, second| second >= first }
129
- end
130
-
131
- def self.non_increasing?(series)
132
- return false unless series
133
-
134
- series.each_cons(2).all? { |first, second| second <= first }
135
- end
136
- end
137
- end
138
- end
@@ -1,192 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Analysis
5
- # Candlestick and chart pattern recognition
6
- class PatternRecognizer
7
- # Detect common candlestick patterns
8
- def self.detect_candlestick_patterns(opens, highs, lows, closes)
9
- return [] if closes.nil? || closes.length < 3
10
-
11
- patterns = []
12
-
13
- (2...closes.length).each do |i|
14
- first_candle = build_candle(opens, highs, lows, closes, i - 2)
15
- second_candle = build_candle(opens, highs, lows, closes, i - 1)
16
- third_candle = build_candle(opens, highs, lows, closes, i)
17
-
18
- # Engulfing patterns
19
- if bullish_engulfing?(first_candle, second_candle)
20
- patterns << { type: :bullish_engulfing, index: i, strength: :medium }
21
- end
22
-
23
- if bearish_engulfing?(first_candle, second_candle)
24
- patterns << { type: :bearish_engulfing, index: i, strength: :medium }
25
- end
26
-
27
- # Hammer pattern
28
- patterns << { type: :hammer, index: i - 1, strength: :medium } if hammer?(second_candle)
29
-
30
- # Shooting star
31
- patterns << { type: :shooting_star, index: i - 1, strength: :medium } if shooting_star?(second_candle)
32
-
33
- # Three white soldiers / three black crows
34
- if three_white_soldiers?([first_candle, second_candle, third_candle])
35
- patterns << { type: :three_white_soldiers, index: i, strength: :strong }
36
- end
37
-
38
- if three_black_crows?([first_candle, second_candle, third_candle])
39
- patterns << { type: :three_black_crows, index: i, strength: :strong }
40
- end
41
- end
42
-
43
- patterns
44
- end
45
-
46
- # Detect chart patterns
47
- def self.detect_chart_patterns(highs, lows, closes)
48
- return [] if closes.nil? || closes.length < 20
49
-
50
- patterns = []
51
-
52
- # Head and Shoulders (simplified)
53
- if head_and_shoulders?(highs, lows)
54
- patterns << { type: :head_and_shoulders, strength: :strong, direction: :bearish }
55
- end
56
-
57
- # Double Top/Bottom
58
- double_pattern = double_top_bottom?(highs, lows, closes)
59
- patterns << double_pattern if double_pattern
60
-
61
- patterns
62
- end
63
-
64
- def self.bullish_engulfing?(first_candle, second_candle)
65
- first_open = first_candle[:open]
66
- first_close = first_candle[:close]
67
- second_open = second_candle[:open]
68
- second_close = second_candle[:close]
69
-
70
- first_close < first_open && # First candle is bearish
71
- second_close > second_open && # Second candle is bullish
72
- second_open < first_close && # Second opens below first close
73
- second_close > first_open # Second closes above first open
74
- end
75
-
76
- def self.bearish_engulfing?(first_candle, second_candle)
77
- first_open = first_candle[:open]
78
- first_close = first_candle[:close]
79
- second_open = second_candle[:open]
80
- second_close = second_candle[:close]
81
-
82
- first_close > first_open && # First candle is bullish
83
- second_close < second_open && # Second candle is bearish
84
- second_open > first_close && # Second opens above first close
85
- second_close < first_open # Second closes below first open
86
- end
87
-
88
- def self.hammer?(candle)
89
- body = (candle[:close] - candle[:open]).abs
90
- lower_shadow = [candle[:open], candle[:close]].min - candle[:low]
91
- upper_shadow = candle[:high] - [candle[:open], candle[:close]].max
92
-
93
- lower_shadow > body * 2 && upper_shadow < body * 0.5
94
- end
95
-
96
- def self.shooting_star?(candle)
97
- body = (candle[:close] - candle[:open]).abs
98
- upper_shadow = candle[:high] - [candle[:open], candle[:close]].max
99
- lower_shadow = [candle[:open], candle[:close]].min - candle[:low]
100
-
101
- upper_shadow > body * 2 && lower_shadow < body * 0.5
102
- end
103
-
104
- def self.three_white_soldiers?(candles)
105
- first, second, third = candles
106
- first[:close] > first[:open] &&
107
- second[:close] > second[:open] &&
108
- third[:close] > third[:open] && # All bullish
109
- second[:close] > first[:close] &&
110
- third[:close] > second[:close] # Each closes higher
111
- end
112
-
113
- def self.three_black_crows?(candles)
114
- first, second, third = candles
115
- first[:close] < first[:open] &&
116
- second[:close] < second[:open] &&
117
- third[:close] < third[:open] && # All bearish
118
- second[:close] < first[:close] &&
119
- third[:close] < second[:close] # Each closes lower
120
- end
121
-
122
- def self.head_and_shoulders?(highs, _lows)
123
- return false if highs.length < 20
124
-
125
- # Simplified: look for three peaks with middle one highest
126
- peaks = find_peaks(highs)
127
- return false if peaks.length < 3
128
-
129
- # Check if middle peak is highest (head)
130
- middle_idx = peaks.length / 2
131
- head = peaks[middle_idx]
132
- left_shoulder = peaks[middle_idx - 1]
133
- right_shoulder = peaks[middle_idx + 1]
134
-
135
- head[:value] > left_shoulder[:value] && head[:value] > right_shoulder[:value]
136
- end
137
-
138
- def self.double_top_bottom?(highs, lows, _closes)
139
- return false if highs.length < 20
140
-
141
- # Double top: two similar highs with dip in between
142
- peaks = find_peaks(highs)
143
- troughs = find_troughs(lows)
144
-
145
- if peaks.length >= 2
146
- peak1 = peaks[-2]
147
- peak2 = peaks[-1]
148
- # Check if peaks are similar (within 2%)
149
- if (peak1[:value] - peak2[:value]).abs / peak1[:value] < 0.02
150
- return { type: :double_top, strength: :medium, direction: :bearish }
151
- end
152
- end
153
-
154
- if troughs.length >= 2
155
- trough1 = troughs[-2]
156
- trough2 = troughs[-1]
157
- # Check if troughs are similar
158
- if (trough1[:value] - trough2[:value]).abs / trough1[:value] < 0.02
159
- return { type: :double_bottom, strength: :medium, direction: :bullish }
160
- end
161
- end
162
-
163
- false
164
- end
165
-
166
- def self.find_peaks(values)
167
- peaks = []
168
- (1...(values.length - 1)).each do |i|
169
- peaks << { index: i, value: values[i] } if values[i] > values[i - 1] && values[i] > values[i + 1]
170
- end
171
- peaks
172
- end
173
-
174
- def self.find_troughs(values)
175
- troughs = []
176
- (1...(values.length - 1)).each do |i|
177
- troughs << { index: i, value: values[i] } if values[i] < values[i - 1] && values[i] < values[i + 1]
178
- end
179
- troughs
180
- end
181
-
182
- def self.build_candle(opens, highs, lows, closes, index)
183
- {
184
- open: opens[index],
185
- high: highs[index],
186
- low: lows[index],
187
- close: closes[index]
188
- }
189
- end
190
- end
191
- end
192
- end