From ea7ce13a7d4b235e40134c06120eb629a9adfdf4 Mon Sep 17 00:00:00 2001 From: Michael Studman Date: Wed, 22 Oct 2025 03:22:24 +1100 Subject: [PATCH] Increasing trades.price decimal scale (#89) * Changing trades.price to have a larger scale - a scale of 4 causes destructive rounding when calculating transaction cost; changes to the UI to allow for inputting and showing increased scale trade prices; test case --- app/views/shared/_money_field.html.erb | 6 ++-- app/views/trades/_form.html.erb | 2 +- app/views/trades/show.html.erb | 2 ++ ...06155348_increase_trade_price_precision.rb | 9 +++++ db/schema.rb | 2 +- test/models/trade_test.rb | 34 +++++++++++++++++++ 6 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20250806155348_increase_trade_price_precision.rb diff --git a/app/views/shared/_money_field.html.erb b/app/views/shared/_money_field.html.erb index 059294013..787798c44 100644 --- a/app/views/shared/_money_field.html.erb +++ b/app/views/shared/_money_field.html.erb @@ -48,14 +48,14 @@ inline: true, placeholder: "100", value: if options[:value] - sprintf("%.#{currency.default_precision}f", options[:value]) + sprintf("%.#{options[:precision].presence || currency.default_precision}f", options[:value]) elsif form.object && form.object.respond_to?(amount_method) val = form.object.public_send(amount_method) - sprintf("%.#{currency.default_precision}f", val) if val.present? + sprintf("%.#{options[:precision].presence || currency.default_precision}f", val) if val.present? end, min: options[:min] || -99999999999999, max: options[:max] || 99999999999999, - step: currency.step, + step: options[:step] || currency.step, disabled: options[:disabled], data: { "money-field-target": "amount", diff --git a/app/views/trades/_form.html.erb b/app/views/trades/_form.html.erb index 57e22907a..cb248ece9 100644 --- a/app/views/trades/_form.html.erb +++ b/app/views/trades/_form.html.erb @@ -50,7 +50,7 @@ <% if %w[buy sell].include?(type) %> <%= form.number_field :qty, label: t(".qty"), placeholder: "10", min: 0.000000000000000001, step: "any", required: true %> - <%= form.money_field :price, label: t(".price"), required: true %> + <%= form.money_field :price, label: t(".price"), step: 'any', precision: 10, required: true %> <% end %> diff --git a/app/views/trades/show.html.erb b/app/views/trades/show.html.erb index c80b379d3..227470fba 100644 --- a/app/views/trades/show.html.erb +++ b/app/views/trades/show.html.erb @@ -40,6 +40,8 @@ disable_currency: true, auto_submit: true, min: 0, + step: "any", + precision: 10, disabled: @entry.linked? %> <% end %> <% end %> diff --git a/db/migrate/20250806155348_increase_trade_price_precision.rb b/db/migrate/20250806155348_increase_trade_price_precision.rb new file mode 100644 index 000000000..0f9cb4020 --- /dev/null +++ b/db/migrate/20250806155348_increase_trade_price_precision.rb @@ -0,0 +1,9 @@ +class IncreaseTradePricePrecision < ActiveRecord::Migration[7.2] + def up + change_column :trades, :price, :decimal, precision: 19, scale: 10 + end + + def down + change_column :trades, :price, :decimal, precision: 19, scale: 4 + end +end diff --git a/db/schema.rb b/db/schema.rb index dac00a6e5..90d190dcf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -789,7 +789,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_08_143007) do create_table "trades", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "security_id", null: false t.decimal "qty", precision: 19, scale: 4 - t.decimal "price", precision: 19, scale: 4 + t.decimal "price", precision: 19, scale: 10 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "currency" diff --git a/test/models/trade_test.rb b/test/models/trade_test.rb index c358a5ba1..c86f44210 100644 --- a/test/models/trade_test.rb +++ b/test/models/trade_test.rb @@ -20,4 +20,38 @@ class TradeTest < ActiveSupport::TestCase name = Trade.build_name("buy", 0.25, "BTC") assert_equal "Buy 0.25 shares of BTC", name end + + test "price scale is preserved at 10 decimal places" do + security = Security.create!(ticker: "TEST", exchange_operating_mic: "XNAS") + + # up to 10 decimal places — should persist exactly + precise_price = BigDecimal("12.3456789012") + trade = Trade.create!( + security: security, + price: precise_price, + qty: 10000, + currency: "USD" + ) + + trade.reload + + assert_equal precise_price, trade.price + end + + test "price is rounded to 10 decimal places" do + security = Security.create!(ticker: "TEST", exchange_operating_mic: "XNAS") + + # over 10 decimal places — will be rounded + price_with_too_many_decimals = BigDecimal("1.123456789012345") + trade = Trade.create!( + security: security, + price: price_with_too_many_decimals, + qty: 1, + currency: "USD" + ) + + trade.reload + + assert_equal BigDecimal("1.1234567890"), trade.price + end end