forked from finfex/gera
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Улучшить алгоритм автокурса - занимать позицию в диапазоне #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
3102779
feat: Улучшить алгоритм автокурса - занимать позицию в диапазоне (#69)
dapi efcaa72
fix: Добавить raise для неизвестного calculator_type и документацию в…
dapi 60d5af4
chore: Добавить Makefile и docker-compose.yml для разработки
dapi 70d7ab8
Merge branch 'master' into feature/69-position-aware-autorate
dapi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| .PHONY: test test-all db-up db-down | ||
|
|
||
| # Run calculator and exchange_rate tests (the ones we fixed) | ||
| test: | ||
| mise exec -- bundle exec rspec \ | ||
| spec/services/gera/autorate_calculators/isolated_spec.rb \ | ||
| spec/models/gera/exchange_rate_spec.rb \ | ||
| --no-color | ||
|
|
||
| # Run all tests | ||
| test-all: | ||
| mise exec -- bundle exec rspec --no-color | ||
|
|
||
| # Start MySQL for testing | ||
| db-up: | ||
| docker-compose up -d | ||
| @echo "Waiting for MySQL..." | ||
| @for i in $$(seq 1 30); do \ | ||
| docker exec gera-legacy-mysql-1 mysqladmin ping -h localhost -u root -p1111 2>/dev/null && break || sleep 2; \ | ||
| done | ||
| @echo "MySQL is ready" | ||
|
|
||
| # Stop MySQL | ||
| db-down: | ||
| docker-compose down |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'active_support/core_ext/module/delegation' | ||
| require 'active_support/core_ext/object/blank' | ||
|
|
||
| module Gera | ||
| module AutorateCalculators | ||
| class Base | ||
| include Virtus.model strict: true | ||
|
|
||
| AUTO_COMISSION_GAP = 0.001 | ||
|
|
||
| attribute :exchange_rate | ||
| attribute :external_rates | ||
|
|
||
| delegate :position_from, :position_to, :autorate_from, :autorate_to, | ||
| to: :exchange_rate | ||
|
|
||
| def call | ||
| raise NotImplementedError, "#{self.class}#call must be implemented" | ||
| end | ||
|
|
||
| protected | ||
|
|
||
| def could_be_calculated? | ||
| !external_rates.nil? && exchange_rate.target_autorate_setting&.could_be_calculated? | ||
| end | ||
|
|
||
| def external_rates_in_target_position | ||
| return nil unless external_rates.present? | ||
|
|
||
| external_rates[(position_from - 1)..(position_to - 1)] | ||
| end | ||
|
|
||
| def external_rates_in_target_comission | ||
| return [] unless external_rates_in_target_position.present? | ||
|
|
||
| external_rates_in_target_position.select do |rate| | ||
| (autorate_from..autorate_to).include?(rate.target_rate_percent) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Gera | ||
| module AutorateCalculators | ||
| # Legacy калькулятор - сохраняет текущее поведение. | ||
| # Вычитает фиксированный GAP из комиссии первого конкурента в диапазоне. | ||
| # Может "перепрыгивать" позиции выше целевого диапазона. | ||
| class Legacy < Base | ||
| def call | ||
| return 0 unless could_be_calculated? | ||
| return autorate_from unless external_rates_in_target_position.present? | ||
| return autorate_from if external_rates_in_target_comission.empty? | ||
|
|
||
| external_rates_in_target_comission.first.target_rate_percent - AUTO_COMISSION_GAP | ||
| end | ||
| end | ||
| end | ||
| end |
118 changes: 118 additions & 0 deletions
118
app/services/gera/autorate_calculators/position_aware.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Gera | ||
| module AutorateCalculators | ||
| # Калькулятор с учётом позиций выше целевого диапазона. | ||
| # Гарантирует, что обменник займёт позицию внутри диапазона position_from..position_to, | ||
| # а не перепрыгнет выше. | ||
| # | ||
| # Поддерживает: | ||
| # - UC-6: Адаптивный GAP для плотных рейтингов | ||
| # - UC-8: Исключение своего обменника из расчёта | ||
| # - UC-9: Защита от манипуляторов с аномальными курсами | ||
| class PositionAware < Base | ||
| # Минимальный GAP (используется когда разница между позициями меньше стандартного) | ||
| MIN_GAP = 0.0001 | ||
|
|
||
| def call | ||
| return 0 unless could_be_calculated? | ||
|
|
||
| # UC-8: Фильтрация своего обменника | ||
| filtered = filtered_external_rates | ||
| return autorate_from unless filtered.present? | ||
|
|
||
| rates_in_target_position = filtered[(position_from - 1)..(position_to - 1)] | ||
| return autorate_from unless rates_in_target_position.present? | ||
|
|
||
| valid_rates = rates_in_target_position.select do |rate| | ||
| (autorate_from..autorate_to).include?(rate.target_rate_percent) | ||
| end | ||
| return autorate_from if valid_rates.empty? | ||
|
|
||
| target_rate = valid_rates.first | ||
|
|
||
| # UC-6: Адаптивный GAP | ||
| gap = calculate_adaptive_gap(filtered, target_rate) | ||
| target_comission = target_rate.target_rate_percent - gap | ||
|
|
||
| # Проверяем, не перепрыгнем ли мы позицию выше position_from | ||
| adjusted_comission = adjust_for_position_above(target_comission, target_rate, filtered) | ||
|
|
||
| adjusted_comission | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # UC-8: Фильтрация своего обменника | ||
| def filtered_external_rates | ||
| return external_rates unless Gera.our_exchanger_id.present? | ||
|
|
||
| external_rates.reject { |rate| rate.exchanger_id == Gera.our_exchanger_id } | ||
| end | ||
|
|
||
| # UC-6: Адаптивный GAP | ||
| def calculate_adaptive_gap(rates, target_rate) | ||
| return AUTO_COMISSION_GAP if position_from <= 1 | ||
|
|
||
| rate_above = rates[position_from - 2] | ||
| return AUTO_COMISSION_GAP unless rate_above | ||
|
|
||
| diff = target_rate.target_rate_percent - rate_above.target_rate_percent | ||
|
|
||
| # Если разница между позициями меньше стандартного GAP, | ||
| # используем половину разницы (но не меньше MIN_GAP) | ||
| if diff.positive? && diff < AUTO_COMISSION_GAP | ||
| [diff / 2.0, MIN_GAP].max | ||
| else | ||
| AUTO_COMISSION_GAP | ||
| end | ||
| end | ||
|
|
||
| def adjust_for_position_above(target_comission, target_rate, rates) | ||
| return target_comission if position_from <= 1 | ||
|
|
||
| # UC-9: Найти ближайшую нормальную позицию выше | ||
| rate_above = find_non_anomalous_rate_above(rates) | ||
| return target_comission unless rate_above | ||
|
|
||
| rate_above_comission = rate_above.target_rate_percent | ||
|
|
||
| # Если после вычитания GAP комиссия станет меньше (выгоднее) чем у позиции выше - | ||
| # мы перепрыгнём её. Нужно скорректировать. | ||
| if target_comission < rate_above_comission | ||
| # Устанавливаем комиссию равную или чуть выше (хуже) чем у позиции выше, | ||
| # но не хуже чем у целевой позиции | ||
| safe_comission = [rate_above_comission, target_rate.target_rate_percent].min | ||
|
|
||
| # Если одинаковые курсы - оставляем как есть, BestChange определит позицию по вторичным критериям | ||
| return safe_comission | ||
| end | ||
|
|
||
| target_comission | ||
| end | ||
|
|
||
| # UC-9: Найти ближайшую нормальную (не аномальную) позицию выше целевой | ||
| def find_non_anomalous_rate_above(rates) | ||
| return nil if position_from <= 1 | ||
|
|
||
| # Берём все позиции выше целевой (от 0 до position_from - 2) | ||
| rates_above = rates[0..(position_from - 2)] | ||
| return nil unless rates_above.present? | ||
|
|
||
| # Если фильтрация аномалий отключена - просто берём ближайшую позицию выше | ||
| threshold = Gera.anomaly_threshold_percent | ||
| return rates_above.last unless threshold&.positive? && rates.size >= 3 | ||
|
|
||
| # Вычисляем медиану для определения аномалий | ||
| all_comissions = rates.map(&:target_rate_percent).sort | ||
| median = all_comissions[all_comissions.size / 2] | ||
|
|
||
| # Ищем ближайшую нормальную позицию сверху вниз | ||
| rates_above.reverse.find do |rate| | ||
| deviation = ((rate.target_rate_percent - median) / median * 100).abs | ||
| deviation <= threshold | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
db/migrate/20251224134401_add_calculator_type_to_exchange_rates.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class AddCalculatorTypeToExchangeRates < ActiveRecord::Migration[6.0] | ||
| def change | ||
| add_column :gera_exchange_rates, :calculator_type, :string, default: 'legacy', null: false | ||
| add_index :gera_exchange_rates, :calculator_type | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| version: '3.8' | ||
|
|
||
| services: | ||
| mysql: | ||
| image: mysql:8.0 | ||
| ports: | ||
| - "3306:3306" | ||
| environment: | ||
| MYSQL_ROOT_PASSWORD: 1111 | ||
| MYSQL_DATABASE: kassa_admin_test | ||
| volumes: | ||
| - mysql_data:/var/lib/mysql | ||
| healthcheck: | ||
| test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p1111"] | ||
| interval: 5s | ||
| timeout: 5s | ||
| retries: 10 | ||
| start_period: 30s | ||
| command: --default-authentication-plugin=mysql_native_password | ||
|
|
||
| volumes: | ||
| mysql_data: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Добавь в README пример как пользоваться этой конфигурацией