diff --git a/Gemfile b/Gemfile index e2cf0d4..a4a41a2 100644 --- a/Gemfile +++ b/Gemfile @@ -43,6 +43,10 @@ gem "thruster", require: false gem "chartkick" +gem "sidekiq", "~> 7.3" + +gem "redis", "~> 5.3" + # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] gem "image_processing", "~> 1.2" diff --git a/Gemfile.lock b/Gemfile.lock index 3d3fe7b..bce4a41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,7 +83,7 @@ GEM bindex (0.8.1) bootsnap (1.21.1) msgpack (~> 1.2) - brakeman (7.1.2) + brakeman (8.0.1) racc builder (3.3.0) bullet (8.1.0) @@ -203,7 +203,7 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.8.0) + prism (1.9.0) propshaft (1.3.1) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -255,12 +255,17 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.1) - rbs (3.10.2) + rbs (3.10.3) logger + tsort rdoc (7.1.0) erb psych (>= 4.0.0) tsort + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.26.4) + connection_pool regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) @@ -283,8 +288,8 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.6) - rubocop (1.82.1) + rspec-support (3.13.7) + rubocop (1.84.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -292,7 +297,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.48.0, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -326,6 +331,12 @@ GEM securerandom (0.4.1) shoulda-matchers (6.5.0) activesupport (>= 5.2.0) + sidekiq (7.3.9) + base64 + connection_pool (>= 2.3.0) + logger + rack (>= 2.2.4) + redis-client (>= 0.22.2) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -366,7 +377,7 @@ GEM thruster (0.1.17-x86_64-linux) timeout (0.6.0) tsort (0.2.0) - turbo-rails (2.0.21) + turbo-rails (2.0.23) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -411,6 +422,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.1) + redis (~> 5.3) rspec-rails (~> 7.1, >= 7.1.1) rubocop (~> 1.69) rubocop-performance (~> 1.24) @@ -419,6 +431,7 @@ DEPENDENCIES rubocop-rspec (~> 3.3) ruby-lsp shoulda-matchers (~> 6.5) + sidekiq (~> 7.3) simplecov (~> 0.21.2) solid_cable solid_cache diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9e471ad..df50ad2 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,11 +1,11 @@ class DashboardController < ApplicationController def index @wallets = current_user.wallets.includes(:holdings, :transactions, :strategy) - + @total_wallets = @wallets.count @active_wallets = @wallets.active.count @inactive_wallets = @wallets.inactive.count - + @wallet_summaries = @wallets.map do |wallet| { wallet: wallet, @@ -16,12 +16,12 @@ def index has_strategy: wallet.strategy.present? } end - + @total_invested = @wallet_summaries.sum { |ws| ws[:total_invested] } @total_current_value = @wallet_summaries.sum { |ws| ws[:current_value] } @total_profit_loss = @total_current_value - @total_invested @total_profit_loss_percentage = @total_invested.zero? ? 0 : ((@total_profit_loss / @total_invested) * 100) - + @recent_transactions = current_user.wallets .joins(:transactions) .includes(transactions: :instrument) @@ -29,43 +29,43 @@ def index .limit(10) .flat_map(&:transactions) .first(10) - - + + @wallet_distribution = @wallet_summaries.map do |summary| - [summary[:wallet].name, summary[:current_value].to_f] + [ summary[:wallet].name, summary[:current_value].to_f ] end.to_h - + transactions_last_6_months = current_user.wallets .joins(:transactions) .where("transactions.occurred_at >= ?", 6.months.ago) .pluck("transactions.occurred_at") - + @transactions_by_month = transactions_last_6_months .compact .group_by { |date| date.beginning_of_month.strftime("%b %Y") } .transform_values(&:count) .sort_by { |month, _| Date.parse("01 #{month}") } .to_h - + @buy_vs_sell = { "Buys" => current_user.wallets.joins(:transactions).merge(Transaction.buy).count, "Sells" => current_user.wallets.joins(:transactions).merge(Transaction.sell).count } - + holdings_with_instruments = current_user.wallets .joins(holdings: :instrument) .select("instruments.ticker, holdings.average_price, holdings.quantity") - + @top_instruments = holdings_with_instruments .group_by(&:ticker) - .map { |ticker, holdings| + .map { |ticker, holdings| total = holdings.sum { |h| (h.average_price || 0) * (h.quantity || 0) } - [ticker, total.to_f] + [ ticker, total.to_f ] } .sort_by { |_, value| -value } .first(5) .to_h - + @wallet_comparison_data = @wallet_summaries.map do |summary| [ summary[:wallet].name, @@ -76,14 +76,14 @@ def index ] end.to_h end - + private - + def calculate_total_invested(wallet) - wallet.transactions.buy.sum('price * quantity') + wallet.transactions.buy.sum("price * quantity") end - + def calculate_current_value(wallet) - wallet.holdings.sum('average_price * quantity') + wallet.holdings.sum("average_price * quantity") end -end \ No newline at end of file +end diff --git a/app/models/instrument.rb b/app/models/instrument.rb index 22daf6e..7cb0817 100644 --- a/app/models/instrument.rb +++ b/app/models/instrument.rb @@ -2,4 +2,5 @@ class Instrument < ApplicationRecord has_many :holdings, dependent: :destroy has_many :transactions, dependent: :destroy has_many :wallets, through: :holdings + has_many :instrument_metrics, dependent: :destroy end diff --git a/app/models/instrument_metric.rb b/app/models/instrument_metric.rb new file mode 100644 index 0000000..a534572 --- /dev/null +++ b/app/models/instrument_metric.rb @@ -0,0 +1,3 @@ +class InstrumentMetric < ApplicationRecord + belongs_to :instrument +end diff --git a/app/models/strategy_rule.rb b/app/models/strategy_rule.rb index cd9d567..3547c31 100644 --- a/app/models/strategy_rule.rb +++ b/app/models/strategy_rule.rb @@ -6,8 +6,8 @@ class StrategyRule < ApplicationRecord validates :asset_kind, presence: true validates :strategy_id, presence: true - validate :validate_percentage_rule, if: -> { rule_type == 'percentage' } - validate :validate_prohibition_rule, if: -> { rule_type == 'prohibition' } + validate :validate_percentage_rule, if: -> { rule_type == "percentage" } + validate :validate_prohibition_rule, if: -> { rule_type == "prohibition" } # Métodos auxiliares def prohibition? diff --git a/compose.yml b/compose.yml index 68fbfc7..f75b4b6 100644 --- a/compose.yml +++ b/compose.yml @@ -50,9 +50,19 @@ services: networks: - ledger_net + redis: + container_name: ledger_redis + image: "redis:latest" + command: redis-server + volumes: + - redis_data:/data + networks: + - ledger_net + volumes: postgres_data: gem_cache: + redis_data: networks: ledger_net: diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..c665c01 --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,7 @@ +Sidekiq.configure_server do |config| + config.redis = { url: ENV.fetch("REDIS_URL") } +end + +Sidekiq.configure_client do |config| + config.redis = { url: ENV.fetch("REDIS_URL") } +end diff --git a/config/routes.rb b/config/routes.rb index 3b75d34..1dc3c3d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,14 @@ +require "sidekiq/web" + Rails.application.routes.draw do devise_for :users # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + Sidekiq::Web.use Rack::Auth::Basic do |username, password| + username == ENV["SIDEKIQ_USERNAME"] && password == ENV["SIDEKIQ_PASSWORD"] + end + + mount Sidekiq::Web => "/sidekiq" # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check diff --git a/db/migrate/20260131171800_create_instrument_metrics.rb b/db/migrate/20260131171800_create_instrument_metrics.rb new file mode 100644 index 0000000..333d20b --- /dev/null +++ b/db/migrate/20260131171800_create_instrument_metrics.rb @@ -0,0 +1,17 @@ +class CreateInstrumentMetrics < ActiveRecord::Migration[8.1] + def change + create_table :instrument_metrics do |t| + t.references :instrument, null: false, foreign_key: true + t.decimal :price, precision: 15, scale: 4, null: false + t.decimal :dy, precision: 5, scale: 2 + t.decimal :p_vp, precision: 5, scale: 2 + t.decimal :daily_liquidity, precision: 15, scale: 2 + t.decimal :market_cap, precision: 20, scale: 2 + t.datetime :recorded_at, null: false + + t.timestamps + end + + add_index :instrument_metrics, [ :instrument_id, :recorded_at ] + end +end diff --git a/db/schema.rb b/db/schema.rb index 32eebba..e5e0599 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_12_25_031419) do +ActiveRecord::Schema[8.1].define(version: 2026_01_31_171800) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -26,6 +26,20 @@ t.index ["wallet_id"], name: "index_holdings_on_wallet_id" end + create_table "instrument_metrics", force: :cascade do |t| + t.datetime "created_at", null: false + t.decimal "daily_liquidity", precision: 15, scale: 2 + t.decimal "dy", precision: 5, scale: 2 + t.bigint "instrument_id", null: false + t.decimal "market_cap", precision: 20, scale: 2 + t.decimal "p_vp", precision: 5, scale: 2 + t.decimal "price", precision: 15, scale: 4, null: false + t.datetime "recorded_at", null: false + t.datetime "updated_at", null: false + t.index ["instrument_id", "recorded_at"], name: "index_instrument_metrics_on_instrument_id_and_recorded_at" + t.index ["instrument_id"], name: "index_instrument_metrics_on_instrument_id" + end + create_table "instruments", force: :cascade do |t| t.datetime "created_at", null: false t.string "kind" @@ -91,6 +105,7 @@ add_foreign_key "holdings", "instruments" add_foreign_key "holdings", "wallets" + add_foreign_key "instrument_metrics", "instruments" add_foreign_key "strategies", "wallets" add_foreign_key "strategy_rules", "strategies" add_foreign_key "transactions", "instruments"