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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +560 -106
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/README.md +2 -3
- data/docs/RELEASE_GUIDE.md +376 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/docs/ruby_guide.md +6232 -0
- data/examples/README.md +51 -66
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +43 -21
- data/lib/ollama/config.rb +4 -1
- data/lib/ollama/document_loader.rb +163 -0
- data/lib/ollama/embeddings.rb +42 -13
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +8 -0
- metadata +31 -53
- data/docs/GEM_RELEASE_GUIDE.md +0 -794
- data/docs/GET_RUBYGEMS_SECRET.md +0 -151
- data/docs/QUICK_OTP_SETUP.md +0 -80
- data/docs/QUICK_RELEASE.md +0 -106
- data/docs/RUBYGEMS_OTP_SETUP.md +0 -199
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- data/exe/dhan_console +0 -4
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module DhanHQ
|
|
4
|
-
module Indicators
|
|
5
|
-
# Technical analysis indicators
|
|
6
|
-
class TechnicalIndicators
|
|
7
|
-
# Simple Moving Average
|
|
8
|
-
def self.sma(prices, period)
|
|
9
|
-
return [] if prices.nil? || prices.empty? || period < 1
|
|
10
|
-
|
|
11
|
-
prices.each_cons(period).map { |window| window.sum.to_f / period }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Exponential Moving Average
|
|
15
|
-
def self.ema(prices, period)
|
|
16
|
-
return [] if prices.nil? || prices.empty? || period < 1
|
|
17
|
-
|
|
18
|
-
multiplier = 2.0 / (period + 1)
|
|
19
|
-
ema_values = []
|
|
20
|
-
ema_values << prices.first.to_f
|
|
21
|
-
|
|
22
|
-
prices[1..].each do |price|
|
|
23
|
-
ema_values << ((price.to_f - ema_values.last) * multiplier + ema_values.last)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
ema_values
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Relative Strength Index
|
|
30
|
-
def self.rsi(prices, period = 14)
|
|
31
|
-
return [] if prices.nil? || prices.length < period + 1
|
|
32
|
-
|
|
33
|
-
gains = []
|
|
34
|
-
losses = []
|
|
35
|
-
|
|
36
|
-
prices.each_cons(2) do |prev, curr|
|
|
37
|
-
change = curr - prev
|
|
38
|
-
gains << [change, 0].max
|
|
39
|
-
losses << (change.negative? ? -change : 0)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
rsi_values = []
|
|
43
|
-
avg_gain = gains.first(period).sum.to_f / period
|
|
44
|
-
avg_loss = losses.first(period).sum.to_f / period
|
|
45
|
-
|
|
46
|
-
rsi_values << calculate_rsi_value(avg_gain, avg_loss)
|
|
47
|
-
|
|
48
|
-
(period...gains.length).each do |i|
|
|
49
|
-
avg_gain = ((avg_gain * (period - 1)) + gains[i]) / period
|
|
50
|
-
avg_loss = ((avg_loss * (period - 1)) + losses[i]) / period
|
|
51
|
-
rsi_values << calculate_rsi_value(avg_gain, avg_loss)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
rsi_values
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# MACD (Moving Average Convergence Divergence)
|
|
58
|
-
def self.macd(prices, fast_period = 12, slow_period = 26, signal_period = 9)
|
|
59
|
-
return { macd: [], signal: [], histogram: [] } if prices.nil? || prices.empty?
|
|
60
|
-
|
|
61
|
-
fast_ema = ema(prices, fast_period)
|
|
62
|
-
slow_ema = ema(prices, slow_period)
|
|
63
|
-
|
|
64
|
-
min_length = [fast_ema.length, slow_ema.length].min
|
|
65
|
-
macd_line = fast_ema.last(min_length).zip(slow_ema.last(min_length)).map { |f, s| f - s }
|
|
66
|
-
|
|
67
|
-
signal_line = ema(macd_line, signal_period)
|
|
68
|
-
histogram = macd_line.last(signal_line.length).zip(signal_line).map { |m, s| m - s }
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
macd: macd_line,
|
|
72
|
-
signal: signal_line,
|
|
73
|
-
histogram: histogram
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Bollinger Bands
|
|
78
|
-
def self.bollinger_bands(prices, period = 20, std_dev = 2)
|
|
79
|
-
return { upper: [], middle: [], lower: [] } if prices.nil? || prices.empty?
|
|
80
|
-
|
|
81
|
-
sma_values = sma(prices, period)
|
|
82
|
-
bands = { upper: [], middle: sma_values, lower: [] }
|
|
83
|
-
|
|
84
|
-
prices.each_cons(period).with_index do |window, idx|
|
|
85
|
-
mean = sma_values[idx]
|
|
86
|
-
variance = window.map { |p| (p - mean)**2 }.sum / period
|
|
87
|
-
std = Math.sqrt(variance)
|
|
88
|
-
|
|
89
|
-
bands[:upper] << mean + (std_dev * std)
|
|
90
|
-
bands[:lower] << mean - (std_dev * std)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
bands
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Average True Range
|
|
97
|
-
def self.atr(highs, lows, closes, period = 14)
|
|
98
|
-
return [] if highs.nil? || lows.nil? || closes.nil?
|
|
99
|
-
return [] if [highs.length, lows.length, closes.length].min < 2
|
|
100
|
-
|
|
101
|
-
true_ranges = []
|
|
102
|
-
(1...highs.length).each do |i|
|
|
103
|
-
tr1 = highs[i] - lows[i]
|
|
104
|
-
tr2 = (highs[i] - closes[i - 1]).abs
|
|
105
|
-
tr3 = (lows[i] - closes[i - 1]).abs
|
|
106
|
-
true_ranges << [tr1, tr2, tr3].max
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
return [] if true_ranges.length < period
|
|
110
|
-
|
|
111
|
-
atr_values = []
|
|
112
|
-
atr_values << true_ranges.first(period).sum.to_f / period
|
|
113
|
-
|
|
114
|
-
(period...true_ranges.length).each do |i|
|
|
115
|
-
atr = ((atr_values.last * (period - 1)) + true_ranges[i]) / period
|
|
116
|
-
atr_values << atr
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
atr_values
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Support and Resistance levels
|
|
123
|
-
def self.support_resistance(highs, lows, closes, lookback = 20)
|
|
124
|
-
return { support: [], resistance: [] } if highs.nil? || lows.nil? || closes.nil?
|
|
125
|
-
|
|
126
|
-
support_levels = []
|
|
127
|
-
resistance_levels = []
|
|
128
|
-
|
|
129
|
-
(lookback...highs.length).each do |i|
|
|
130
|
-
window_highs = highs[(i - lookback)..i]
|
|
131
|
-
window_lows = lows[(i - lookback)..i]
|
|
132
|
-
|
|
133
|
-
local_high = window_highs.max
|
|
134
|
-
local_low = window_lows.min
|
|
135
|
-
|
|
136
|
-
# Resistance: price touched high multiple times
|
|
137
|
-
if window_highs.count(local_high) >= 2
|
|
138
|
-
resistance_levels << { price: local_high, strength: window_highs.count(local_high) }
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Support: price touched low multiple times
|
|
142
|
-
if window_lows.count(local_low) >= 2
|
|
143
|
-
support_levels << { price: local_low, strength: window_lows.count(local_low) }
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
{ support: support_levels.uniq { |s| s[:price] }, resistance: resistance_levels.uniq { |r| r[:price] } }
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def self.calculate_rsi_value(avg_gain, avg_loss)
|
|
151
|
-
return 100 if avg_loss.zero?
|
|
152
|
-
|
|
153
|
-
rs = avg_gain / avg_loss
|
|
154
|
-
100 - (100 / (1 + rs))
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../analysis/trend_analyzer"
|
|
4
|
-
require_relative "../services/data_service"
|
|
5
|
-
|
|
6
|
-
module DhanHQ
|
|
7
|
-
module Scanners
|
|
8
|
-
# Scanner for intraday options buying opportunities
|
|
9
|
-
class IntradayOptionsScanner
|
|
10
|
-
def initialize
|
|
11
|
-
@data_service = Services::DataService.new
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def scan_for_options_setups(underlying_symbol, exchange_segment: "IDX_I", min_score: 50, verbose: false)
|
|
15
|
-
puts " 🔍 Analyzing underlying: #{underlying_symbol} (#{exchange_segment})" if verbose
|
|
16
|
-
|
|
17
|
-
# Analyze underlying
|
|
18
|
-
underlying_analysis = analyze_underlying(underlying_symbol, exchange_segment)
|
|
19
|
-
if underlying_analysis[:error]
|
|
20
|
-
return error_result("Failed to analyze underlying: #{underlying_analysis[:error]}", verbose)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
puts " ✅ Underlying analysis complete" if verbose
|
|
24
|
-
|
|
25
|
-
# Get option chain - first get expiry list, then fetch chain for first expiry
|
|
26
|
-
expiry_list_result = get_option_chain(underlying_symbol, exchange_segment)
|
|
27
|
-
if expiry_list_result[:error]
|
|
28
|
-
return error_result("Failed to get expiry list: #{expiry_list_result[:error]}", verbose)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
expiries = extract_expiries(expiry_list_result)
|
|
32
|
-
return error_result("No expiries found in option chain", verbose) unless expiries
|
|
33
|
-
|
|
34
|
-
# Get chain for first expiry (next expiry)
|
|
35
|
-
next_expiry = expiries.first
|
|
36
|
-
puts " ✅ Found #{expiries.length} expiries, fetching chain for: #{next_expiry}" if verbose
|
|
37
|
-
|
|
38
|
-
option_chain = get_option_chain(underlying_symbol, exchange_segment, expiry: next_expiry)
|
|
39
|
-
if option_chain[:error]
|
|
40
|
-
return error_result("Failed to get option chain for expiry #{next_expiry}: #{option_chain[:error]}", verbose)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
puts " ✅ Option chain retrieved for expiry: #{next_expiry}" if verbose
|
|
44
|
-
|
|
45
|
-
# Find best options setups
|
|
46
|
-
setups = find_options_setups(underlying_analysis[:analysis], option_chain, min_score: min_score,
|
|
47
|
-
verbose: verbose)
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
underlying: underlying_symbol,
|
|
51
|
-
underlying_analysis: underlying_analysis[:analysis],
|
|
52
|
-
setups: setups
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def analyze_underlying(symbol, exchange_segment)
|
|
59
|
-
result = @data_service.execute(
|
|
60
|
-
action: "get_historical_data",
|
|
61
|
-
params: {
|
|
62
|
-
"symbol" => symbol,
|
|
63
|
-
"exchange_segment" => exchange_segment,
|
|
64
|
-
"from_date" => (Date.today - 30).strftime("%Y-%m-%d"),
|
|
65
|
-
"to_date" => Date.today.strftime("%Y-%m-%d"),
|
|
66
|
-
"interval" => "5" # 5-minute for intraday
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
return { error: "Failed to fetch data: #{result[:error] || 'Unknown error'}" } if result[:error]
|
|
71
|
-
return { error: "No result data returned" } unless result[:result]
|
|
72
|
-
|
|
73
|
-
ohlc_data = convert_to_ohlc(result)
|
|
74
|
-
return { error: "Failed to convert data to OHLC format" } if ohlc_data.nil? || ohlc_data.empty?
|
|
75
|
-
|
|
76
|
-
analysis = Analysis::TrendAnalyzer.analyze(ohlc_data)
|
|
77
|
-
return { error: "Analysis returned empty result" } if analysis.nil? || analysis.empty?
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
symbol: symbol,
|
|
81
|
-
analysis: analysis
|
|
82
|
-
}
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def get_option_chain(symbol, exchange_segment, expiry: nil)
|
|
86
|
-
@data_service.execute(
|
|
87
|
-
action: "get_option_chain",
|
|
88
|
-
params: {
|
|
89
|
-
"symbol" => symbol,
|
|
90
|
-
"exchange_segment" => exchange_segment,
|
|
91
|
-
"expiry" => expiry
|
|
92
|
-
}
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def find_options_setups(analysis, option_chain, min_score: 50, verbose: false)
|
|
97
|
-
tracking = {
|
|
98
|
-
setups: [],
|
|
99
|
-
rejected: [],
|
|
100
|
-
strikes_evaluated: 0,
|
|
101
|
-
strikes_within_range: 0
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
log_underlying_analysis(analysis, verbose)
|
|
105
|
-
log_option_chain_debug(option_chain, verbose)
|
|
106
|
-
|
|
107
|
-
chain = extract_option_chain(option_chain, verbose)
|
|
108
|
-
return [] unless chain
|
|
109
|
-
|
|
110
|
-
context = build_analysis_context(analysis)
|
|
111
|
-
log_chain_summary(chain, context[:trend], verbose)
|
|
112
|
-
|
|
113
|
-
evaluation_context = {
|
|
114
|
-
tracking: tracking,
|
|
115
|
-
context: context,
|
|
116
|
-
min_score: min_score,
|
|
117
|
-
verbose: verbose
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
chain.each do |strike_key, strike_data|
|
|
121
|
-
strike = strike_key.to_f
|
|
122
|
-
next unless strike_within_range?(strike, context[:current_price])
|
|
123
|
-
|
|
124
|
-
tracking[:strikes_within_range] += 1
|
|
125
|
-
tracking[:strikes_evaluated] += 1
|
|
126
|
-
|
|
127
|
-
evaluate_strike_options(
|
|
128
|
-
strike: strike,
|
|
129
|
-
strike_data: strike_data,
|
|
130
|
-
evaluation_context: evaluation_context
|
|
131
|
-
)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
log_evaluation_summary(tracking, context, min_score, verbose)
|
|
135
|
-
|
|
136
|
-
tracking[:setups].sort_by { |setup| -setup[:score] }.first(5)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def calculate_options_score(option_data, context)
|
|
140
|
-
score = 0
|
|
141
|
-
score += implied_volatility_points(option_data[:implied_volatility])
|
|
142
|
-
score += open_interest_points(option_data[:open_interest])
|
|
143
|
-
score += volume_points(option_data[:volume])
|
|
144
|
-
score += trend_points(option_data[:type], context[:trend], context[:relative_strength_index])
|
|
145
|
-
score += rsi_points(option_data[:type], context[:relative_strength_index])
|
|
146
|
-
score
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def implied_volatility_points(implied_volatility)
|
|
150
|
-
return 0 unless implied_volatility
|
|
151
|
-
|
|
152
|
-
return 30 if implied_volatility < 15
|
|
153
|
-
return 20 if implied_volatility < 25
|
|
154
|
-
return 10 if implied_volatility < 35
|
|
155
|
-
|
|
156
|
-
0
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def open_interest_points(open_interest)
|
|
160
|
-
return 0 unless open_interest
|
|
161
|
-
return 25 if open_interest > 1_000_000
|
|
162
|
-
return 15 if open_interest > 500_000
|
|
163
|
-
return 10 if open_interest > 100_000
|
|
164
|
-
|
|
165
|
-
0
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def volume_points(volume)
|
|
169
|
-
return 0 unless volume
|
|
170
|
-
return 20 if volume > 10_000
|
|
171
|
-
return 10 if volume > 5_000
|
|
172
|
-
|
|
173
|
-
0
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def trend_points(option_type, trend, relative_strength_index)
|
|
177
|
-
return 0 unless trend
|
|
178
|
-
return 15 if trend_alignment?(option_type, trend)
|
|
179
|
-
return 0 unless trend == :sideways
|
|
180
|
-
|
|
181
|
-
rsi_bias_points(option_type, relative_strength_index)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def trend_alignment?(option_type, trend)
|
|
185
|
-
(option_type == :call && trend == :uptrend) || (option_type == :put && trend == :downtrend)
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def rsi_bias_points(option_type, relative_strength_index)
|
|
189
|
-
return 0 unless relative_strength_index
|
|
190
|
-
return 10 if option_type == :call && relative_strength_index > 50
|
|
191
|
-
return 10 if option_type == :put && relative_strength_index < 50
|
|
192
|
-
|
|
193
|
-
0
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def rsi_points(option_type, relative_strength_index)
|
|
197
|
-
return 0 unless relative_strength_index
|
|
198
|
-
return 10 if option_type == :call && relative_strength_index.between?(50, 70)
|
|
199
|
-
return 10 if option_type == :put && relative_strength_index.between?(30, 50)
|
|
200
|
-
|
|
201
|
-
0
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def error_result(message, verbose)
|
|
205
|
-
puts " ⚠️ #{message}" if verbose
|
|
206
|
-
{ error: message }
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def extract_expiries(expiry_list_result)
|
|
210
|
-
expiry_list = expiry_list_result[:result] || expiry_list_result["result"]
|
|
211
|
-
return nil unless expiry_list.is_a?(Hash)
|
|
212
|
-
|
|
213
|
-
expiries = expiry_list[:expiries] || expiry_list["expiries"]
|
|
214
|
-
return nil unless expiries.is_a?(Array) && expiries.any?
|
|
215
|
-
|
|
216
|
-
expiries
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def log_underlying_analysis(analysis, verbose)
|
|
220
|
-
return unless verbose
|
|
221
|
-
|
|
222
|
-
puts " 📊 Underlying Analysis:"
|
|
223
|
-
return puts(" ⚠️ Analysis data not available") unless analysis && !analysis.empty?
|
|
224
|
-
|
|
225
|
-
current_price = analysis[:current_price]
|
|
226
|
-
trend = analysis[:trend]&.dig(:trend)
|
|
227
|
-
relative_strength_index = analysis[:indicators]&.dig(:rsi)
|
|
228
|
-
puts " Current Price: #{current_price || 'N/A'}"
|
|
229
|
-
puts " Trend: #{trend || 'N/A'}"
|
|
230
|
-
puts " RSI: #{relative_strength_index&.round(2) || 'N/A'}"
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def log_option_chain_debug(option_chain, verbose)
|
|
234
|
-
return unless verbose
|
|
235
|
-
|
|
236
|
-
puts " 🔍 Option chain structure:"
|
|
237
|
-
return unless option_chain.is_a?(Hash)
|
|
238
|
-
|
|
239
|
-
puts " Keys: #{option_chain.keys.inspect}"
|
|
240
|
-
puts " Has result?: #{option_chain.key?(:result) || option_chain.key?('result')}"
|
|
241
|
-
result = option_chain[:result] || option_chain["result"]
|
|
242
|
-
return unless result.is_a?(Hash)
|
|
243
|
-
|
|
244
|
-
puts " Result keys: #{result.keys.inspect}"
|
|
245
|
-
puts " Has chain?: #{result.key?(:chain) || result.key?('chain')}"
|
|
246
|
-
puts " Has expiries?: #{result.key?(:expiries) || result.key?('expiries')}"
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def extract_option_chain(option_chain, verbose)
|
|
250
|
-
result = option_chain[:result] || option_chain["result"]
|
|
251
|
-
unless result
|
|
252
|
-
puts " ⚠️ Option chain data not available or invalid (no result)" if verbose
|
|
253
|
-
return nil
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
chain = result[:chain] || result["chain"]
|
|
257
|
-
return chain if chain
|
|
258
|
-
|
|
259
|
-
return nil unless verbose
|
|
260
|
-
|
|
261
|
-
puts " ⚠️ Option chain data not available or invalid (no chain in result)"
|
|
262
|
-
puts " Available keys in result: #{result.keys.inspect}" if result.is_a?(Hash)
|
|
263
|
-
puts " Result structure: #{result.inspect[0..200]}"
|
|
264
|
-
nil
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def build_analysis_context(analysis)
|
|
268
|
-
return { current_price: nil, trend: nil, relative_strength_index: nil } unless analysis
|
|
269
|
-
|
|
270
|
-
{
|
|
271
|
-
current_price: analysis[:current_price],
|
|
272
|
-
trend: analysis[:trend]&.dig(:trend),
|
|
273
|
-
relative_strength_index: analysis[:indicators]&.dig(:rsi)
|
|
274
|
-
}
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def log_chain_summary(chain, trend, verbose)
|
|
278
|
-
return unless verbose
|
|
279
|
-
|
|
280
|
-
puts " Chain strikes: #{chain.keys.length}"
|
|
281
|
-
puts " Looking for: #{preferred_option_label(trend)}"
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def preferred_option_label(trend)
|
|
285
|
-
return "CALL options" if trend == :uptrend
|
|
286
|
-
return "PUT options" if trend == :downtrend
|
|
287
|
-
|
|
288
|
-
"CALL or PUT (sideways trend)"
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
def strike_within_range?(strike, current_price)
|
|
292
|
-
return false unless current_price
|
|
293
|
-
|
|
294
|
-
(strike - current_price).abs / current_price < 0.02
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def evaluate_strike_options(strike:, strike_data:, evaluation_context:)
|
|
298
|
-
context = evaluation_context[:context]
|
|
299
|
-
|
|
300
|
-
call_data = strike_data["ce"] || strike_data[:ce]
|
|
301
|
-
put_data = strike_data["pe"] || strike_data[:pe]
|
|
302
|
-
|
|
303
|
-
if call_data && option_allowed?(:call, context[:trend])
|
|
304
|
-
evaluate_option_setup(
|
|
305
|
-
option_type: :call,
|
|
306
|
-
strike: strike,
|
|
307
|
-
raw_data: call_data,
|
|
308
|
-
evaluation_context: evaluation_context
|
|
309
|
-
)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
return unless put_data && option_allowed?(:put, context[:trend])
|
|
313
|
-
|
|
314
|
-
evaluate_option_setup(
|
|
315
|
-
option_type: :put,
|
|
316
|
-
strike: strike,
|
|
317
|
-
raw_data: put_data,
|
|
318
|
-
evaluation_context: evaluation_context
|
|
319
|
-
)
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def option_allowed?(option_type, trend)
|
|
323
|
-
return %i[uptrend sideways].include?(trend) if option_type == :call
|
|
324
|
-
return %i[downtrend sideways].include?(trend) if option_type == :put
|
|
325
|
-
|
|
326
|
-
false
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
def evaluate_option_setup(option_type:, strike:, raw_data:, evaluation_context:)
|
|
330
|
-
tracking = evaluation_context[:tracking]
|
|
331
|
-
context = evaluation_context[:context]
|
|
332
|
-
min_score = evaluation_context[:min_score]
|
|
333
|
-
verbose = evaluation_context[:verbose]
|
|
334
|
-
|
|
335
|
-
option_data = option_data_for(option_type, raw_data)
|
|
336
|
-
score = calculate_options_score(option_data, context)
|
|
337
|
-
|
|
338
|
-
if score >= min_score
|
|
339
|
-
tracking[:setups] << build_setup(option_data, strike, score)
|
|
340
|
-
return
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
return unless verbose
|
|
344
|
-
|
|
345
|
-
tracking[:rejected] << {
|
|
346
|
-
type: option_type,
|
|
347
|
-
strike: strike,
|
|
348
|
-
score: score,
|
|
349
|
-
reason: "Below min_score (#{min_score})"
|
|
350
|
-
}
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def option_data_for(option_type, raw_data)
|
|
354
|
-
{
|
|
355
|
-
type: option_type,
|
|
356
|
-
implied_volatility: raw_data["implied_volatility"] || raw_data[:implied_volatility],
|
|
357
|
-
open_interest: raw_data["oi"] || raw_data[:oi],
|
|
358
|
-
volume: raw_data["volume"] || raw_data[:volume],
|
|
359
|
-
last_price: raw_data["last_price"] || raw_data[:last_price] || raw_data["ltp"] || raw_data[:ltp]
|
|
360
|
-
}
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
def build_setup(option_data, strike, score)
|
|
364
|
-
{
|
|
365
|
-
type: option_data[:type],
|
|
366
|
-
strike: strike,
|
|
367
|
-
iv: option_data[:implied_volatility],
|
|
368
|
-
oi: option_data[:open_interest],
|
|
369
|
-
volume: option_data[:volume],
|
|
370
|
-
ltp: option_data[:last_price],
|
|
371
|
-
score: score,
|
|
372
|
-
recommendation: recommendation_for_score(score)
|
|
373
|
-
}
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def recommendation_for_score(score)
|
|
377
|
-
return "Strong buy" if score > 70
|
|
378
|
-
return "Moderate buy" if score > 50
|
|
379
|
-
|
|
380
|
-
"Weak"
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
def log_evaluation_summary(tracking, context, min_score, verbose)
|
|
384
|
-
return unless verbose
|
|
385
|
-
|
|
386
|
-
puts " 📊 Evaluation Summary:"
|
|
387
|
-
puts " Strikes within 2% of price: #{tracking[:strikes_within_range]}"
|
|
388
|
-
puts " Strikes evaluated: #{tracking[:strikes_evaluated]}"
|
|
389
|
-
puts " Setups found: #{tracking[:setups].length}"
|
|
390
|
-
|
|
391
|
-
if tracking[:rejected].any?
|
|
392
|
-
log_rejected_setups(tracking[:rejected], min_score)
|
|
393
|
-
elsif tracking[:strikes_evaluated].zero?
|
|
394
|
-
log_no_strike_message(tracking, context)
|
|
395
|
-
end
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def log_rejected_setups(rejected, min_score)
|
|
399
|
-
puts " 📋 Rejected setups: #{rejected.length} (below min_score #{min_score})"
|
|
400
|
-
rejected.first(5).each do |rejection|
|
|
401
|
-
puts " ❌ #{rejection[:type].to_s.upcase} @ #{rejection[:strike]}: Score #{rejection[:score]}/100"
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def log_no_strike_message(tracking, context)
|
|
406
|
-
trend = context[:trend]
|
|
407
|
-
current_price = context[:current_price]
|
|
408
|
-
|
|
409
|
-
if !trend || trend == :sideways
|
|
410
|
-
puts " ⚠️ Sideways trend - no clear directional bias for calls/puts"
|
|
411
|
-
elsif trend == :uptrend && tracking[:strikes_within_range].zero?
|
|
412
|
-
puts " ⚠️ No CALL strikes found within 2% of current price (#{current_price})"
|
|
413
|
-
elsif trend == :downtrend && tracking[:strikes_within_range].zero?
|
|
414
|
-
puts " ⚠️ No PUT strikes found within 2% of current price (#{current_price})"
|
|
415
|
-
else
|
|
416
|
-
puts " ⚠️ No suitable strikes found for current trend (#{trend || 'unknown'})"
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def convert_to_ohlc(historical_data)
|
|
421
|
-
return [] unless historical_data.is_a?(Hash)
|
|
422
|
-
|
|
423
|
-
data = extract_data_payload(historical_data)
|
|
424
|
-
return [] unless data
|
|
425
|
-
|
|
426
|
-
return ohlc_from_hash(data) if data.is_a?(Hash)
|
|
427
|
-
return ohlc_from_array(data) if data.is_a?(Array)
|
|
428
|
-
|
|
429
|
-
[]
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
def extract_data_payload(historical_data)
|
|
433
|
-
outer_result = historical_data[:result] || historical_data["result"]
|
|
434
|
-
return nil unless outer_result.is_a?(Hash)
|
|
435
|
-
|
|
436
|
-
outer_result[:data] || outer_result["data"]
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def ohlc_from_hash(data)
|
|
440
|
-
series = extract_series(data)
|
|
441
|
-
return [] if series[:closes].nil? || series[:closes].empty?
|
|
442
|
-
|
|
443
|
-
max_length = series_lengths(series).max
|
|
444
|
-
return [] if max_length.zero?
|
|
445
|
-
|
|
446
|
-
build_ohlc_rows(series, max_length)
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
def extract_series(data)
|
|
450
|
-
{
|
|
451
|
-
opens: data[:open] || data["open"] || [],
|
|
452
|
-
highs: data[:high] || data["high"] || [],
|
|
453
|
-
lows: data[:low] || data["low"] || [],
|
|
454
|
-
closes: data[:close] || data["close"] || [],
|
|
455
|
-
volumes: data[:volume] || data["volume"] || []
|
|
456
|
-
}
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def series_lengths(series)
|
|
460
|
-
[series[:opens].length, series[:highs].length, series[:lows].length, series[:closes].length]
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
def build_ohlc_rows(series, max_length)
|
|
464
|
-
(0...max_length).map do |index|
|
|
465
|
-
{
|
|
466
|
-
open: series[:opens][index] || series[:closes][index] || 0,
|
|
467
|
-
high: series[:highs][index] || series[:closes][index] || 0,
|
|
468
|
-
low: series[:lows][index] || series[:closes][index] || 0,
|
|
469
|
-
close: series[:closes][index] || 0,
|
|
470
|
-
volume: series[:volumes][index] || 0
|
|
471
|
-
}
|
|
472
|
-
end
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
def ohlc_from_array(data)
|
|
476
|
-
data.filter_map { |bar| normalize_bar(bar) }
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
def normalize_bar(bar)
|
|
480
|
-
return nil unless bar.is_a?(Hash)
|
|
481
|
-
|
|
482
|
-
{
|
|
483
|
-
open: bar["open"] || bar[:open],
|
|
484
|
-
high: bar["high"] || bar[:high],
|
|
485
|
-
low: bar["low"] || bar[:low],
|
|
486
|
-
close: bar["close"] || bar[:close],
|
|
487
|
-
volume: bar["volume"] || bar[:volume]
|
|
488
|
-
}
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
end
|
|
492
|
-
end
|