quantitative 0.2.0 → 0.2.1

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: e3db79bcbb841511c94568e330f60dfb6b68178ae874c56b867c17c538510a01
4
- data.tar.gz: c5da8745035d84023886dcc8fba53f0df473c404b979e084385f6d9a7e19d536
3
+ metadata.gz: bf0aa73684247efc7bc9750c27c856a094fa362d99c1eba73bd70457da8f3a97
4
+ data.tar.gz: 56dcaa82230328cb44149ceb957420484217e33c6f601ea135997391bfb95440
5
5
  SHA512:
6
- metadata.gz: 23c01e976533a62eddd80d5a774c55d23c644f6b9902392277f8a3450849f46a8dd8ead5588d8cc798cbaa36cce6c7ca7404ed332d4b41d6707982fb8e85c7bc
7
- data.tar.gz: 769c9951b5af3cef2f9a4b93c4a49bd13f33876daa18ba90cf78a00503bfea1e87d8fa4ece964e72f0e15dbc712ef857bf3fa102d438e982d308e00a4c376b9d
6
+ metadata.gz: b674a5e4408a69039cf2c7ad2378d065e3f88b409a7530108af1bfc5a186a02690cba8ffb1c8d40a87ad69368f302d9eec0211135beec7db6a9c51c4e1be46d3
7
+ data.tar.gz: e7d7693cd878ebe777f81ca0d90b4f305e645c8547aa9f420cccb4a4f9b53920211a2b7f49af146508756e6e38f5cc7e0c4b57f1c8abdd722f65bd3409abd02f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quantitative (0.2.0)
4
+ quantitative (0.2.1)
5
5
  oj (~> 3.10)
6
6
 
7
7
  GEM
data/lib/quant/errors.rb CHANGED
@@ -10,6 +10,10 @@ module Quant
10
10
  # {Quant::Interval} with an invalid value.
11
11
  class InvalidInterval < Error; end
12
12
 
13
+ # {InvalidIndicatorSource} is raised when attempting to reference
14
+ # an indicator through a source that has not been prepared, yet.
15
+ class InvalidIndicatorSource < Error; end
16
+
13
17
  # {InvalidResolution} is raised when attempting to instantiate
14
18
  # an {Quant::Resolution} with a resolution value that has not been defined.
15
19
  class InvalidResolution < Error; end
@@ -1,10 +1,49 @@
1
1
  module Quant
2
+ # Dominant Cycles measure the primary cycle within a given range. By default, the library
3
+ # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
4
+ # the `min_period` and `max_period` configuration values in {Quant::Config}.
5
+ #
6
+ # Quant.configure_indicators(min_period: 8, max_period: 32)
7
+ #
8
+ # The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
9
+ # the `dominant_cycle_kind` configuration value in {Quant::Config}.
10
+ #
11
+ # Quant.configure_indicators(dominant_cycle_kind: :band_pass)
12
+ #
13
+ # The purpose of these indicators is to compute the dominant cycle and underpin the various
14
+ # indicators that would otherwise be setting an arbitrary lookback period. This makes the
15
+ # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
2
16
  class DominantCycleIndicators < IndicatorsProxy
17
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
18
+ # by correlating the data stream with itself delayed by a lag.
3
19
  def acr; indicator(Indicators::DominantCycles::Acr) end
20
+
21
+ # The band-pass dominant cycle passes signals within a certain frequency
22
+ # range, and attenuates signals outside that range.
23
+ # The trend component of the signal is removed, leaving only the cyclical
24
+ # component. Then we count number of iterations between zero crossings
25
+ # and this is the `period` of the dominant cycle.
4
26
  def band_pass; indicator(Indicators::DominantCycles::BandPass) end
27
+
28
+ # Homodyne means the signal is multiplied by itself. More precisely,
29
+ # we want to multiply the signal of the current bar with the complex
30
+ # value of the signal one bar ago
5
31
  def homodyne; indicator(Indicators::DominantCycles::Homodyne) end
6
32
 
33
+ # The Dual Differentiator algorithm computes the phase angle from the
34
+ # analytic signal as the arctangent of the ratio of the imaginary
35
+ # component to the real component. Further, the angular frequency
36
+ # is defined as the rate change of phase. We can use these facts to
37
+ # derive the cycle period.
7
38
  def differential; indicator(Indicators::DominantCycles::Differential) end
39
+
40
+ # The phase accumulation method of computing the dominant cycle measures
41
+ # the phase at each sample by taking the arctangent of the ratio of the
42
+ # quadrature component to the in-phase component. The phase is then
43
+ # accumulated and the period is derived from the phase.
8
44
  def phase_accumulator; indicator(Indicators::DominantCycles::PhaseAccumulator) end
45
+
46
+ # Static, arbitrarily set period.
47
+ def half_period; indicator(Indicators::DominantCycles::HalfPeriod) end
9
48
  end
10
49
  end
@@ -22,20 +22,20 @@ module Quant
22
22
  attribute :reversal, default: false
23
23
  end
24
24
 
25
- # Auto-Correlation Reversals
25
+ # Auto-Correlation Reversals is a method of computing the dominant cycle
26
+ # by correlating the data stream with itself delayed by a lag.
27
+ # Construction of the autocorrelation periodogram starts with the
28
+ # autocorrelation function using the minimum three bars of averaging.
29
+ # The cyclic information is extracted using a discrete Fourier transform
30
+ # (DFT) of the autocorrelation results.
26
31
  class Acr < DominantCycle
27
- def average_length
28
- 3 # AvgLength
29
- end
30
-
31
- def bandwidth
32
- deg2rad(370)
33
- end
32
+ BANDWIDTH_DEGREES = 370
33
+ BANDWIDTH_RADIANS = BANDWIDTH_DEGREES * Math::PI / 180.0
34
34
 
35
35
  def compute_auto_correlations
36
36
  (min_period..max_period).each do |period|
37
37
  corr = Statistics::Correlation.new
38
- average_length.times do |lookback_period|
38
+ micro_period.times do |lookback_period|
39
39
  corr.add(p(lookback_period).filter, p(period + lookback_period).filter)
40
40
  end
41
41
  p0.corr[period] = corr.coefficient
@@ -46,8 +46,8 @@ module Quant
46
46
  p0.maxpwr = 0.995 * p1.maxpwr
47
47
 
48
48
  (min_period..max_period).each do |period|
49
- (average_length..max_period).each do |n|
50
- radians = bandwidth * n / period
49
+ (micro_period..max_period).each do |n|
50
+ radians = BANDWIDTH_RADIANS * n / period
51
51
  p0.cospart[period] += p0.corr[n] * Math.cos(radians)
52
52
  p0.sinpart[period] += p0.corr[n] * Math.sin(radians)
53
53
  end
@@ -98,4 +98,4 @@ module Quant
98
98
  end
99
99
  end
100
100
  end
101
- end
101
+ end
@@ -14,6 +14,11 @@ module Quant
14
14
  attribute :direction, default: :flat
15
15
  end
16
16
 
17
+ # The band-pass dominant cycle passes signals within a certain frequency
18
+ # range, and attenuates signals outside that range.
19
+ # The trend component of the signal is revoved, leaving only the cyclical
20
+ # component. Then we count number of iterations between zero crossings
21
+ # and this is the `period` of the dominant cycle.
17
22
  class BandPass < DominantCycle
18
23
  def bandwidth
19
24
  0.75
@@ -1,9 +1,11 @@
1
1
  module Quant
2
2
  class Indicators
3
3
  class DominantCycles
4
- # The Dual Differentiator algorithm computes the phase angle from the analytic signal as the arctangent of
5
- # the ratio of the imaginary component to the real compo- nent. Further, the angular frequency is defined
6
- # as the rate change of phase. We can use these facts to derive the cycle period.
4
+ # The Dual Differentiator algorithm computes the phase angle from the
5
+ # analytic signal as the arctangent of the ratio of the imaginary
6
+ # component to the real component. Further, the angular frequency
7
+ # is defined as the rate change of phase. We can use these facts to
8
+ # derive the cycle period.
7
9
  class Differential < DominantCycle
8
10
  def compute_period
9
11
  p0.ddd = (p0.q2 * (p0.i2 - p1.i2)) - (p0.i2 * (p0.q2 - p1.q2))
@@ -2,6 +2,20 @@ require_relative "../indicator"
2
2
 
3
3
  module Quant
4
4
  class Indicators
5
+ # Dominant Cycles measure the primary cycle within a given range. By default, the library
6
+ # is wired to look for cycles between 10 and 48 bars. These values can be adjusted by setting
7
+ # the `min_period` and `max_period` configuration values in {Quant::Config}.
8
+ #
9
+ # Quant.configure_indicators(min_period: 8, max_period: 32)
10
+ #
11
+ # The default dominant cycle kind is the `half_period` filter. This can be adjusted by setting
12
+ # the `dominant_cycle_kind` configuration value in {Quant::Config}.
13
+ #
14
+ # Quant.configure_indicators(dominant_cycle_kind: :band_pass)
15
+ #
16
+ # The purpose of these indicators is to compute the dominant cycle and underpin the various
17
+ # indicators that would otherwise be setting an arbitrary lookback period. This makes the
18
+ # indicators adaptive and auto-tuning to the market dynamics. Or so the theory goes!
5
19
  class DominantCycles
6
20
  class DominantCyclePoint < Quant::Indicators::IndicatorPoint
7
21
  attribute :smooth, default: 0.0
@@ -39,6 +53,8 @@ module Quant
39
53
  p0.inst_period = p0.inst_period.clamp(min_period, max_period)
40
54
  end
41
55
 
56
+ attr_reader :points
57
+
42
58
  # constrain magnitude of change in phase
43
59
  def constrain_period_magnitude_change
44
60
  p0.inst_period = [1.5 * p1.inst_period, p0.inst_period].min
@@ -0,0 +1,21 @@
1
+ require_relative "dominant_cycle"
2
+
3
+ module Quant
4
+ class Indicators
5
+ class DominantCycles
6
+ # This dominant cycle indicator is based on the half period
7
+ # that is the midpoint of the `min_period` and `max_period`
8
+ # configured in the `Quant.config.indicators` object.
9
+ # Effectively providing a static, arbitrarily set period.
10
+ class HalfPeriodPoint < Quant::Indicators::IndicatorPoint
11
+ attribute :period, default: :half_period
12
+ end
13
+
14
+ class HalfPeriod < DominantCycle
15
+ def compute
16
+ # No-Op
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,8 +4,9 @@ require_relative "dominant_cycle"
4
4
  module Quant
5
5
  class Indicators
6
6
  class DominantCycles
7
- # Homodyne means the signal is multiplied by itself. More precisely, we want to multiply the signal
8
- # of the current bar with the complex value of the signal one bar ago
7
+ # Homodyne means the signal is multiplied by itself. More precisely,
8
+ # we want to multiply the signal of the current bar with the complex
9
+ # value of the signal one bar ago
9
10
  class Homodyne < DominantCycle
10
11
  def compute_period
11
12
  p0.re = (p0.i2 * p1.i2) + (p0.q2 * p1.q2)
@@ -8,7 +8,7 @@ module Quant
8
8
  # at each sample by taking the arctangent of the ratio of the quadrature
9
9
  # component to the in-phase component. A delta phase is generated by
10
10
  # taking the difference of the phase between successive samples.
11
- # At each sam- ple we can then look backwards, adding up the delta
11
+ # At each sample we can then look backwards, adding up the delta
12
12
  # phases. When the sum of the delta phases reaches 360 degrees,
13
13
  # we must have passed through one full cycle, on average. The process
14
14
  # is repeated for each new sample.
@@ -19,6 +19,7 @@ module Quant
19
19
  @series = series
20
20
  @source = source
21
21
  @points = {}
22
+ series.new_indicator(self)
22
23
  series.each { |tick| self << tick }
23
24
  end
24
25
 
@@ -46,6 +47,14 @@ module Quant
46
47
  Quant.config.indicators.pivot_kind
47
48
  end
48
49
 
50
+ def dominant_cycle
51
+ series.indicators[source].dominant_cycle
52
+ end
53
+
54
+ def dc_period
55
+ dominant_cycle.points[t0].period
56
+ end
57
+
49
58
  def ticks
50
59
  @points.keys
51
60
  end
@@ -13,12 +13,21 @@ module Quant
13
13
  # By design, the {Quant::Indicator} class holds the {Quant::Ticks::Tick} instance
14
14
  # alongside the indicator's computed values for that tick.
15
15
  class IndicatorsProxy
16
- attr_reader :series, :source, :indicators
16
+ attr_reader :series, :source, :dominant_cycle, :indicators
17
17
 
18
18
  def initialize(series:, source:)
19
19
  @series = series
20
20
  @source = source
21
21
  @indicators = {}
22
+ @dominant_cycle = dominant_cycle_indicator
23
+ end
24
+
25
+ def dominant_cycle_indicator
26
+ kind = Quant.config.indicators.dominant_cycle_kind.to_s
27
+ base_class_name = kind.split("_").map(&:capitalize).join
28
+ class_name = "Quant::Indicators::DominantCycles::#{base_class_name}"
29
+ indicator_class = Object.const_get(class_name)
30
+ indicator_class.new(series:, source:)
22
31
  end
23
32
 
24
33
  # Instantiates the indicator class and stores it in the indicators hash. Once
@@ -36,6 +45,7 @@ module Quant
36
45
  # The IndicatorsProxy class is not responsible for enforcing
37
46
  # this order of events.
38
47
  def <<(tick)
48
+ dominant_cycle << tick
39
49
  indicators.each_value { |indicator| indicator << tick }
40
50
  end
41
51
 
@@ -7,6 +7,16 @@ module Quant
7
7
  @indicator_sources = {}
8
8
  end
9
9
 
10
+ def new_indicator(indicator)
11
+ @indicator_sources[indicator.source] ||= Indicators.new(series: @series, source: indicator.source)
12
+ end
13
+
14
+ def [](source)
15
+ return @indicator_sources[source] if @indicator_sources.key?(source)
16
+
17
+ raise Quant::Errors::InvalidIndicatorSource, "Invalid source, #{source.inspect}."
18
+ end
19
+
10
20
  def <<(tick)
11
21
  @indicator_sources.each_value { |indicator| indicator << tick }
12
22
  end
data/lib/quant/series.rb CHANGED
@@ -101,6 +101,20 @@ module Quant
101
101
  "#<#{self.class.name} symbol=#{symbol} interval=#{interval} ticks=#{ticks.size}>"
102
102
  end
103
103
 
104
+ # When the first indicator is instantiated, it will also lead to instantiating
105
+ # the dominant cycle indicator. The `new_indicator_lock` prevents reentrant calls
106
+ # to the `new_indicator` method with infinite recursion.
107
+ def new_indicator(indicator)
108
+ return if @new_indicator_lock
109
+
110
+ begin
111
+ @new_indicator_lock = true
112
+ indicators.new_indicator(indicator)
113
+ ensure
114
+ @new_indicator_lock = false
115
+ end
116
+ end
117
+
104
118
  def <<(tick)
105
119
  tick = Ticks::Spot.new(price: tick) if tick.is_a?(Numeric)
106
120
  indicators << tick unless tick.series?
@@ -8,14 +8,16 @@ module Quant
8
8
  # papers and books on the subject of technical analysis by John Ehlers where he variously suggests
9
9
  # a minimum period of 8 or 10 and a max period of 48.
10
10
  #
11
- # The half period is the average of the max_period and min_period.
11
+ # The half period is the average of the max_period and min_period. It is read-only and always computed
12
+ # relative to `min_period` and `max_period`.
13
+ #
12
14
  # The micro period comes from Ehler's writings on Swami charts and auto-correlation computations, which
13
15
  # is a period of 3 bars. It is useful enough in various indicators to be its own setting.
14
16
  #
15
17
  # The dominant cycle kind is the kind of dominant cycle to use in the indicator. The default is +:settings+
16
18
  # which means the dominant cycle is whatever the +max_period+ is set to. It is not adaptive when configured
17
19
  # this way. The other kinds are adaptive and are computed from the series data. The choices are:
18
- # * +:settings+ - the max_period is the dominant cycle and is not adaptive
20
+ # * +:half_period+ - the half_period is the dominant cycle and is not adaptive
19
21
  # * +:band_pass+ - The zero crossings of the band pass filter are used to compute the dominant cycle
20
22
  # * +:auto_correlation_reversal+ - The dominant cycle is computed from the auto-correlation of the series.
21
23
  # * +:homodyne+ - The dominant cycle is computed from the homodyne discriminator.
@@ -48,8 +50,8 @@ module Quant
48
50
  new
49
51
  end
50
52
 
51
- attr_accessor :max_period, :min_period, :half_period, :micro_period
52
- attr_accessor :dominant_cycle_kind, :pivot_kind
53
+ attr_reader :max_period, :min_period, :half_period
54
+ attr_accessor :micro_period, :dominant_cycle_kind, :pivot_kind
53
55
 
54
56
  def initialize(**settings)
55
57
  @max_period = settings[:max_period] || Settings::MAX_PERIOD
@@ -64,14 +66,22 @@ module Quant
64
66
  def apply_settings(**settings)
65
67
  @max_period = settings.fetch(:max_period, @max_period)
66
68
  @min_period = settings.fetch(:min_period, @min_period)
67
- @half_period = settings.fetch(:half_period, @half_period || compute_half_period)
69
+ compute_half_period
68
70
  @micro_period = settings.fetch(:micro_period, @micro_period)
69
71
  @dominant_cycle_kind = settings.fetch(:dominant_cycle_kind, @dominant_cycle_kind)
70
72
  @pivot_kind = settings.fetch(:pivot_kind, @pivot_kind)
71
73
  end
72
74
 
75
+ def max_period=(value)
76
+ (@max_period = value).tap { compute_half_period }
77
+ end
78
+
79
+ def min_period=(value)
80
+ (@min_period = value).tap { compute_half_period }
81
+ end
82
+
73
83
  def compute_half_period
74
- (max_period + min_period) / 2
84
+ @half_period = (max_period + min_period) / 2
75
85
  end
76
86
  end
77
87
  end
@@ -23,7 +23,7 @@ module Quant
23
23
  ).freeze
24
24
 
25
25
  DOMINANT_CYCLE_KINDS = %i(
26
- settings
26
+ half_period
27
27
  band_pass
28
28
  auto_correlation_reversal
29
29
  homodyne
data/lib/quant/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quant
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quantitative
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Lang
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-11 00:00:00.000000000 Z
11
+ date: 2024-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -56,6 +56,7 @@ files:
56
56
  - lib/quant/indicators/dominant_cycles/band_pass.rb
57
57
  - lib/quant/indicators/dominant_cycles/differential.rb
58
58
  - lib/quant/indicators/dominant_cycles/dominant_cycle.rb
59
+ - lib/quant/indicators/dominant_cycles/half_period.rb
59
60
  - lib/quant/indicators/dominant_cycles/homodyne.rb
60
61
  - lib/quant/indicators/dominant_cycles/phase_accumulator.rb
61
62
  - lib/quant/indicators/indicator.rb