ollama-client 0.2.5 → 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 +13 -0
- data/README.md +138 -76
- 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/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -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 +41 -35
- data/lib/ollama/config.rb +4 -1
- data/lib/ollama/document_loader.rb +1 -1
- data/lib/ollama/embeddings.rb +41 -26
- 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 +7 -0
- metadata +14 -48
- 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,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
|