diff --git a/.gitignore b/.gitignore index 783440f..a1ecec9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # rspec failure tracking .rspec_status *.gem + +# Serena AI assistant workspace +.serena/ diff --git a/Gemfile.lock b/Gemfile.lock index c8e9582..ccc6381 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - quantitative (0.4.0) + quantitative (0.4.1) csv oj (~> 3.10) zeitwerk (~> 2.6) diff --git a/lib/quant/ticks/ohlc.rb b/lib/quant/ticks/ohlc.rb index b5fb4ea..6a0de8b 100644 --- a/lib/quant/ticks/ohlc.rb +++ b/lib/quant/ticks/ohlc.rb @@ -45,8 +45,11 @@ def initialize( @low_price = low_price.to_f @close_price = close_price.to_f - @base_volume = (volume || base_volume).to_i - @target_volume = (target_volume || @base_volume).to_i + # Volumes are floats, not ints — crypto markets and many futures markets express + # fractional base/target volumes routinely (e.g., 0.12345 BTC). Existing specs already + # asserted Float values (`eq(2.0)`); a prior `.to_i` regression silently truncated. + @base_volume = (volume || base_volume).to_f + @target_volume = (target_volume || @base_volume).to_f @trades = trades.to_i @green = green.nil? ? compute_green : green diff --git a/lib/quant/ticks/spot.rb b/lib/quant/ticks/spot.rb index d778c48..22f241d 100644 --- a/lib/quant/ticks/spot.rb +++ b/lib/quant/ticks/spot.rb @@ -39,8 +39,12 @@ def initialize( @close_timestamp = extract_time(timestamp || close_timestamp || Quant.current_time) @open_timestamp = @close_timestamp - @base_volume = (volume || base_volume).to_i - @target_volume = (target_volume || @base_volume).to_i + # Volumes are floats, not ints — crypto markets and many futures markets express + # fractional base/target volumes routinely (e.g., 0.12345 BTC). Existing specs already + # asserted Float values (`eq(2.0)`); a prior `.to_i` regression silently truncated. + # See OHLC for the same fix. + @base_volume = (volume || base_volume).to_f + @target_volume = (target_volume || @base_volume).to_f @trades = trades.to_i super() diff --git a/lib/quant/version.rb b/lib/quant/version.rb index 547a7e5..8f90815 100644 --- a/lib/quant/version.rb +++ b/lib/quant/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Quant - VERSION = "0.4.0" + VERSION = "0.4.1" end diff --git a/quantitative.gemspec b/quantitative.gemspec index 5107230..aebe721 100644 --- a/quantitative.gemspec +++ b/quantitative.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. diff --git a/spec/lib/quant/ticks/ohlc_spec.rb b/spec/lib/quant/ticks/ohlc_spec.rb index e70b0f9..9ded54e 100644 --- a/spec/lib/quant/ticks/ohlc_spec.rb +++ b/spec/lib/quant/ticks/ohlc_spec.rb @@ -25,7 +25,7 @@ ) end - it { expect(tick.inspect).to eq("#") } + it { expect(tick.inspect).to eq("#") } end describe "equality" do @@ -71,6 +71,23 @@ end end + # Regression guard: volumes must be Float, not Integer-truncated. Crypto markets + # express fractional volumes routinely (e.g., 0.12345 BTC); a prior `.to_i` coercion + # in #initialize silently truncated them and passed loose `eq(2.0)` assertions + # because Ruby's `==` coerces `2 == 2.0`. This block asserts both the value AND the + # class so future regressions fail loudly. + context "with fractional crypto-style volumes" do + let(:json) do + { "ot" => open_time.to_i, "ct" => close_time, "o" => 1.0, + "bv" => 0.12345, "tv" => 5_678.901_234 } + end + + it { expect(subject.base_volume).to eq(0.12345) } + it { expect(subject.target_volume).to eq(5_678.901_234) } + it { expect(subject.base_volume).to be_a(Float) } + it { expect(subject.target_volume).to be_a(Float) } + end + context "without volume" do let(:json) { { "ot" => open_time.to_i, "ct" => close_time, "o" => 1.0 } } diff --git a/spec/lib/quant/ticks/spot_spec.rb b/spec/lib/quant/ticks/spot_spec.rb index 71d7cd7..64b1501 100644 --- a/spec/lib/quant/ticks/spot_spec.rb +++ b/spec/lib/quant/ticks/spot_spec.rb @@ -31,6 +31,24 @@ expect(subject.target_volume).to eq(0.0) end end + + # Regression guard: volumes must be Float, not Integer-truncated. Crypto markets + # (and individual trade prints) express fractional volumes routinely (e.g., + # 0.12345 BTC). A prior `.to_i` coercion in #initialize silently truncated and + # passed loose `eq(2.0)` assertions because Ruby's `==` coerces `2 == 2.0`. + # This block asserts both the value AND the class so future regressions fail loudly. + # See OHLC for the parallel fix. + context "with fractional crypto-style volumes" do + let(:hash) do + { "ct" => close_time.to_i, "cp" => 1.0, + "bv" => 0.12345, "tv" => 5_678.901_234 } + end + + it { expect(subject.base_volume).to eq(0.12345) } + it { expect(subject.target_volume).to eq(5_678.901_234) } + it { expect(subject.base_volume).to be_a(Float) } + it { expect(subject.target_volume).to be_a(Float) } + end end describe ".from_json" do @@ -60,7 +78,7 @@ ) end - it { expect(tick.inspect).to eq("#") } + it { expect(tick.inspect).to eq("#") } end describe "#corresponding?" do