DhanHQ 2.1.0 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f37f6e3a5253ced5b53845fe8cdb43d7f13cbbfae8699d3ffa29737a0ac8386
4
- data.tar.gz: 9035ad6b0d08a5d83b342341c6d799e06e860c1f66eabeea16cdd4dacdd5ed9c
3
+ metadata.gz: 0ee56ccabd71b9f769e99d6470cf32309c4f6fa17eec5de17417b1c153ee9293
4
+ data.tar.gz: 000fcabd622e0e690f6c65bf03665b8e4b3dfc23572534d29101fca0576757be
5
5
  SHA512:
6
- metadata.gz: b72eef88a4c4e6e1a41a09ffc3da297b6cf701d9d7c79b63b7eec7c76a19355134c3daa3d5d64b5eb0cfb537a61529bd7906230a8b6c4f97176404f0afc0e586
7
- data.tar.gz: 32dc61f28fed01d6cb1caced8e343c29b10e482ed3b8673e32131a59c47c794a7ad98bcd889057bd1a5b4a6ef4ca403c7ed5e2763853e91a09a3d50ae553cae5
6
+ metadata.gz: 6aeecd46b742a5cbdf74ffb4e38086be502385fda9e18bee4b6413a488b005b2a713dd089cd2d83c6628096d084d063bb6da468292b785d08f968fc9336838ac
7
+ data.tar.gz: b4fe91bca2051073b297693d1ea2789911b8b50b3a948ccf96cc7b0647a1fe824020f39ca0cbfebf53946b453253d8348df7d673eb199068d24d444453191b8d
data/README.md CHANGED
@@ -44,9 +44,9 @@ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Lo
44
44
 
45
45
  **Minimum environment variables**
46
46
 
47
- | Variable | Purpose |
48
- | --- | --- |
49
- | `CLIENT_ID` | Trading account client id issued by Dhan. |
47
+ | Variable | Purpose |
48
+ | -------------- | ------------------------------------------------- |
49
+ | `CLIENT_ID` | Trading account client id issued by Dhan. |
50
50
  | `ACCESS_TOKEN` | API access token generated from the Dhan console. |
51
51
 
52
52
  `configure_with_env` raises if either value is missing. Load them via `dotenv`,
@@ -58,14 +58,14 @@ initialisation.
58
58
  Set these variables _before_ calling `configure_with_env` when you need to
59
59
  override defaults supplied by the gem:
60
60
 
61
- | Variable | When to use |
62
- | --- | --- |
63
- | `DHAN_LOG_LEVEL` | Adjust logger verbosity (`INFO` by default). |
64
- | `DHAN_BASE_URL` | Point REST calls to a different API hostname. |
65
- | `DHAN_WS_VERSION` | Pin to a specific WebSocket API version. |
66
- | `DHAN_WS_ORDER_URL` | Override the order update WebSocket endpoint. |
67
- | `DHAN_WS_USER_TYPE` | Switch between `SELF` and `PARTNER` streaming modes. |
68
- | `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when `DHAN_WS_USER_TYPE=PARTNER`. |
61
+ | Variable | When to use |
62
+ | ----------------------------------------- | ---------------------------------------------------- |
63
+ | `DHAN_LOG_LEVEL` | Adjust logger verbosity (`INFO` by default). |
64
+ | `DHAN_BASE_URL` | Point REST calls to a different API hostname. |
65
+ | `DHAN_WS_VERSION` | Pin to a specific WebSocket API version. |
66
+ | `DHAN_WS_ORDER_URL` | Override the order update WebSocket endpoint. |
67
+ | `DHAN_WS_USER_TYPE` | Switch between `SELF` and `PARTNER` streaming modes. |
68
+ | `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when `DHAN_WS_USER_TYPE=PARTNER`. |
69
69
 
70
70
  ### Logging
71
71
 
@@ -461,3 +461,9 @@ PRs welcome! Please include tests for new packet decoders and WS behaviors (chun
461
461
  ## License
462
462
 
463
463
  MIT.
464
+
465
+ ## Technical Analysis (Indicators + Multi-Timeframe)
466
+
467
+ See the guide for computing indicators and aggregating cross-timeframe bias:
468
+
469
+ - docs/technical_analysis.md
@@ -0,0 +1,143 @@
1
+ # Technical Analysis Guide
2
+
3
+ This guide explains how to use the technical analysis modules bundled with this gem: fetching historical OHLC, computing indicators, and producing multi-timeframe summaries.
4
+
5
+ ## Modules Overview
6
+
7
+ - `TA::TechnicalAnalysis`: Fetches intraday OHLC (1/5/15/25/60) from Dhan APIs with throttling/backoff, computes RSI/MACD/ADX/ATR, and returns a structured indicators hash.
8
+ - `TA::Indicators`: Adapters for `ruby-technical-analysis` and `technical-analysis` gems, including safe fallbacks.
9
+ - `TA::Candles`: Utilities for converting API series to candles and resampling (used for offline data only).
10
+ - `TA::Fetcher`: Handles API calls, 90-day windowing, throttling, and retries.
11
+ - `DhanHQ::Analysis::MultiTimeframeAnalyzer`: Consumes the indicator hash and outputs a consolidated bias summary across timeframes.
12
+
13
+ ## Prerequisites
14
+
15
+ - Environment variables set: `CLIENT_ID`, `ACCESS_TOKEN`
16
+ - Optional indicator gems:
17
+ - `gem install ruby-technical-analysis technical-analysis`
18
+
19
+ ## Quick Start: Compute Indicators
20
+
21
+ ```ruby
22
+ require "ta"
23
+
24
+ DhanHQ.configure_with_env
25
+
26
+ ta = TA::TechnicalAnalysis.new(throttle_seconds: 2.5, max_retries: 3)
27
+ indicators = ta.compute(
28
+ exchange_segment: "NSE_EQ",
29
+ instrument: "EQUITY",
30
+ security_id: "1333",
31
+ intervals: [1, 5, 15, 25, 60] # each fetched directly from API
32
+ )
33
+ ```
34
+
35
+ Output structure:
36
+
37
+ ```ruby
38
+ {
39
+ meta: { exchange_segment: "...", instrument: "...", security_id: "...", from_date: "YYYY-MM-DD", to_date: "YYYY-MM-DD" },
40
+ indicators: {
41
+ m1: { rsi: Float|nil, adx: Float|nil, atr: Float|nil, macd: { macd: Float|nil, signal: Float|nil, hist: Float|nil } },
42
+ m5: { ... },
43
+ m15: { ... },
44
+ m25: { ... },
45
+ m60: { ... }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Notes:
51
+ - `to_date` defaults to today-or-last-trading-day via `TA::MarketCalendar`.
52
+ - If `days_back` is not provided, the class auto-selects a sufficient lookback (trading days) per the selected intervals and indicator periods (max of [2×ADX, MACD slow, RSI+1, ATR+1]).
53
+ - Requests are throttled with jitter; rate-limit errors trigger exponential backoff.
54
+
55
+ ## Offline Input (JSON OHLC)
56
+
57
+ ```ruby
58
+ raw = JSON.parse(File.read("ohlc.json"))
59
+ indicators = TA::TechnicalAnalysis.new.compute_from_file(
60
+ path: "ohlc.json", base_interval: 1, intervals: [1,5,15,25,60]
61
+ )
62
+ ```
63
+
64
+ ## Analyze Multi-Timeframe Bias
65
+
66
+ ```ruby
67
+ require "DhanHQ"
68
+
69
+ analyzer = DhanHQ::Analysis::MultiTimeframeAnalyzer.new(data: indicators)
70
+ summary = analyzer.call
71
+ ```
72
+
73
+ Example summary:
74
+
75
+ ```ruby
76
+ {
77
+ meta: { security_id: "1333", instrument: "EQUITY", exchange_segment: "NSE_EQ" },
78
+ summary: {
79
+ bias: :bullish, # :bullish | :bearish | :neutral
80
+ setup: :buy_on_dip, # :buy_on_dip | :sell_on_rise | :range_trade
81
+ confidence: 0.78, # 0.0..1.0 weighted across timeframes
82
+ rationale: {
83
+ rsi: "Upward momentum across M5–M60",
84
+ macd: "MACD bullish signals dominant",
85
+ adx: "Strong higher timeframe trend",
86
+ atr: "Volatility expansion"
87
+ },
88
+ trend_strength: {
89
+ short_term: :weak_bullish,
90
+ medium_term: :neutral_to_bullish,
91
+ long_term: :strong_bullish
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ## CLI Script
98
+
99
+ A convenience script exists at `bin/ta_strategy.rb` to compute indicators and print JSON. Example:
100
+
101
+ ```bash
102
+ ./bin/ta_strategy.rb --segment NSE_EQ --instrument EQUITY --security-id 1333 \
103
+ --from 2025-10-06 --to 2025-10-07 --debug
104
+ ```
105
+
106
+ Options:
107
+ - `--print-creds` to verify env
108
+ - `--data-file` to compute from JSON instead of API
109
+ - `--interval` (with `--data-file`) to specify base file interval
110
+ - `--rsi`, `--atr`, `--adx`, `--macd` to tune periods
111
+
112
+ ## Options Buying Advisor CLI
113
+
114
+ Use `bin/options_advisor.rb` to compute indicators, summarize multi-TF bias, and produce a single index options-buying recommendation (CE/PE). If you do not provide `--spot`, the script fetches spot via MarketFeed LTP automatically.
115
+
116
+ Examples:
117
+
118
+ ```bash
119
+ # Auto-fetch spot via MarketFeed.ltp and chain via OptionChain model
120
+ ./bin/options_advisor.rb --segment IDX_I --instrument INDEX --security-id 13 --symbol NIFTY
121
+
122
+ # Provide pre-fetched option chain from file (JSON array of strikes)
123
+ ./bin/options_advisor.rb --segment IDX_I --instrument INDEX --security-id 13 --symbol NIFTY \
124
+ --chain-file ./chain.json
125
+
126
+ # Override spot explicitly
127
+ ./bin/options_advisor.rb --segment IDX_I --instrument INDEX --security-id 13 --symbol NIFTY --spot 24890
128
+
129
+ # Verbose debug logging (prints steps to STDERR; JSON output unchanged)
130
+ ./bin/options_advisor.rb --segment IDX_I --instrument INDEX --security-id 13 --symbol NIFTY --debug
131
+ ```
132
+
133
+ Behavior:
134
+ - Spot: when `--spot` is omitted, the script calls `DhanHQ::Models::MarketFeed.ltp({ SEG => [security_id] })` and reads `data[SEG][security_id]["last_price"]`.
135
+ - Option chain: when `--chain-file` is omitted, the advisor fetches via `DhanHQ::Models::OptionChain` (nearest expiry), transforming OC into an internal array of strikes with CE/PE legs.
136
+ - Debugging: with `--debug`, the script logs options, spot resolution, indicators meta, missing fields per timeframe, analyzer summary, and advisor output to STDERR.
137
+
138
+ ## Best Practices & Tips
139
+
140
+ - Keep intervals minimal per run to reduce rate limits; increase `throttle_seconds` if needed.
141
+ - For higher intervals (e.g., 60m), ensure adequate `days_back` (auto-calculation is enabled by default).
142
+ - The analyzer is heuristic; adjust weights or thresholds as your strategy matures.
143
+ - See the advisor helpers under `lib/dhanhq/analysis/helpers/` to customize bias and moneyness rules.
@@ -131,13 +131,11 @@ module DhanHQ
131
131
  # Download URL for the detailed instrument master CSV.
132
132
  DETAILED_CSV_URL = "https://images.dhan.co/api-data/api-scrip-master-detailed.csv"
133
133
 
134
- # API routes that require a `client-id` header in addition to the access token.
135
- DATA_API_PATHS = %w[
136
- /v2/marketfeed/ltp
137
- /v2/marketfeed/ohlc
138
- /v2/marketfeed/quote
134
+ # API route prefixes that require a `client-id` header in addition to the access token.
135
+ DATA_API_PREFIXES = %w[
136
+ /v2/marketfeed/
139
137
  /v2/optionchain
140
- /v2/optionchain/expirylist
138
+ /v2/instrument/
141
139
  ].freeze
142
140
 
143
141
  # Mapping of DhanHQ error codes to SDK error classes for consistent exception handling.
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validates the exchange segment param for instrument list endpoint
6
+ class InstrumentListContract < BaseContract
7
+ params do
8
+ required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -26,6 +26,9 @@ module DhanHQ
26
26
  # @param path [String] The API endpoint path.
27
27
  # @return [Hash] The request headers.
28
28
  def build_headers(path)
29
+ # Public CSV endpoint for segment-wise instruments requires no auth
30
+ return { "Accept" => "text/csv" } if path.start_with?("/v2/instrument/")
31
+
29
32
  headers = {
30
33
  "Content-Type" => "application/json",
31
34
  "Accept" => "application/json",
@@ -43,7 +46,8 @@ module DhanHQ
43
46
  # @param path [String] The API endpoint path.
44
47
  # @return [Boolean] True if the path belongs to a DATA API.
45
48
  def data_api?(path)
46
- DhanHQ::Constants::DATA_API_PATHS.include?(path)
49
+ prefixes = DhanHQ::Constants::DATA_API_PREFIXES
50
+ prefixes.any? { |p| path.start_with?(p) }
47
51
  end
48
52
 
49
53
  # Prepares the request payload based on the HTTP method.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../contracts/instrument_list_contract"
4
+
5
+ module DhanHQ
6
+ module Models
7
+ # Model wrapper for fetching instruments by exchange segment.
8
+ class Instrument < BaseModel
9
+ attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument, :series,
10
+ :lot_size, :tick_size, :expiry_date, :strike_price, :option_type
11
+
12
+ class << self
13
+ # @return [DhanHQ::Resources::Instruments]
14
+ def resource
15
+ @resource ||= DhanHQ::Resources::Instruments.new
16
+ end
17
+
18
+ # Retrieve instruments for a given segment, returning an array of models.
19
+ # @param exchange_segment [String]
20
+ # @return [Array<Instrument>]
21
+ def by_segment(exchange_segment)
22
+ validate_params!({ exchange_segment: exchange_segment }, DhanHQ::Contracts::InstrumentListContract)
23
+
24
+ csv_text = resource.by_segment(exchange_segment)
25
+ return [] unless csv_text.is_a?(String) && !csv_text.empty?
26
+
27
+ require "csv"
28
+ rows = CSV.parse(csv_text, headers: true)
29
+ rows.map { |r| new(normalize_csv_row(r), skip_validation: true) }
30
+ end
31
+
32
+ def normalize_csv_row(row)
33
+ {
34
+ security_id: row["SECURITY_ID"].to_s,
35
+ symbol_name: row["SYMBOL_NAME"],
36
+ display_name: row["DISPLAY_NAME"],
37
+ exchange_segment: row["EXCH_ID"],
38
+ instrument: row["INSTRUMENT"],
39
+ series: row["SERIES"],
40
+ lot_size: row["LOT_SIZE"]&.to_f,
41
+ tick_size: row["TICK_SIZE"]&.to_f,
42
+ expiry_date: row["SM_EXPIRY_DATE"],
43
+ strike_price: row["STRIKE_PRICE"]&.to_f,
44
+ option_type: row["OPTION_TYPE"]
45
+ }
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def validation_contract
52
+ nil
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Resources
5
+ # Resource client for fetching segment-wise instrument lists.
6
+ class Instruments < BaseAPI
7
+ # Instruments are served via the non-trading/data tier
8
+ API_TYPE = :data_api
9
+ # Base path for instruments endpoint
10
+ HTTP_PATH = "/v2/instrument"
11
+
12
+ # Fetch instruments for a given exchange segment.
13
+ # Returns CSV text; the client parses to Array<Hash> upstream if needed.
14
+ #
15
+ # @param exchange_segment [String] e.g. "NSE_EQ", "NSE_FNO", "IDX_I"
16
+ # @return [String] CSV content
17
+ def by_segment(exchange_segment)
18
+ path = "#{HTTP_PATH}/#{exchange_segment}"
19
+ resp = client.connection.get(path)
20
+ if resp.status.between?(300, 399) && resp.headers["location"]
21
+ redirect_url = resp.headers["location"]
22
+ return Faraday.get(redirect_url).body
23
+ end
24
+ resp.body
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.0"
5
+ VERSION = "2.1.3"
6
6
  end
data/lib/DhanHQ.rb CHANGED
@@ -44,6 +44,7 @@ require_relative "DhanHQ/resources/trades"
44
44
  require_relative "DhanHQ/resources/historical_data"
45
45
  require_relative "DhanHQ/resources/margin_calculator"
46
46
  require_relative "DhanHQ/resources/market_feed"
47
+ require_relative "DhanHQ/resources/instruments"
47
48
  require_relative "DhanHQ/resources/edis"
48
49
  require_relative "DhanHQ/resources/kill_switch"
49
50
  require_relative "DhanHQ/resources/profile"
@@ -56,6 +57,7 @@ require_relative "DhanHQ/models/forever_order"
56
57
  require_relative "DhanHQ/models/super_order"
57
58
  require_relative "DhanHQ/models/historical_data"
58
59
  require_relative "DhanHQ/models/market_feed"
60
+ require_relative "DhanHQ/models/instrument"
59
61
  require_relative "DhanHQ/models/position"
60
62
  require_relative "DhanHQ/models/holding"
61
63
  require_relative "DhanHQ/models/ledger_entry"
@@ -68,6 +70,12 @@ require_relative "DhanHQ/models/profile"
68
70
  require_relative "DhanHQ/constants"
69
71
  require_relative "DhanHQ/ws"
70
72
  require_relative "DhanHQ/ws/singleton_lock"
73
+ require_relative "ta"
74
+ require_relative "dhanhq/analysis/multi_timeframe_analyzer"
75
+ require_relative "dhanhq/analysis/helpers/bias_aggregator"
76
+ require_relative "dhanhq/analysis/helpers/moneyness_helper"
77
+ require_relative "dhanhq/contracts/options_buying_advisor_contract"
78
+ require_relative "dhanhq/analysis/options_buying_advisor"
71
79
 
72
80
  # The top-level module for the DhanHQ client library.
73
81
  #
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Analysis
5
+ class BiasAggregator
6
+ DEFAULT_WEIGHTS = { m1: 0.1, m5: 0.2, m15: 0.25, m25: 0.15, m60: 0.3 }.freeze
7
+
8
+ def initialize(indicators, config = {})
9
+ @indicators = indicators || {}
10
+ @weights = config[:timeframe_weights] || DEFAULT_WEIGHTS
11
+ @min_adx = (config[:min_adx_for_trend] || 22).to_f
12
+ @strong_adx = (config[:strong_adx] || 35).to_f
13
+ end
14
+
15
+ def call
16
+ score = 0.0
17
+ wsum = 0.0
18
+ refs = []
19
+ notes = []
20
+
21
+ @weights.each do |tf, w|
22
+ next unless @indicators[tf]
23
+
24
+ s = score_tf(@indicators[tf])
25
+ next if s.nil?
26
+
27
+ score += s * w
28
+ wsum += w
29
+ refs << tf
30
+ end
31
+
32
+ avg = wsum.zero? ? 0.5 : (score / wsum)
33
+ bias = if avg > 0.55
34
+ :bullish
35
+ elsif avg < 0.45
36
+ :bearish
37
+ else
38
+ :neutral
39
+ end
40
+
41
+ { bias: bias, confidence: avg.round(2), refs: refs, notes: notes }
42
+ end
43
+
44
+ private
45
+
46
+ def score_tf(val)
47
+ rsi = val[:rsi]
48
+ macd = val[:macd] || {}
49
+ hist = macd[:hist]
50
+ adx = val[:adx]
51
+
52
+ rsi_component = case rsi
53
+ when nil then 0.5
54
+ else
55
+ return 0.65 if rsi >= 55
56
+ return 0.35 if rsi <= 45
57
+
58
+ 0.5
59
+ end
60
+
61
+ macd_component = case hist
62
+ when nil then 0.5
63
+ else
64
+ hist >= 0 ? 0.6 : 0.4
65
+ end
66
+
67
+ adx_component = case adx
68
+ when nil then 0.5
69
+ else
70
+ if adx >= @strong_adx
71
+ 0.65
72
+ elsif adx >= @min_adx
73
+ 0.55
74
+ else
75
+ 0.45
76
+ end
77
+ end
78
+
79
+ (rsi_component * 0.4) + (macd_component * 0.3) + (adx_component * 0.3)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Analysis
5
+ module MoneynessHelper
6
+ module_function
7
+
8
+ def pick_moneyness(indicators:, min_adx:, strong_adx:, bias: nil)
9
+ # Mark bias as intentionally observed for future rules
10
+ bias&.to_sym
11
+
12
+ m60 = indicators[:m60] || {}
13
+ adx = m60[:adx].to_f
14
+ rsi = m60[:rsi].to_f
15
+
16
+ return :atm if adx < min_adx
17
+ return :otm if adx >= strong_adx && rsi >= 60
18
+
19
+ :atm
20
+ end
21
+ end
22
+ end
23
+ end