From 032808dd1a8aa837051e8b6c5aa90ff659d208ed Mon Sep 17 00:00:00 2001 From: Michael Lang Date: Sat, 16 May 2026 20:13:52 -0400 Subject: [PATCH 1/3] Bump to v0.4.1: fix fractional volume truncation in OHLC and Spot ticks Volumes were coerced with `.to_i`, silently truncating fractional values common in crypto and futures markets (e.g. 0.12345 BTC). Changed to `.to_f` in both Quant::Ticks::OHLC and Quant::Ticks::Spot initializers, added regression specs that assert both value and class, updated changelog URI in gemspec, and added .serena/ to .gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +++ lib/quant/ticks/ohlc.rb | 7 +++++-- lib/quant/ticks/spot.rb | 8 ++++++-- lib/quant/version.rb | 2 +- quantitative.gemspec | 2 +- spec/lib/quant/ticks/ohlc_spec.rb | 17 +++++++++++++++++ spec/lib/quant/ticks/spot_spec.rb | 18 ++++++++++++++++++ 7 files changed, 51 insertions(+), 6 deletions(-) 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/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..bec3c3c 100644 --- a/spec/lib/quant/ticks/ohlc_spec.rb +++ b/spec/lib/quant/ticks/ohlc_spec.rb @@ -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..efab596 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 From 48aeae611242ba661064fb7c6d685a7138d3c589 Mon Sep 17 00:00:00 2001 From: Michael Lang Date: Sat, 16 May 2026 20:15:32 -0400 Subject: [PATCH 2/3] Update Gemfile.lock to v0.4.1 Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 1ce71c3ff7994b7141fec7882c9575198c4bd57d Mon Sep 17 00:00:00 2001 From: Michael Lang Date: Sat, 16 May 2026 20:17:02 -0400 Subject: [PATCH 3/3] Fix #inspect specs to expect Float volume format (v=88.0 not v=88) Volumes are now stored as Float; inspect output reflects that. Co-Authored-By: Claude Sonnet 4.6 --- spec/lib/quant/ticks/ohlc_spec.rb | 2 +- spec/lib/quant/ticks/spot_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/quant/ticks/ohlc_spec.rb b/spec/lib/quant/ticks/ohlc_spec.rb index bec3c3c..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 diff --git a/spec/lib/quant/ticks/spot_spec.rb b/spec/lib/quant/ticks/spot_spec.rb index efab596..64b1501 100644 --- a/spec/lib/quant/ticks/spot_spec.rb +++ b/spec/lib/quant/ticks/spot_spec.rb @@ -78,7 +78,7 @@ ) end - it { expect(tick.inspect).to eq("#") } + it { expect(tick.inspect).to eq("#") } end describe "#corresponding?" do