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
@@ -59,6 +59,139 @@ module DhanHQ
59
59
  end
60
60
  end
61
61
 
62
+ def price_range_stats(price_ranges)
63
+ return nil unless price_ranges.is_a?(Array) && price_ranges.any?
64
+
65
+ {
66
+ min: price_ranges.min.round(2),
67
+ max: price_ranges.max.round(2),
68
+ avg: (price_ranges.sum / price_ranges.length).round(2),
69
+ count: price_ranges.length
70
+ }
71
+ end
72
+
73
+ def build_expired_options_summary(stats)
74
+ {
75
+ data_points: stats[:data_points] || 0,
76
+ avg_volume: stats[:avg_volume]&.round(2),
77
+ avg_open_interest: stats[:avg_open_interest]&.round(2),
78
+ avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
79
+ price_range_stats: price_range_stats(stats[:price_ranges]),
80
+ has_ohlc: stats[:has_ohlc],
81
+ has_volume: stats[:has_volume],
82
+ has_open_interest: stats[:has_open_interest],
83
+ has_implied_volatility: stats[:has_implied_volatility]
84
+ }
85
+ end
86
+
87
+ def build_option_chain_summary(chain_result)
88
+ chain = chain_result[:result][:chain]
89
+ underlying_price = chain_result[:result][:underlying_last_price]
90
+
91
+ unless chain.is_a?(Hash)
92
+ return [{ expiry: chain_result[:result][:expiry], chain_type: chain.class },
93
+ underlying_price]
94
+ end
95
+
96
+ strike_prices = chain.keys.sort_by(&:to_f)
97
+ first_strike_data = strike_prices.any? ? chain[strike_prices.first] : nil
98
+ atm_strike = select_atm_strike(strike_prices, underlying_price)
99
+ atm_data = atm_strike ? chain[atm_strike] : nil
100
+ sample_greeks = build_sample_greeks(atm_data, atm_strike)
101
+
102
+ summary = {
103
+ expiry: chain_result[:result][:expiry],
104
+ underlying_last_price: underlying_price,
105
+ strikes_count: strike_prices.length,
106
+ has_call_options: option_type_present?(first_strike_data, "ce"),
107
+ has_put_options: option_type_present?(first_strike_data, "pe"),
108
+ has_greeks: sample_greeks.any?,
109
+ strike_range: strike_range_summary(strike_prices),
110
+ sample_greeks: sample_greeks.any? ? sample_greeks : nil
111
+ }
112
+
113
+ [summary, underlying_price]
114
+ end
115
+
116
+ def select_atm_strike(strike_prices, underlying_price)
117
+ return strike_prices.first unless underlying_price && strike_prices.any?
118
+
119
+ strike_prices.min_by { |strike| (strike.to_f - underlying_price).abs }
120
+ end
121
+
122
+ def option_type_present?(strike_data, key)
123
+ strike_data.is_a?(Hash) && (strike_data.key?(key) || strike_data.key?(key.to_sym))
124
+ end
125
+
126
+ def strike_range_summary(strike_prices)
127
+ return nil if strike_prices.empty?
128
+
129
+ {
130
+ min: strike_prices.first,
131
+ max: strike_prices.last,
132
+ sample_strikes: strike_prices.first(5)
133
+ }
134
+ end
135
+
136
+ def build_sample_greeks(atm_data, atm_strike)
137
+ return {} unless atm_data.is_a?(Hash)
138
+
139
+ sample = {}
140
+ call_data = atm_data["ce"] || atm_data[:ce]
141
+ put_data = atm_data["pe"] || atm_data[:pe]
142
+
143
+ call_greeks = extract_greeks(call_data)
144
+ sample[:call] = greeks_summary(call_greeks, call_data, atm_strike) if call_greeks
145
+
146
+ put_greeks = extract_greeks(put_data)
147
+ sample[:put] = greeks_summary(put_greeks, put_data, atm_strike) if put_greeks
148
+
149
+ sample
150
+ end
151
+
152
+ def extract_greeks(option_data)
153
+ return nil unless option_data.is_a?(Hash)
154
+ return nil unless option_data.key?("greeks") || option_data.key?(:greeks)
155
+
156
+ option_data["greeks"] || option_data[:greeks]
157
+ end
158
+
159
+ def greeks_summary(greeks, option_data, atm_strike)
160
+ {
161
+ strike: atm_strike,
162
+ delta: greeks["delta"] || greeks[:delta],
163
+ theta: greeks["theta"] || greeks[:theta],
164
+ gamma: greeks["gamma"] || greeks[:gamma],
165
+ vega: greeks["vega"] || greeks[:vega],
166
+ iv: option_data["implied_volatility"] || option_data[:implied_volatility],
167
+ oi: option_data["oi"] || option_data[:oi],
168
+ last_price: option_data["last_price"] || option_data[:last_price]
169
+ }
170
+ end
171
+
172
+ def format_score_breakdown(details)
173
+ "Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
174
+ "Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
175
+ end
176
+
177
+ def format_option_setup_details(setup)
178
+ iv = setup[:iv]&.round(2) || "N/A"
179
+ oi = setup[:oi] || "N/A"
180
+ volume = setup[:volume] || "N/A"
181
+ "IV: #{iv}% | OI: #{oi} | Volume: #{volume}"
182
+ end
183
+
184
+ def handle_option_chain_result(chain_result)
185
+ if chain_result[:result] && chain_result[:result][:chain]
186
+ chain_summary, underlying_price = build_option_chain_summary(chain_result)
187
+ puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
188
+ puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
189
+ puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
190
+ elsif chain_result[:error]
191
+ puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
192
+ end
193
+ end
194
+
62
195
  # Main execution
63
196
  if __FILE__ == $PROGRAM_NAME
64
197
  # Configure DhanHQ
@@ -267,7 +400,7 @@ if __FILE__ == $PROGRAM_NAME
267
400
  puts " ✅ Historical data retrieved"
268
401
  puts " 📊 Type: #{result[:type]}"
269
402
  puts " 📊 Records: #{result[:result][:count]}"
270
- if result[:result][:count] == 0
403
+ if result[:result][:count].zero?
271
404
  puts " ⚠️ No data found for date range #{from_date} to #{to_date}"
272
405
  puts " (This may be normal if market was closed or data unavailable)"
273
406
  end
@@ -300,26 +433,7 @@ if __FILE__ == $PROGRAM_NAME
300
433
  puts " 📊 Expiry: #{result[:result][:expiry_date]}"
301
434
  if result[:result][:summary_stats]
302
435
  stats = result[:result][:summary_stats]
303
- concise_summary = {
304
- data_points: stats[:data_points] || 0,
305
- avg_volume: stats[:avg_volume]&.round(2),
306
- avg_open_interest: stats[:avg_open_interest]&.round(2),
307
- avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
308
- price_range_stats: if stats[:price_ranges]&.is_a?(Array) && !stats[:price_ranges].empty?
309
- {
310
- min: stats[:price_ranges].min.round(2),
311
- max: stats[:price_ranges].max.round(2),
312
- avg: (stats[:price_ranges].sum / stats[:price_ranges].length).round(2),
313
- count: stats[:price_ranges].length
314
- }
315
- else
316
- nil
317
- end,
318
- has_ohlc: stats[:has_ohlc],
319
- has_volume: stats[:has_volume],
320
- has_open_interest: stats[:has_open_interest],
321
- has_implied_volatility: stats[:has_implied_volatility]
322
- }
436
+ concise_summary = build_expired_options_summary(stats)
323
437
  puts " 📊 Data summary: #{JSON.pretty_generate(concise_summary)}"
324
438
  else
325
439
  puts " 📊 Data available but summary stats not found"
@@ -355,82 +469,7 @@ if __FILE__ == $PROGRAM_NAME
355
469
  exchange_segment: "IDX_I",
356
470
  expiry: next_expiry
357
471
  )
358
- if chain_result[:result] && chain_result[:result][:chain]
359
- chain = chain_result[:result][:chain]
360
- underlying_price = chain_result[:result][:underlying_last_price]
361
-
362
- chain_summary = if chain.is_a?(Hash)
363
- strike_prices = chain.keys.sort_by { |k| k.to_f }
364
- first_strike_data = chain[strike_prices.first] unless strike_prices.empty?
365
-
366
- atm_strike = if underlying_price && !strike_prices.empty?
367
- strike_prices.min_by { |s| (s.to_f - underlying_price).abs }
368
- else
369
- strike_prices.first
370
- end
371
- atm_data = chain[atm_strike] if atm_strike
372
-
373
- sample_greeks = {}
374
- if atm_data.is_a?(Hash)
375
- ce_data = atm_data["ce"] || atm_data[:ce]
376
- pe_data = atm_data["pe"] || atm_data[:pe]
377
-
378
- if ce_data.is_a?(Hash) && (ce_data.key?("greeks") || ce_data.key?(:greeks))
379
- ce_greeks = ce_data["greeks"] || ce_data[:greeks]
380
- sample_greeks[:call] = {
381
- strike: atm_strike,
382
- delta: ce_greeks["delta"] || ce_greeks[:delta],
383
- theta: ce_greeks["theta"] || ce_greeks[:theta],
384
- gamma: ce_greeks["gamma"] || ce_greeks[:gamma],
385
- vega: ce_greeks["vega"] || ce_greeks[:vega],
386
- iv: ce_data["implied_volatility"] || ce_data[:implied_volatility],
387
- oi: ce_data["oi"] || ce_data[:oi],
388
- last_price: ce_data["last_price"] || ce_data[:last_price]
389
- }
390
- end
391
-
392
- if pe_data.is_a?(Hash) && (pe_data.key?("greeks") || pe_data.key?(:greeks))
393
- pe_greeks = pe_data["greeks"] || pe_data[:greeks]
394
- sample_greeks[:put] = {
395
- strike: atm_strike,
396
- delta: pe_greeks["delta"] || pe_greeks[:delta],
397
- theta: pe_greeks["theta"] || pe_greeks[:theta],
398
- gamma: pe_greeks["gamma"] || pe_greeks[:gamma],
399
- vega: pe_greeks["vega"] || pe_greeks[:vega],
400
- iv: pe_data["implied_volatility"] || pe_data[:implied_volatility],
401
- oi: pe_data["oi"] || pe_data[:oi],
402
- last_price: pe_data["last_price"] || pe_data[:last_price]
403
- }
404
- end
405
- end
406
-
407
- {
408
- expiry: chain_result[:result][:expiry],
409
- underlying_last_price: underlying_price,
410
- strikes_count: strike_prices.length,
411
- has_call_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("ce") || first_strike_data.key?(:ce)),
412
- has_put_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("pe") || first_strike_data.key?(:pe)),
413
- has_greeks: !sample_greeks.empty?,
414
- strike_range: if strike_prices.empty?
415
- nil
416
- else
417
- {
418
- min: strike_prices.first,
419
- max: strike_prices.last,
420
- sample_strikes: strike_prices.first(5)
421
- }
422
- end,
423
- sample_greeks: sample_greeks.empty? ? nil : sample_greeks
424
- }
425
- else
426
- { expiry: chain_result[:result][:expiry], chain_type: chain.class }
427
- end
428
- puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
429
- puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
430
- puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
431
- elsif chain_result[:error]
432
- puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
433
- end
472
+ handle_option_chain_result(chain_result)
434
473
  end
435
474
  elsif expiry_list_result[:error]
436
475
  puts " ⚠️ #{expiry_list_result[:error]}"
@@ -573,7 +612,7 @@ if __FILE__ == $PROGRAM_NAME
573
612
  puts " 📈 #{candidate[:symbol]}: Score #{candidate[:score]}/100"
574
613
  if candidate[:score_details]
575
614
  details = candidate[:score_details]
576
- puts " Breakdown: Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
615
+ puts " Breakdown: #{format_score_breakdown(details)}"
577
616
  end
578
617
  trend = candidate[:analysis][:trend]
579
618
  puts " Trend: #{trend[:trend]} (#{trend[:strength]}% strength)"
@@ -605,7 +644,7 @@ if __FILE__ == $PROGRAM_NAME
605
644
  puts " ✅ Found #{options_setups[:setups].length} options setups:"
606
645
  options_setups[:setups].each do |setup|
607
646
  puts " 📊 #{setup[:type].to_s.upcase} @ #{setup[:strike]}"
608
- puts " IV: #{setup[:iv]&.round(2) || 'N/A'}% | OI: #{setup[:oi] || 'N/A'} | Volume: #{setup[:volume] || 'N/A'}"
647
+ puts " #{format_option_setup_details(setup)}"
609
648
  puts " Score: #{setup[:score]}/100 | Recommendation: #{setup[:recommendation]}"
610
649
  end
611
650
  else
@@ -617,6 +656,163 @@ if __FILE__ == $PROGRAM_NAME
617
656
  puts " #{e.backtrace.first(3).join("\n ")}" if e.backtrace
618
657
  end
619
658
 
659
+ puts
660
+ puts "=" * 60
661
+ puts "TOOL CALLING EXAMPLE (Using Executor + Structured Tool Classes)"
662
+ puts "=" * 60
663
+ puts
664
+
665
+ # ============================================================
666
+ # TOOL CALLING WITH EXECUTOR
667
+ # ============================================================
668
+ puts "Example: Agentic Tool Calling with DhanHQ Tools"
669
+ puts "─" * 60
670
+ puts "This demonstrates the new tool calling pattern using:"
671
+ puts " - Structured Tool classes (type-safe schemas)"
672
+ puts " - Executor (automatic tool execution loop)"
673
+ puts " - chat_raw() internally (via Executor)"
674
+ puts
675
+
676
+ # Define DhanHQ tools using structured Tool classes
677
+ market_quote_tool = Ollama::Tool.new(
678
+ type: "function",
679
+ function: Ollama::Tool::Function.new(
680
+ name: "get_market_quote",
681
+ description: "Get market quote for a symbol. Returns OHLC, depth, volume, and other market data. " \
682
+ "Finds instrument automatically using exchange_segment and symbol.",
683
+ parameters: Ollama::Tool::Function::Parameters.new(
684
+ type: "object",
685
+ properties: {
686
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
687
+ type: "string",
688
+ description: "Stock or index symbol (e.g., RELIANCE, NIFTY)"
689
+ ),
690
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
691
+ type: "string",
692
+ description: "Exchange segment",
693
+ enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
694
+ )
695
+ },
696
+ required: %w[symbol exchange_segment]
697
+ )
698
+ )
699
+ )
700
+
701
+ live_ltp_tool = Ollama::Tool.new(
702
+ type: "function",
703
+ function: Ollama::Tool::Function.new(
704
+ name: "get_live_ltp",
705
+ description: "Get live last traded price (LTP) for a symbol. Fast API for current price. " \
706
+ "Finds instrument automatically using exchange_segment and symbol.",
707
+ parameters: Ollama::Tool::Function::Parameters.new(
708
+ type: "object",
709
+ properties: {
710
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
711
+ type: "string",
712
+ description: "Stock or index symbol"
713
+ ),
714
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
715
+ type: "string",
716
+ description: "Exchange segment",
717
+ enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
718
+ )
719
+ },
720
+ required: %w[symbol exchange_segment]
721
+ )
722
+ )
723
+ )
724
+
725
+ # Define tools with structured Tool classes and callables
726
+ tools = {
727
+ "get_market_quote" => {
728
+ tool: market_quote_tool,
729
+ callable: lambda do |symbol:, exchange_segment:|
730
+ result = DhanHQDataTools.get_market_quote(
731
+ symbol: symbol.to_s,
732
+ exchange_segment: exchange_segment.to_s
733
+ )
734
+
735
+ if result[:error]
736
+ { error: result[:error] }
737
+ else
738
+ quote = result[:result][:quote]
739
+ {
740
+ symbol: symbol,
741
+ exchange_segment: exchange_segment,
742
+ last_price: quote[:last_price],
743
+ volume: quote[:volume],
744
+ ohlc: quote[:ohlc],
745
+ change_percent: quote[:net_change]
746
+ }
747
+ end
748
+ rescue StandardError => e
749
+ { error: e.message }
750
+ end
751
+ },
752
+
753
+ "get_live_ltp" => {
754
+ tool: live_ltp_tool,
755
+ callable: lambda do |symbol:, exchange_segment:|
756
+ result = DhanHQDataTools.get_live_ltp(
757
+ symbol: symbol.to_s,
758
+ exchange_segment: exchange_segment.to_s
759
+ )
760
+
761
+ if result[:error]
762
+ { error: result[:error] }
763
+ else
764
+ {
765
+ symbol: symbol,
766
+ exchange_segment: exchange_segment,
767
+ ltp: result[:result][:ltp],
768
+ timestamp: result[:result][:timestamp]
769
+ }
770
+ end
771
+ rescue StandardError => e
772
+ { error: e.message }
773
+ end
774
+ }
775
+ }
776
+
777
+ # Create executor with tools
778
+ # Create client with same configuration as other examples
779
+ executor_config = Ollama::Config.new
780
+ executor_config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
781
+ executor_config.temperature = 0.2
782
+ executor_config.timeout = 60
783
+ executor_client = Ollama::Client.new(config: executor_config)
784
+
785
+ executor = Ollama::Agent::Executor.new(
786
+ executor_client,
787
+ tools: tools,
788
+ max_steps: 10
789
+ )
790
+
791
+ begin
792
+ puts "🔄 Starting agentic tool-calling loop..."
793
+ puts "User query: Get market quote for RELIANCE and also check NIFTY's current price"
794
+ puts
795
+
796
+ result = executor.run(
797
+ system: "You are a market data assistant. Use the available tools to get market data. " \
798
+ "You can call multiple tools in sequence. When you have the data, summarize it for the user.",
799
+ user: "Get market quote for RELIANCE stock and also check NIFTY's current price"
800
+ )
801
+
802
+ puts
803
+ puts "=" * 60
804
+ puts "Tool Calling Result:"
805
+ puts "=" * 60
806
+ puts result
807
+ puts
808
+ rescue Ollama::Error => e
809
+ puts "❌ Error: #{e.message}"
810
+ puts e.backtrace.first(5).join("\n") if e.backtrace
811
+ rescue StandardError => e
812
+ puts "❌ Unexpected error: #{e.message}"
813
+ puts e.backtrace.first(3).join("\n") if e.backtrace
814
+ end
815
+
620
816
  puts
621
817
  puts "=" * 60
622
818
  puts "DhanHQ Agent Summary:"
@@ -628,5 +824,6 @@ if __FILE__ == $PROGRAM_NAME
628
824
  puts " ✅ Technical Analysis: Trend analysis, SMC concepts, Pattern recognition, Indicators (RSI, MACD, MA, etc.)"
629
825
  puts " ✅ Scanners: Swing trading scanner, Intraday options scanner"
630
826
  puts " ✅ Analysis Agents: Technical analysis agent with LLM interpretation"
827
+ puts " ✅ Tool Calling: Executor with structured Tool classes (NEW!)"
631
828
  puts "=" * 60
632
829
  end
@@ -35,8 +35,8 @@ module DhanHQ
35
35
 
36
36
  prices.each_cons(2) do |prev, curr|
37
37
  change = curr - prev
38
- gains << (change > 0 ? change : 0)
39
- losses << (change < 0 ? -change : 0)
38
+ gains << [change, 0].max
39
+ losses << (change.negative? ? -change : 0)
40
40
  end
41
41
 
42
42
  rsi_values = []
@@ -81,7 +81,7 @@ module DhanHQ
81
81
  sma_values = sma(prices, period)
82
82
  bands = { upper: [], middle: sma_values, lower: [] }
83
83
 
84
- prices.each_cons(period).each_with_index do |window, idx|
84
+ prices.each_cons(period).with_index do |window, idx|
85
85
  mean = sma_values[idx]
86
86
  variance = window.map { |p| (p - mean)**2 }.sum / period
87
87
  std = Math.sqrt(variance)
@@ -147,8 +147,6 @@ module DhanHQ
147
147
  { support: support_levels.uniq { |s| s[:price] }, resistance: resistance_levels.uniq { |r| r[:price] } }
148
148
  end
149
149
 
150
- private
151
-
152
150
  def self.calculate_rsi_value(avg_gain, avg_loss)
153
151
  return 100 if avg_loss.zero?
154
152