ollama-client 0.2.1 → 0.2.3

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +220 -12
  4. data/docs/CLOUD.md +29 -0
  5. data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
  6. data/docs/FEATURES_ADDED.md +145 -0
  7. data/docs/HANDLERS_ANALYSIS.md +190 -0
  8. data/docs/README.md +37 -0
  9. data/docs/SCHEMA_FIXES.md +147 -0
  10. data/docs/TEST_UPDATES.md +107 -0
  11. data/examples/README.md +92 -0
  12. data/examples/advanced_complex_schemas.rb +6 -3
  13. data/examples/advanced_multi_step_agent.rb +13 -7
  14. data/examples/chat_console.rb +143 -0
  15. data/examples/complete_workflow.rb +14 -4
  16. data/examples/dhan_console.rb +843 -0
  17. data/examples/dhanhq/agents/base_agent.rb +0 -2
  18. data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
  19. data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
  20. data/examples/dhanhq/analysis/market_structure.rb +44 -28
  21. data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
  22. data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
  23. data/examples/dhanhq/dhanhq_agent.rb +296 -99
  24. data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
  25. data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
  26. data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
  27. data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
  28. data/examples/dhanhq/services/data_service.rb +5 -7
  29. data/examples/dhanhq/services/trading_service.rb +0 -3
  30. data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
  31. data/examples/dhanhq/technical_analysis_runner.rb +216 -162
  32. data/examples/dhanhq/test_tool_calling.rb +538 -0
  33. data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
  34. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
  35. data/examples/dhanhq_agent.rb +159 -116
  36. data/examples/dhanhq_tools.rb +1158 -251
  37. data/examples/multi_step_agent_with_external_data.rb +368 -0
  38. data/examples/structured_tools.rb +89 -0
  39. data/examples/test_dhanhq_tool_calling.rb +375 -0
  40. data/examples/test_tool_calling.rb +160 -0
  41. data/examples/tool_calling_direct.rb +124 -0
  42. data/examples/tool_dto_example.rb +94 -0
  43. data/exe/dhan_console +4 -0
  44. data/exe/ollama-client +1 -1
  45. data/lib/ollama/agent/executor.rb +116 -15
  46. data/lib/ollama/client.rb +118 -55
  47. data/lib/ollama/config.rb +36 -0
  48. data/lib/ollama/dto.rb +187 -0
  49. data/lib/ollama/embeddings.rb +77 -0
  50. data/lib/ollama/options.rb +104 -0
  51. data/lib/ollama/response.rb +121 -0
  52. data/lib/ollama/tool/function/parameters/property.rb +72 -0
  53. data/lib/ollama/tool/function/parameters.rb +101 -0
  54. data/lib/ollama/tool/function.rb +78 -0
  55. data/lib/ollama/tool.rb +60 -0
  56. data/lib/ollama/version.rb +1 -1
  57. data/lib/ollama_client.rb +3 -0
  58. metadata +31 -3
  59. /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
  60. /data/{TESTING.md → docs/TESTING.md} +0 -0
@@ -24,8 +24,6 @@ module DhanHQ
24
24
  return low_confidence_decision(decision) if low_confidence?(decision)
25
25
 
26
26
  decision
27
- rescue Ollama::Error => e
28
- error_decision(e.message)
29
27
  rescue StandardError => e
30
28
  error_decision(e.message)
31
29
  end
@@ -30,8 +30,7 @@ module DhanHQ
30
30
 
31
31
  protected
32
32
 
33
- def build_analysis_prompt(market_context:)
34
- # Not used, but required by base class
33
+ def build_analysis_prompt(*)
35
34
  ""
36
35
  end
37
36
 
@@ -69,8 +69,7 @@ module DhanHQ
69
69
 
70
70
  protected
71
71
 
72
- def build_analysis_prompt(market_context:)
73
- # Not used in this agent, but required by base class
72
+ def build_analysis_prompt(*)
74
73
  ""
75
74
  end
76
75
 
@@ -79,58 +78,72 @@ module DhanHQ
79
78
  def convert_to_ohlc(historical_data)
80
79
  return [] unless historical_data.is_a?(Hash)
81
80
 
82
- # Navigate to the actual data: result -> result -> data
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)
83
91
  outer_result = historical_data[:result] || historical_data["result"]
84
- return [] unless outer_result.is_a?(Hash)
92
+ return nil unless outer_result.is_a?(Hash)
85
93
 
86
- data = outer_result[:data] || outer_result["data"]
87
- return [] unless data
94
+ outer_result[:data] || outer_result["data"]
95
+ end
88
96
 
89
- # Handle DhanHQ format: {open: [...], high: [...], low: [...], close: [...], volume: [...]}
90
- if data.is_a?(Hash)
91
- opens = data[:open] || data["open"] || []
92
- highs = data[:high] || data["high"] || []
93
- lows = data[:low] || data["low"] || []
94
- closes = data[:close] || data["close"] || []
95
- volumes = data[:volume] || data["volume"] || []
96
-
97
- return [] if closes.nil? || closes.empty?
98
-
99
- # Convert parallel arrays to array of hashes
100
- max_length = [opens.length, highs.length, lows.length, closes.length].max
101
- return [] if max_length.zero?
102
-
103
- ohlc_data = []
104
-
105
- (0...max_length).each do |i|
106
- ohlc_data << {
107
- open: opens[i] || closes[i] || 0,
108
- high: highs[i] || closes[i] || 0,
109
- low: lows[i] || closes[i] || 0,
110
- close: closes[i] || 0,
111
- volume: volumes[i] || 0
112
- }
113
- end
97
+ def ohlc_from_hash(data)
98
+ series = extract_series(data)
99
+ return [] if series[:closes].nil? || series[:closes].empty?
114
100
 
115
- return ohlc_data
116
- end
101
+ max_length = series_lengths(series).max
102
+ return [] if max_length.zero?
117
103
 
118
- # Handle array format: [{open, high, low, close, volume}, ...]
119
- if data.is_a?(Array)
120
- return data.map do |bar|
121
- next nil unless bar.is_a?(Hash)
122
-
123
- {
124
- open: bar["open"] || bar[:open],
125
- high: bar["high"] || bar[:high],
126
- low: bar["low"] || bar[:low],
127
- close: bar["close"] || bar[:close],
128
- volume: bar["volume"] || bar[:volume]
129
- }
130
- end.compact
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
+ }
131
130
  end
131
+ end
132
132
 
133
- []
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
+ }
134
147
  end
135
148
 
136
149
  def interpret_analysis(symbol, analysis)
@@ -214,7 +227,7 @@ module DhanHQ
214
227
  }
215
228
  end
216
229
 
217
- def build_recommendation_schema(trading_style)
230
+ def build_recommendation_schema(_trading_style)
218
231
  {
219
232
  "type" => "object",
220
233
  "properties" => {
@@ -223,7 +236,12 @@ module DhanHQ
223
236
  "stop_loss" => { "type" => "number" },
224
237
  "target_price" => { "type" => "number" },
225
238
  "risk_reward_ratio" => { "type" => "number" },
226
- "confidence" => { "type" => "number", "minimum" => 0, "maximum" => 1 },
239
+ "confidence" => {
240
+ "type" => "number",
241
+ "minimum" => 0,
242
+ "maximum" => 1,
243
+ "description" => "Confidence (0.0 to 1.0)"
244
+ },
227
245
  "reasoning" => { "type" => "string" },
228
246
  "timeframe" => { "type" => "string" }
229
247
  }
@@ -9,34 +9,8 @@ module DhanHQ
9
9
  def self.analyze_trend(highs, lows, closes)
10
10
  return { trend: :unknown, strength: 0 } if closes.nil? || closes.length < 3
11
11
 
12
- # Higher highs and higher lows = Uptrend
13
- # Lower highs and lower lows = Downtrend
14
- recent_highs = highs.last(10) if highs
15
- recent_lows = lows.last(10) if lows
16
- recent_closes = closes.last(10)
17
-
18
- higher_highs = recent_highs ? recent_highs.each_cons(2).all? { |a, b| b >= a } : false
19
- higher_lows = recent_lows ? recent_lows.each_cons(2).all? { |a, b| b >= a } : false
20
- lower_highs = recent_highs ? recent_highs.each_cons(2).all? { |a, b| b <= a } : false
21
- lower_lows = recent_lows ? recent_lows.each_cons(2).all? { |a, b| b <= a } : false
22
-
23
- trend = if higher_highs && higher_lows
24
- :uptrend
25
- elsif lower_highs && lower_lows
26
- :downtrend
27
- else
28
- :sideways
29
- end
30
-
31
- # Calculate trend strength using moving averages
32
- sma_20 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 20)
33
- sma_50 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 50)
34
-
35
- strength = if sma_20.last && sma_50.last
36
- ((sma_20.last - sma_50.last) / sma_50.last * 100).abs
37
- else
38
- 0
39
- end
12
+ trend = infer_trend(highs, lows)
13
+ strength = moving_average_strength(closes)
40
14
 
41
15
  { trend: trend, strength: strength.round(2) }
42
16
  end
@@ -117,6 +91,48 @@ module DhanHQ
117
91
 
118
92
  { broken: broken, direction: direction, current_trend: trend[:trend] }
119
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
120
136
  end
121
137
  end
122
138
  end
@@ -11,35 +11,31 @@ module DhanHQ
11
11
  patterns = []
12
12
 
13
13
  (2...closes.length).each do |i|
14
- o1, h1, l1, c1 = opens[i - 2], highs[i - 2], lows[i - 2], closes[i - 2]
15
- o2, h2, l2, c2 = opens[i - 1], highs[i - 1], lows[i - 1], closes[i - 1]
16
- o3, h3, l3, c3 = opens[i], highs[i], lows[i], closes[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
17
 
18
18
  # Engulfing patterns
19
- if bullish_engulfing?(o1, c1, o2, c2)
19
+ if bullish_engulfing?(first_candle, second_candle)
20
20
  patterns << { type: :bullish_engulfing, index: i, strength: :medium }
21
21
  end
22
22
 
23
- if bearish_engulfing?(o1, c1, o2, c2)
23
+ if bearish_engulfing?(first_candle, second_candle)
24
24
  patterns << { type: :bearish_engulfing, index: i, strength: :medium }
25
25
  end
26
26
 
27
27
  # Hammer pattern
28
- if hammer?(o2, h2, l2, c2)
29
- patterns << { type: :hammer, index: i - 1, strength: :medium }
30
- end
28
+ patterns << { type: :hammer, index: i - 1, strength: :medium } if hammer?(second_candle)
31
29
 
32
30
  # Shooting star
33
- if shooting_star?(o2, h2, l2, c2)
34
- patterns << { type: :shooting_star, index: i - 1, strength: :medium }
35
- end
31
+ patterns << { type: :shooting_star, index: i - 1, strength: :medium } if shooting_star?(second_candle)
36
32
 
37
33
  # Three white soldiers / three black crows
38
- if three_white_soldiers?(c1, c2, c3, o1, o2, o3)
34
+ if three_white_soldiers?([first_candle, second_candle, third_candle])
39
35
  patterns << { type: :three_white_soldiers, index: i, strength: :strong }
40
36
  end
41
37
 
42
- if three_black_crows?(c1, c2, c3, o1, o2, o3)
38
+ if three_black_crows?([first_candle, second_candle, third_candle])
43
39
  patterns << { type: :three_black_crows, index: i, strength: :strong }
44
40
  end
45
41
  end
@@ -65,46 +61,62 @@ module DhanHQ
65
61
  patterns
66
62
  end
67
63
 
68
- private
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
69
 
70
- def self.bullish_engulfing?(o1, c1, o2, c2)
71
- c1 < o1 && # First candle is bearish
72
- c2 > o2 && # Second candle is bullish
73
- o2 < c1 && # Second opens below first close
74
- c2 > o1 # Second closes above first open
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
75
74
  end
76
75
 
77
- def self.bearish_engulfing?(o1, c1, o2, c2)
78
- c1 > o1 && # First candle is bullish
79
- c2 < o2 && # Second candle is bearish
80
- o2 > c1 && # Second opens above first close
81
- c2 < o1 # Second closes below first open
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
82
86
  end
83
87
 
84
- def self.hammer?(open, high, low, close)
85
- body = (close - open).abs
86
- lower_shadow = [open, close].min - low
87
- upper_shadow = high - [open, close].max
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
88
92
 
89
93
  lower_shadow > body * 2 && upper_shadow < body * 0.5
90
94
  end
91
95
 
92
- def self.shooting_star?(open, high, low, close)
93
- body = (close - open).abs
94
- upper_shadow = high - [open, close].max
95
- lower_shadow = [open, close].min - low
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]
96
100
 
97
101
  upper_shadow > body * 2 && lower_shadow < body * 0.5
98
102
  end
99
103
 
100
- def self.three_white_soldiers?(c1, c2, c3, o1, o2, o3)
101
- c1 > o1 && c2 > o2 && c3 > o3 && # All bullish
102
- c2 > c1 && c3 > c2 # Each closes higher
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
103
111
  end
104
112
 
105
- def self.three_black_crows?(c1, c2, c3, o1, o2, o3)
106
- c1 < o1 && c2 < o2 && c3 < o3 && # All bearish
107
- c2 < c1 && c3 < c2 # Each closes lower
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
108
120
  end
109
121
 
110
122
  def self.head_and_shoulders?(highs, _lows)
@@ -123,8 +135,8 @@ module DhanHQ
123
135
  head[:value] > left_shoulder[:value] && head[:value] > right_shoulder[:value]
124
136
  end
125
137
 
126
- def self.double_top_bottom?(highs, lows, closes)
127
- return nil if highs.length < 20
138
+ def self.double_top_bottom?(highs, lows, _closes)
139
+ return false if highs.length < 20
128
140
 
129
141
  # Double top: two similar highs with dip in between
130
142
  peaks = find_peaks(highs)
@@ -148,15 +160,13 @@ module DhanHQ
148
160
  end
149
161
  end
150
162
 
151
- nil
163
+ false
152
164
  end
153
165
 
154
166
  def self.find_peaks(values)
155
167
  peaks = []
156
168
  (1...(values.length - 1)).each do |i|
157
- if values[i] > values[i - 1] && values[i] > values[i + 1]
158
- peaks << { index: i, value: values[i] }
159
- end
169
+ peaks << { index: i, value: values[i] } if values[i] > values[i - 1] && values[i] > values[i + 1]
160
170
  end
161
171
  peaks
162
172
  end
@@ -164,12 +174,19 @@ module DhanHQ
164
174
  def self.find_troughs(values)
165
175
  troughs = []
166
176
  (1...(values.length - 1)).each do |i|
167
- if values[i] < values[i - 1] && values[i] < values[i + 1]
168
- troughs << { index: i, value: values[i] }
169
- end
177
+ troughs << { index: i, value: values[i] } if values[i] < values[i - 1] && values[i] < values[i + 1]
170
178
  end
171
179
  troughs
172
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
173
190
  end
174
191
  end
175
192
  end
@@ -20,9 +20,9 @@ module DhanHQ
20
20
  return {} if closes.nil? || closes.empty?
21
21
 
22
22
  # Technical indicators
23
- sma_20 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 20)
24
- sma_50 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 50)
25
- ema_12 = DhanHQ::Indicators::TechnicalIndicators.ema(closes, 12)
23
+ sma20 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 20)
24
+ sma50 = DhanHQ::Indicators::TechnicalIndicators.sma(closes, 50)
25
+ ema12 = DhanHQ::Indicators::TechnicalIndicators.ema(closes, 12)
26
26
  rsi = DhanHQ::Indicators::TechnicalIndicators.rsi(closes, 14)
27
27
  macd = DhanHQ::Indicators::TechnicalIndicators.macd(closes)
28
28
 
@@ -43,9 +43,9 @@ module DhanHQ
43
43
  trend: trend,
44
44
  structure_break: structure_break,
45
45
  indicators: {
46
- sma_20: sma_20.last,
47
- sma_50: sma_50.last,
48
- ema_12: ema_12.last,
46
+ sma20: sma20.last,
47
+ sma50: sma50.last,
48
+ ema12: ema12.last,
49
49
  rsi: rsi.last,
50
50
  macd: macd[:macd].last,
51
51
  macd_signal: macd[:signal].last,
@@ -64,8 +64,6 @@ module DhanHQ
64
64
  }
65
65
  end
66
66
 
67
- private
68
-
69
67
  def self.extract_closes(data)
70
68
  data.map { |d| d[:close] || d["close"] || d["c"] || d[:c] }.compact
71
69
  end