From deb216a0cf200d7b101b59e3525045ccdd8aa0e2 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 13:27:20 +0800 Subject: [PATCH 01/18] add ruby api --- .gitignore | 15 + .rspec | 3 + .rubocop.yml | 23 + .ruby-version | 1 + .yardopts | 6 + CHANGELOG.md | 258 ++++++ Gemfile | 3 + Rakefile | 8 + bin/console | 8 + bybit-connector-ruby.gemspec | 26 + examples/quickstart.rb | 12 + lib/bybit.rb | 10 + lib/bybit/authentication.rb | 16 + lib/bybit/client.rb | 80 ++ lib/bybit/configuration.rb | 36 + lib/bybit/error.rb | 45 + lib/bybit/rest_api/account_service.rb | 295 +++++++ lib/bybit/rest_api/affiliate_service.rb | 40 + lib/bybit/rest_api/asset_service.rb | 46 + lib/bybit/rest_api/base_service.rb | 11 + lib/bybit/rest_api/bot_service.rb | 368 ++++++++ lib/bybit/rest_api/broker_service.rb | 103 +++ lib/bybit/rest_api/crypto_loan_service.rb | 350 ++++++++ lib/bybit/rest_api/earn_service.rb | 981 ++++++++++++++++++++++ lib/bybit/rest_api/market_service.rb | 338 ++++++++ lib/bybit/rest_api/p2p_service.rb | 241 ++++++ lib/bybit/rest_api/position_service.rb | 191 +++++ lib/bybit/rest_api/rfq_service.rb | 206 +++++ lib/bybit/rest_api/spot_margin_service.rb | 58 ++ lib/bybit/rest_api/trade_service.rb | 276 ++++++ lib/bybit/rest_api/user_service.rb | 225 +++++ lib/bybit/session.rb | 102 +++ lib/bybit/utils/wire_keys.rb | 30 + lib/bybit/version.rb | 5 + spec/bybit_spec.rb | 13 + spec/spec_helper.rb | 12 + 36 files changed, 4441 insertions(+) create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 .yardopts create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 Rakefile create mode 100755 bin/console create mode 100644 bybit-connector-ruby.gemspec create mode 100644 examples/quickstart.rb create mode 100644 lib/bybit.rb create mode 100644 lib/bybit/authentication.rb create mode 100644 lib/bybit/client.rb create mode 100644 lib/bybit/configuration.rb create mode 100644 lib/bybit/error.rb create mode 100644 lib/bybit/rest_api/account_service.rb create mode 100644 lib/bybit/rest_api/affiliate_service.rb create mode 100644 lib/bybit/rest_api/asset_service.rb create mode 100644 lib/bybit/rest_api/base_service.rb create mode 100644 lib/bybit/rest_api/bot_service.rb create mode 100644 lib/bybit/rest_api/broker_service.rb create mode 100644 lib/bybit/rest_api/crypto_loan_service.rb create mode 100644 lib/bybit/rest_api/earn_service.rb create mode 100644 lib/bybit/rest_api/market_service.rb create mode 100644 lib/bybit/rest_api/p2p_service.rb create mode 100644 lib/bybit/rest_api/position_service.rb create mode 100644 lib/bybit/rest_api/rfq_service.rb create mode 100644 lib/bybit/rest_api/spot_margin_service.rb create mode 100644 lib/bybit/rest_api/trade_service.rb create mode 100644 lib/bybit/rest_api/user_service.rb create mode 100644 lib/bybit/session.rb create mode 100644 lib/bybit/utils/wire_keys.rb create mode 100644 lib/bybit/version.rb create mode 100644 spec/bybit_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bb1721 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/.bundle/ +/vendor/bundle/ +/vendor/ +/coverage/ +/pkg/ +/doc/ +/tmp/ +/log/ +Gemfile.lock +*.gem +.DS_Store +.env +.env.local +.idea/ +.vscode/ diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..7a2cc1a --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..c354d40 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,23 @@ +AllCops: + TargetRubyVersion: 3.0 + NewCops: disable + SuggestExtensions: false + Exclude: + - 'vendor/**/*' + - 'spec/**/*' + - 'examples/**/*' + - 'bin/**/*' +Style/Documentation: + Enabled: false +Metrics/MethodLength: + Max: 40 +Metrics/ClassLength: + Max: 2500 +Metrics/AbcSize: + Max: 40 +Metrics/ParameterLists: + Max: 12 +Metrics/BlockLength: + Enabled: false +Naming/AccessorMethodName: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..4a36342 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.0 diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..3d024da --- /dev/null +++ b/.yardopts @@ -0,0 +1,6 @@ +--markup markdown +--protected +--no-private +- +CHANGELOG.md +LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..513aded --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,258 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.1.0 — 2026-07-20 + +### Added +- `AccountService#batch_set_collateral(...)` +- `AccountService#get_info(...)` +- `AccountService#get_instruments(...)` +- `AccountService#get_borrow_history(...)` +- `AccountService#get_collateral_info(...)` +- `AccountService#get_dcp_info(...)` +- `AccountService#get_fee_rate(...)` +- `AccountService#get_mmp_state(...)` +- `AccountService#get_smp_group(...)` +- `AccountService#get_transaction_log(...)` +- `AccountService#get_transferable_amount(...)` +- `AccountService#get_user_settings(...)` +- `AccountService#manual_borrow(...)` +- `AccountService#manual_repay(...)` +- `AccountService#no_convert_repay(...)` +- `AccountService#one_click_repay(...)` +- `AccountService#reset_mmp(...)` +- `AccountService#set_collateral_coin(...)` +- `AccountService#set_margin_mode(...)` +- `AccountService#set_mmp(...)` +- `AccountService#set_price_limit(...)` +- `AccountService#set_spot_hedging(...)` +- `AccountService#upgrade_to_uta_pro(...)` +- `AffiliateService#get_sub_list(...)` +- `AffiliateService#get_user_list(...)` +- `AssetService#get_coin_balance(...)` +- `AssetService#get_coin_greeks(...)` +- `AssetService#query_funding_detail(...)` +- `BrokerService#distribute_award(...)` +- `BrokerService#get_award_info(...)` +- `BrokerService#get_distribution_record(...)` +- `BrokerService#query_account_info(...)` +- `BrokerService#query_all_uid_details(...)` +- `BrokerService#query_cap(...)` +- `BrokerService#query_earning(...)` +- `BrokerService#set_api_limit(...)` +- `CryptoLoanService#adjust_ltv(...)` +- `CryptoLoanService#get_adjustment_history(...)` +- `CryptoLoanService#get_collateral_data(...)` +- `CryptoLoanService#get_loanable_data(...)` +- `CryptoLoanService#get_max_collateral_amount(...)` +- `CryptoLoanService#get_max_loan(...)` +- `CryptoLoanService#get_position(...)` +- `CryptoLoanService#get_fixed_borrow_contract_info(...)` +- `CryptoLoanService#get_fixed_borrow_order_info(...)` +- `CryptoLoanService#get_fixed_borrow_order_quote(...)` +- `CryptoLoanService#get_fixed_renew_info(...)` +- `CryptoLoanService#get_fixed_supply_contract_info(...)` +- `CryptoLoanService#get_fixed_supply_order_info(...)` +- `CryptoLoanService#get_fixed_supply_order_quote(...)` +- `CryptoLoanService#borrow_fixed(...)` +- `CryptoLoanService#cancel_fixed_borrow_order(...)` +- `CryptoLoanService#repay_fixed_fully(...)` +- `CryptoLoanService#renew_fixed(...)` +- `CryptoLoanService#post_crypto_loan_fixed_repay_collateral(...)` +- `CryptoLoanService#cancel_fixed_supply_order(...)` +- `CryptoLoanService#get_flexible_borrow_history(...)` +- `CryptoLoanService#get_flexible_ongoing_coin(...)` +- `CryptoLoanService#get_flexible_repayment_history(...)` +- `CryptoLoanService#borrow_flexible(...)` +- `CryptoLoanService#repay_flexible(...)` +- `CryptoLoanService#post_crypto_loan_flexible_repay_collateral(...)` +- `BotService#close_dca_bot(...)` +- `BotService#create_dca_bot(...)` +- `EarnService#add_liquidity(...)` +- `EarnService#add_margin(...)` +- `EarnService#claim_liquidity_interest(...)` +- `EarnService#get_advance_earn_order(...)` +- `EarnService#get_advance_earn_position(...)` +- `EarnService#get_advance_earn_product(...)` +- `EarnService#get_advance_earn_product_extra_info(...)` +- `EarnService#get_double_win_leverage(...)` +- `EarnService#get_apr_history(...)` +- `EarnService#get_hourly_yield_history(...)` +- `EarnService#get_order_history(...)` +- `EarnService#get_position_get(...)` +- `EarnService#get_product(...)` +- `EarnService#get_yield_history(...)` +- `EarnService#get_fixed_term_order(...)` +- `EarnService#get_fixed_term_position(...)` +- `EarnService#get_fixed_term_product(...)` +- `EarnService#get_hold_to_earn_product(...)` +- `EarnService#get_hold_to_earn_yield_history(...)` +- `EarnService#get_liquidity_mining_liquidation_records(...)` +- `EarnService#get_liquidity_mining_orders(...)` +- `EarnService#get_liquidity_mining_positions(...)` +- `EarnService#get_liquidity_mining_products(...)` +- `EarnService#get_liquidity_mining_yield_records(...)` +- `EarnService#get_rwa_nav_chart(...)` +- `EarnService#get_rwa_order_list(...)` +- `EarnService#get_rwa_position_list(...)` +- `EarnService#get_rwa_product_list(...)` +- `EarnService#get_smart_leverage_redeem_est_amount_list(...)` +- `EarnService#get_token_daily_yield(...)` +- `EarnService#get_token_historical_apr(...)` +- `EarnService#get_token_hourly_yield(...)` +- `EarnService#get_token_order_list(...)` +- `EarnService#get_token_position(...)` +- `EarnService#get_token_product(...)` +- `EarnService#list_coupons(...)` +- `EarnService#modify_earn_position(...)` +- `EarnService#place_advance_earn_order(...)` +- `EarnService#place_order(...)` +- `EarnService#place_fixed_term_order(...)` +- `EarnService#place_rwa_order(...)` +- `EarnService#place_token_order(...)` +- `EarnService#pwm_asset_trend(...)` +- `EarnService#pwm_claim(...)` +- `EarnService#pwm_create_custom_plan(...)` +- `EarnService#pwm_fund_nav(...)` +- `EarnService#pwm_fund_transfer(...)` +- `EarnService#pwm_get_new_plan_detail(...)` +- `EarnService#pwm_get_plan_detail(...)` +- `EarnService#pwm_inst_create_fund(...)` +- `EarnService#pwm_inst_create_investment_plan(...)` +- `EarnService#pwm_inst_create_sub_account(...)` +- `EarnService#pwm_inst_get_investment_plans(...)` +- `EarnService#pwm_inst_list_funds(...)` +- `EarnService#pwm_inst_list_orders(...)` +- `EarnService#pwm_inst_manage_investment_plan(...)` +- `EarnService#pwm_inst_manage_order(...)` +- `EarnService#pwm_inst_settle_profit(...)` +- `EarnService#pwm_invest_more(...)` +- `EarnService#pwm_list_investment_plans(...)` +- `EarnService#pwm_list_order(...)` +- `EarnService#pwm_list_product_cards(...)` +- `EarnService#pwm_query_fund_transfer_result(...)` +- `EarnService#pwm_redeem(...)` +- `EarnService#pwm_subscribe(...)` +- `EarnService#redeem_fixed_term(...)` +- `EarnService#reinvest_liquidity(...)` +- `EarnService#remove_liquidity(...)` +- `EarnService#set_fixed_term_auto_invest(...)` +- `TradeService#get_history(...)` +- `BotService#close_combo_bot(...)` +- `BotService#create_combo_bot(...)` +- `BotService#get_combo_detail(...)` +- `BotService#get_combo_limit(...)` +- `BotService#close_futures_grid_bot(...)` +- `BotService#create_futures_grid_bot(...)` +- `BotService#get_futures_grid_detail(...)` +- `BotService#validate_futures_grid_input(...)` +- `BotService#close_futures_martingale_bot(...)` +- `BotService#create_futures_martingale_bot(...)` +- `BotService#get_futures_martingale_detail(...)` +- `BotService#get_futures_martingale_limit(...)` +- `BotService#close_grid_bot(...)` +- `BotService#create_grid_bot(...)` +- `BotService#query_grid_detail(...)` +- `BotService#validate_grid_input(...)` +- `MarketService#get_adl_alert(...)` +- `MarketService#get_delivery_price(...)` +- `MarketService#get_fee_group_info(...)` +- `MarketService#get_funding_rate_history(...)` +- `MarketService#get_historical_volatility(...)` +- `MarketService#get_index_price_components(...)` +- `MarketService#get_index_price_kline(...)` +- `MarketService#get_instruments_info(...)` +- `MarketService#get_insurance_pool(...)` +- `MarketService#get_long_short_ratio(...)` +- `MarketService#get_mark_price_kline(...)` +- `MarketService#get_kline(...)` +- `MarketService#get_new_delivery_price(...)` +- `MarketService#get_open_interest(...)` +- `MarketService#get_order_price_limit(...)` +- `MarketService#get_orderbook(...)` +- `MarketService#get_premium_index_price_kline(...)` +- `MarketService#get_recent_public_trades(...)` +- `MarketService#get_risk_limit(...)` +- `MarketService#get_rpi_orderbook(...)` +- `MarketService#get_server_time(...)` +- `MarketService#get_tickers(...)` +- `TradeService#amend_order(...)` +- `TradeService#batch_amend_orders(...)` +- `TradeService#batch_cancel_orders(...)` +- `TradeService#batch_create_orders(...)` +- `TradeService#cancel_all_orders(...)` +- `TradeService#cancel_order(...)` +- `TradeService#create_order(...)` +- `TradeService#dcp_set_timewindow(...)` +- `TradeService#get_open_orders(...)` +- `TradeService#get_order_history_get(...)` +- `TradeService#get_spot_borrow_quota(...)` +- `TradeService#pre_check_order(...)` +- `P2pService#get_account_info(...)` +- `P2pService#get_ads(...)` +- `P2pService#get_all_orders(...)` +- `P2pService#get_chat_messages(...)` +- `P2pService#get_counterparty_user_info(...)` +- `P2pService#get_my_ad_details(...)` +- `P2pService#get_my_ads(...)` +- `P2pService#get_order_detail(...)` +- `P2pService#get_pending_orders(...)` +- `P2pService#get_user_payment(...)` +- `P2pService#mark_order_as_paid(...)` +- `P2pService#post_ad(...)` +- `P2pService#release_assets(...)` +- `P2pService#remove_ad(...)` +- `P2pService#send_chat_message(...)` +- `P2pService#update_ad(...)` +- `P2pService#upload_chat_file(...)` +- `PositionService#add_reduce_margin(...)` +- `PositionService#confirm_new_risk_limit(...)` +- `PositionService#get_close_position(...)` +- `PositionService#get_closed_pnl(...)` +- `PositionService#get_move_position_history(...)` +- `PositionService#get_info_get(...)` +- `PositionService#move_position(...)` +- `PositionService#set_auto_add_margin(...)` +- `PositionService#set_leverage(...)` +- `PositionService#set_trading_stop(...)` +- `PositionService#switch_position_mode(...)` +- `RfqService#accept_non_lp_quote(...)` +- `RfqService#cancel_all_quotes(...)` +- `RfqService#cancel_all_rfqs(...)` +- `RfqService#cancel_quote(...)` +- `RfqService#cancel_rfq(...)` +- `RfqService#create_quote(...)` +- `RfqService#create_rfq(...)` +- `RfqService#execute_quote(...)` +- `RfqService#get_public_trades(...)` +- `RfqService#get_quotes(...)` +- `RfqService#get_quotes_realtime(...)` +- `RfqService#get_config(...)` +- `RfqService#get_rfqs(...)` +- `RfqService#get_rfqs_realtime(...)` +- `RfqService#get_trade_history(...)` +- `SpotMarginService#get_historical_interest_rate(...)` +- `SpotMarginService#get_position_tiers(...)` +- `SpotMarginService#get_tiered_collateral_ratio(...)` +- `SpotMarginService#get_vip_margin_data(...)` +- `UserService#create_sub_api_key(...)` +- `UserService#create_sub_member(...)` +- `UserService#delete_api_key(...)` +- `UserService#delete_sub_api_key(...)` +- `UserService#delete_sub_member_v5(...)` +- `UserService#frozen_sub_member(...)` +- `UserService#get_affiliate_custom_open_info_v5(...)` +- `UserService#get_member_account_type(...)` +- `UserService#list_sub_api_keys_v5(...)` +- `UserService#query_api_key(...)` +- `UserService#query_escrow_sub_members_v5(...)` +- `UserService#query_referrals(...)` +- `UserService#query_sub_members(...)` +- `UserService#query_sub_members_v5(...)` +- `UserService#sign_agreement(...)` +- `UserService#update_api_key(...)` +- `UserService#update_sub_api_key(...)` diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..fa75df1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gemspec diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d9ebf5b --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' +require 'rubocop/rake_task' + +RSpec::Core::RakeTask.new(:spec) +RuboCop::RakeTask.new + +task default: %i[rubocop spec] diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..d437c9f --- /dev/null +++ b/bin/console @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'bybit' +require 'irb' + +IRB.start(__FILE__) diff --git a/bybit-connector-ruby.gemspec b/bybit-connector-ruby.gemspec new file mode 100644 index 0000000..f27a075 --- /dev/null +++ b/bybit-connector-ruby.gemspec @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative 'lib/bybit/version' + +Gem::Specification.new do |s| + s.name = 'bybit-connector-ruby' + s.version = Bybit::VERSION + s.summary = 'Official Bybit V5 REST API connector for Ruby' + s.description = 'Ruby SDK for the Bybit V5 REST API with typed keyword-arg method signatures.' + s.authors = ['Bybit'] + s.license = 'MIT' + s.homepage = 'https://github.com/bybit-exchange/bybit.ruby.api' + s.required_ruby_version = '>= 3.0' + s.files = Dir['lib/**/*.rb', 'LICENSE*', 'README.md', 'CHANGELOG.md'] + s.require_paths = ['lib'] + s.metadata = { + 'source_code_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api', + 'documentation_uri' => 'https://bybit-exchange.github.io/docs/v5/intro', + 'changelog_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api/blob/main/CHANGELOG.md', + 'rubygems_mfa_required' => 'true' + } + s.add_dependency 'faraday', '~> 2.0' + s.add_development_dependency 'rspec', '~> 3.12' + s.add_development_dependency 'rubocop', '~> 1.60' + s.add_development_dependency 'yard', '~> 0.9' +end diff --git a/examples/quickstart.rb b/examples/quickstart.rb new file mode 100644 index 0000000..caf4688 --- /dev/null +++ b/examples/quickstart.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bybit' + +Bybit.configure do |c| + c.api_key = ENV['BYBIT_KEY'] + c.api_secret = ENV['BYBIT_SECRET'] + c.testnet = true +end + +client = Bybit::Client.new +puts client.market.get_server_time.inspect diff --git a/lib/bybit.rb b/lib/bybit.rb new file mode 100644 index 0000000..113a4cf --- /dev/null +++ b/lib/bybit.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'bybit/version' +require 'bybit/error' +require 'bybit/configuration' +require 'bybit/authentication' +require 'bybit/session' +require 'bybit/utils/wire_keys' +require 'bybit/rest_api/base_service' +require 'bybit/client' diff --git a/lib/bybit/authentication.rb b/lib/bybit/authentication.rb new file mode 100644 index 0000000..abf779c --- /dev/null +++ b/lib/bybit/authentication.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'openssl' + +module Bybit + # HMAC-SHA256 signer for the Bybit V5 REST API. + # Payload format: timestamp + api_key + recv_window + (query_string OR request_body). + module Authentication + module_function + + def sign_v5(api_secret, timestamp, api_key, recv_window, payload) + msg = "#{timestamp}#{api_key}#{recv_window}#{payload}" + OpenSSL::HMAC.hexdigest('SHA256', api_secret, msg) + end + end +end diff --git a/lib/bybit/client.rb b/lib/bybit/client.rb new file mode 100644 index 0000000..f3c94c0 --- /dev/null +++ b/lib/bybit/client.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# gen-sdk-ruby:service-requires:start +require 'bybit/rest_api/account_service' +require 'bybit/rest_api/affiliate_service' +require 'bybit/rest_api/asset_service' +require 'bybit/rest_api/bot_service' +require 'bybit/rest_api/broker_service' +require 'bybit/rest_api/crypto_loan_service' +require 'bybit/rest_api/earn_service' +require 'bybit/rest_api/market_service' +require 'bybit/rest_api/p2p_service' +require 'bybit/rest_api/position_service' +require 'bybit/rest_api/rfq_service' +require 'bybit/rest_api/spot_margin_service' +require 'bybit/rest_api/trade_service' +require 'bybit/rest_api/user_service' +# gen-sdk-ruby:service-requires:end + +module Bybit + # Global configuration block — Bybit.configure { |c| c.api_key = ... } + class << self + attr_writer :configuration + def configuration + @configuration ||= Configuration.new + end + def configure + yield configuration + end + end + + class Client + # gen-sdk-ruby:client-readers:start + attr_reader :account + attr_reader :affiliate + attr_reader :asset + attr_reader :bot + attr_reader :broker + attr_reader :crypto_loan + attr_reader :earn + attr_reader :market + attr_reader :p2p + attr_reader :position + attr_reader :rfq + attr_reader :spot_margin + attr_reader :trade + attr_reader :user + # gen-sdk-ruby:client-readers:end + + def initialize(config = nil, **overrides) + cfg = (config || Bybit.configuration).dup + overrides.each { |k, v| cfg.public_send("#{k}=", v) } + session = Session.new(cfg) + @config = cfg + + # gen-sdk-ruby:client-inits:start + @account = RestApi::AccountService.new(session) + @affiliate = RestApi::AffiliateService.new(session) + @asset = RestApi::AssetService.new(session) + @bot = RestApi::BotService.new(session) + @broker = RestApi::BrokerService.new(session) + @crypto_loan = RestApi::CryptoLoanService.new(session) + @earn = RestApi::EarnService.new(session) + @market = RestApi::MarketService.new(session) + @p2p = RestApi::P2pService.new(session) + @position = RestApi::PositionService.new(session) + @rfq = RestApi::RfqService.new(session) + @spot_margin = RestApi::SpotMarginService.new(session) + @trade = RestApi::TradeService.new(session) + @user = RestApi::UserService.new(session) + # gen-sdk-ruby:client-inits:end + end + + # Redacted #inspect so console printing doesn't leak the secret. + def inspect + "#" + end + alias_method :to_s, :inspect + end +end diff --git a/lib/bybit/configuration.rb b/lib/bybit/configuration.rb new file mode 100644 index 0000000..a8b3ac5 --- /dev/null +++ b/lib/bybit/configuration.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bybit + BASE_URL_MAINNET = 'https://api.bybit.com' + BASE_URL_TESTNET = 'https://api-testnet.bybit.com' + DEFAULT_RECV_WINDOW = '5000' + DEFAULT_TIMEOUT = 10 + + class Configuration + attr_accessor :api_key, :api_secret, :testnet, :base_url, + :recv_window, :timeout, :faraday_connection + + def initialize + @testnet = false + @recv_window = DEFAULT_RECV_WINDOW + @timeout = DEFAULT_TIMEOUT + end + + def resolved_base_url + base_url || (testnet ? BASE_URL_TESTNET : BASE_URL_MAINNET) + end + + # Redact credentials from #inspect and #to_s so a stray puts/logger call + # doesn't leak the secret into log aggregation. + def inspect + "#" + end + alias_method :to_s, :inspect + + private + + def redact(v) + v.nil? || v.empty? ? '(unset)' : '[REDACTED]' + end + end +end diff --git a/lib/bybit/error.rb b/lib/bybit/error.rb new file mode 100644 index 0000000..b0b7c2b --- /dev/null +++ b/lib/bybit/error.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Bybit + class Error < StandardError; end + + # Raised when the server returns HTTP 200 + retCode != 0 (the V5 norm), + # or when a transport-level error is enriched with a Bybit body payload. + class ApiError < Error + attr_reader :ret_code, :ret_msg, :result, :time, :http_status + + def initialize(response, http_status: nil) + @ret_code = response['retCode'] + @ret_msg = response['retMsg'] + @result = response['result'] + @time = response['time'] + @http_status = http_status + super("[#{@ret_code}] #{@ret_msg}") + end + end + + class AuthError < ApiError; end + class RateLimitError < ApiError; end + class TimeoutError < Error; end + class NetworkError < Error; end + + class ParseError < Error + attr_reader :body, :http_status + def initialize(message, body: nil, http_status: nil) + @body = body + @http_status = http_status + super(message) + end + end + + AUTH_RET_CODES = [10002, 10003, 10004, 10005, 10007, 10009, 10010, 10029].freeze + RATE_LIMIT_RET_CODES = [10006, 10018].freeze + + # Route a V5 response with retCode != 0 to the most specific error subclass. + def self.api_error_from(response, http_status: nil) + code = response['retCode'] + return AuthError.new(response, http_status: http_status) if AUTH_RET_CODES.include?(code) + return RateLimitError.new(response, http_status: http_status) if RATE_LIMIT_RET_CODES.include?(code) + ApiError.new(response, http_status: http_status) + end +end diff --git a/lib/bybit/rest_api/account_service.rb b/lib/bybit/rest_api/account_service.rb new file mode 100644 index 0000000..7aeac61 --- /dev/null +++ b/lib/bybit/rest_api/account_service.rb @@ -0,0 +1,295 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class AccountService < BaseService + # Batch Set Collateral + # + # POST /v5/account/set-collateral-switch-batch + # + # @param request [Array] Array of collateral switch objects + # @see https://bybit-exchange.github.io/docs/v5/account/batch-set-collateral + def batch_set_collateral(request:, **kwargs) + params = kwargs.merge(request: request) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/set-collateral-switch-batch', body: params) + end + + # Get Account Info + # + # GET /v5/account/info + # + # @see https://bybit-exchange.github.io/docs/v5/account/account-info + def get_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/info', params: params) + end + + # Get Account Instruments + # + # GET /v5/account/instruments-info + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + def get_instruments(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/instruments-info', params: params) + end + + # Get Borrow History + # + # GET /v5/account/borrow-history + # + # @option kwargs [String] :currency USDC, USDT, BTC, ETH + # @option kwargs [Integer] :start_time The start timestamp (ms) + # @option kwargs [Integer] :end_time The end timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + # @see https://bybit-exchange.github.io/docs/v5/account/borrow-history + def get_borrow_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/borrow-history', params: params) + end + + # Get Collateral Info + # + # GET /v5/account/collateral-info + # + # @option kwargs [String] :currency Asset currency of all current collateral + # @see https://bybit-exchange.github.io/docs/v5/account/collateral-info + def get_collateral_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/collateral-info', params: params) + end + + # Get DCP Info + # + # GET /v5/account/query-dcp-info + # + # @see https://bybit-exchange.github.io/docs/v5/account/dcp-info + def get_dcp_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/query-dcp-info', params: params) + end + + # Get Fee Rate + # + # GET /v5/account/fee-rate + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin. SOL, BTC, ETH. Apply to option only + # @see https://bybit-exchange.github.io/docs/v5/account/fee-rate + def get_fee_rate(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/fee-rate', params: params) + end + + # Get MMP State + # + # GET /v5/account/mmp-state + # + # @param base_coin [String] Base coin + # @see https://bybit-exchange.github.io/docs/v5/account/get-mmp-state + def get_mmp_state(base_coin:, **kwargs) + params = kwargs.merge(base_coin: base_coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/mmp-state', params: params) + end + + # Get SMP Group + # + # GET /v5/account/smp-group + # + # @see https://bybit-exchange.github.io/docs/v5/account/smp-group + def get_smp_group(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/smp-group', params: params) + end + + # Get Transaction Log + # + # GET /v5/account/transaction-log + # + # @option kwargs [String] :account_type Account type. UNIFIED + # @option kwargs [String] :category Product type + # @option kwargs [String] :currency Currency + # @option kwargs [String] :base_coin BaseCoin. e.g., BTC of BTCPERP + # @option kwargs [String] :type Types of transaction logs + # @option kwargs [String] :trans_sub_type Transaction subtype + # @option kwargs [Integer] :start_time The start timestamp (ms) + # @option kwargs [Integer] :end_time The end timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + # @see https://bybit-exchange.github.io/docs/v5/account/transaction-log + def get_transaction_log(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/transaction-log', params: params) + end + + # Get Transferable Amount + # + # GET /v5/account/withdrawal + # + # @param coin_name [String] Coin name + def get_transferable_amount(coin_name:, **kwargs) + params = kwargs.merge(coin_name: coin_name) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/withdrawal', params: params) + end + + # Get User Settings + # + # GET /v5/account/user-setting-config + def get_user_settings(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/user-setting-config', params: params) + end + + # Manual Borrow + # + # POST /v5/account/borrow + # + # @param coin [String] Coin name + # @param amount [String] The amount to borrow + def manual_borrow(coin:, amount:, **kwargs) + params = kwargs.merge(coin: coin, amount: amount) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/borrow', body: params) + end + + # Manual Repay + # + # POST /v5/account/repay + # + # @option kwargs [String] :coin Coin name. If not passed, repay all liabilities + # @option kwargs [String] :amount The amount to repay + def manual_repay(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/repay', body: params) + end + + # No-Convert Repay + # + # POST /v5/account/no-convert-repay + # + # @param coin [String] Coin name + # @option kwargs [String] :amount The amount to repay + # @see https://bybit-exchange.github.io/docs/v5/account/no-convert-repay + def no_convert_repay(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/no-convert-repay', body: params) + end + + # One-Click Repay: convert small balance coins to a specified coin to repay debt in one click. + # + # POST /v5/account/quick-repayment + # + # @option kwargs [String] :coin Coin to repay debt with + def one_click_repay(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/quick-repayment', body: params) + end + + # Reset MMP state for a given base coin. + # + # POST /v5/account/mmp-reset + # + # @param base_coin [String] Base coin + def reset_mmp(base_coin:, **kwargs) + params = kwargs.merge(base_coin: base_coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/mmp-reset', body: params) + end + + # Enable or disable a coin as collateral for the unified account. + # + # POST /v5/account/set-collateral-switch + # + # @param coin [String] Coin symbol + # @param collateral_switch [String] ON or OFF + def set_collateral_coin(coin:, collateral_switch:, **kwargs) + params = kwargs.merge(coin: coin, collateral_switch: collateral_switch) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/set-collateral-switch', body: params) + end + + # Set margin mode for the unified account (ISOLATED_MARGIN, REGULAR_MARGIN, PORTFOLIO_MARGIN). + # + # POST /v5/account/set-margin-mode + # + # @param set_margin_mode [String] Margin mode to set + # @see https://bybit-exchange.github.io/docs/v5/account/set-margin-mode + def set_margin_mode(set_margin_mode:, **kwargs) + params = kwargs.merge(set_margin_mode: set_margin_mode) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/set-margin-mode', body: params) + end + + # Configure Market Maker Protection parameters for a base coin. + # + # POST /v5/account/mmp-modify + # + # @param base_coin [String] Base coin + # @param window [String] Time window (ms) + # @param frozen_period [String] Frozen period (ms) + # @param qty_limit [String] Quantity limit + # @param delta_limit [String] Delta limit + # @see https://bybit-exchange.github.io/docs/v5/account/set-mmp + def set_mmp(base_coin:, window:, frozen_period:, qty_limit:, delta_limit:, **kwargs) + params = kwargs.merge(base_coin: base_coin, window: window, frozen_period: frozen_period, qty_limit: qty_limit, delta_limit: delta_limit) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/mmp-modify', body: params) + end + + # Toggle whether the price limit rule applies to modify order actions for a category. + # + # POST /v5/account/set-limit-px-action + # + # @param category [String] Product type + # @param modify_enable [Boolean] Enable modify price limit check + def set_price_limit(category:, modify_enable:, **kwargs) + params = kwargs.merge(category: category, modify_enable: modify_enable) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/set-limit-px-action', body: params) + end + + # Enable or disable spot hedging mode for the unified account. + # + # POST /v5/account/set-hedging-mode + # + # @param set_hedging_mode [String] ON or OFF + # @see https://bybit-exchange.github.io/docs/v5/account/set-spot-hedge + def set_spot_hedging(set_hedging_mode:, **kwargs) + params = kwargs.merge(set_hedging_mode: set_hedging_mode) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/set-hedging-mode', body: params) + end + + # Upgrade the current account to Unified Trading Account (UTA) Pro. + # + # POST /v5/account/upgrade-to-uta + # + # @see https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account + def upgrade_to_uta_pro(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/account/upgrade-to-uta', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/affiliate_service.rb b/lib/bybit/rest_api/affiliate_service.rb new file mode 100644 index 0000000..0994a75 --- /dev/null +++ b/lib/bybit/rest_api/affiliate_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class AffiliateService < BaseService + # Get affiliate sub-affiliate list + # + # GET /v5/affiliate/affiliate-sub-list + # + # @option kwargs [String] :cursor Cursor for pagination + # @option kwargs [Integer] :size Page size + # @option kwargs [String] :start_date Start date filter + # @option kwargs [String] :end_date End date filter + # @option kwargs [Integer] :sub_aff_id Sub-affiliate ID + # @see https://bybit-exchange.github.io/docs/v5/affiliate/affiliate-sub-list + def get_sub_list(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/affiliate/affiliate-sub-list', params: params) + end + + # Get affiliate user list + # + # GET /v5/affiliate/aff-user-list + # + # @option kwargs [String] :cursor Cursor for pagination + # @option kwargs [Integer] :size Page size + # @option kwargs [Boolean] :need_deposit Whether to include deposit information + # @option kwargs [Boolean] :need30 Whether to include 30-day statistics + # @option kwargs [Boolean] :need365 Whether to include 365-day statistics + # @option kwargs [String] :start_date Start date filter + # @option kwargs [String] :end_date End date filter + def get_user_list(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/affiliate/aff-user-list', params: params) + end + end + end +end diff --git a/lib/bybit/rest_api/asset_service.rb b/lib/bybit/rest_api/asset_service.rb new file mode 100644 index 0000000..2b179b7 --- /dev/null +++ b/lib/bybit/rest_api/asset_service.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class AssetService < BaseService + # Get Coin Balance + # + # GET /v5/asset/transfer/query-account-coins-balance + # + # @param account_type [String] Account type + # @option kwargs [String] :member_id User ID. Required when querying sub UID balance with master API key + # @option kwargs [String] :coin Coin name(s). Multiple coins separated by comma. If not passed, returns all coins + # @option kwargs [Integer] :with_bonus 0(default): not include bonus; 1: include bonus + def get_coin_balance(account_type:, **kwargs) + params = kwargs.merge(account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-account-coins-balance', params: params) + end + + # Get Coin Greeks + # + # GET /v5/asset/coin-greeks + # + # @option kwargs [String] :base_coin Base coin. Default: return all your coins greek data + def get_coin_greeks(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/coin-greeks', params: params) + end + + # Get Funding History + # + # GET /v5/asset/fundinghistory + # + # @option kwargs [String] :create_time_from Start timestamp (ms) + # @option kwargs [String] :create_time_to End timestamp (ms) + # @option kwargs [String] :limit Limit for data size per page. [1, 50]. Default: 20 + # @option kwargs [String] :cursor Cursor. Used for pagination + def query_funding_detail(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/fundinghistory', params: params) + end + end + end +end diff --git a/lib/bybit/rest_api/base_service.rb b/lib/bybit/rest_api/base_service.rb new file mode 100644 index 0000000..8f3d18f --- /dev/null +++ b/lib/bybit/rest_api/base_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class BaseService + def initialize(session) + @session = session + end + end + end +end diff --git a/lib/bybit/rest_api/bot_service.rb b/lib/bybit/rest_api/bot_service.rb new file mode 100644 index 0000000..fc3c4cc --- /dev/null +++ b/lib/bybit/rest_api/bot_service.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class BotService < BaseService + # Close a running DCA bot with a specified settlement mode + # + # POST /v5/dca/close-bot + # + # @param bot_id [Integer] Identifier of the DCA bot to close + # @param close_mode [Integer] Settlement mode used to close the bot + def close_dca_bot(bot_id:, close_mode:, **kwargs) + body = kwargs.merge(bot_id: bot_id, close_mode: close_mode) + @session.sign_request(method: :post, path: '/v5/dca/close-bot', body: body) + end + + # Create a new DCA (Dollar-Cost Averaging) bot with custom parameters + # + # POST /v5/dca/create-bot + # + # @param parameters [Hash] DCA bot configuration parameters + # @option kwargs [Hash] :tools_discovery_parameter Tools discovery parameter object + # @option kwargs [String] :channel Channel identifier + def create_dca_bot(parameters:, **kwargs) + body = kwargs.merge(parameters: parameters) + @session.sign_request(method: :post, path: '/v5/dca/create-bot', body: body) + end + + # Close a running futures combo bot by bot ID + # + # POST /v5/fcombobot/close + # + # @param bot_id [Integer] Combo bot ID to close + # @option kwargs [Integer] :stop_type Stop type identifier + def close_combo_bot(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fcombobot/close', body: body) + end + + # Create a new futures combo bot with multi-symbol portfolio and rebalancing + # + # POST /v5/fcombobot/create + # + # @param leverage [String] Leverage setting for the combo bot + # @param init_margin [String] Initial margin for the combo bot + # @param adjust_position_mode [Integer] Position adjustment mode + # @param symbol_settings [Array] Per-symbol configuration settings for the combo bot + # @option kwargs [String] :adjust_position_percent Position adjustment percent + # @option kwargs [Integer] :adjust_position_time_interval Position adjustment time interval + # @option kwargs [String] :sl_percent Stop loss percent + # @option kwargs [String] :tp_percent Take profit percent + # @option kwargs [Integer] :source Source identifier + # @option kwargs [Integer] :block_source Block source identifier + # @option kwargs [Integer] :create_type Bot creation type + # @option kwargs [Integer] :followed_bot_id ID of the bot being followed + # @option kwargs [String] :init_bonus Initial bonus amount + # @option kwargs [String] :trailing_stop_percent Trailing stop percent + # @option kwargs [String] :channel Channel identifier + def create_combo_bot(leverage:, init_margin:, adjust_position_mode:, symbol_settings:, **kwargs) + body = kwargs.merge( + leverage: leverage, + init_margin: init_margin, + adjust_position_mode: adjust_position_mode, + symbol_settings: symbol_settings + ) + @session.sign_request(method: :post, path: '/v5/fcombobot/create', body: body) + end + + # Get full details of a futures combo bot including PnL, positions, and status + # + # POST /v5/fcombobot/detail + # + # @param bot_id [Integer] Combo bot ID + def get_combo_detail(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fcombobot/detail', body: body) + end + + # Validate combo bot input parameters and return allowable ranges + # + # POST /v5/fcombobot/getlimit + # + # @param leverage [String] Leverage setting + # @param init_margin [String] Initial margin + # @param adjust_position_mode [Integer] Position adjustment mode + # @param symbol_settings [Array] Per-symbol configuration settings + # @option kwargs [String] :adjust_position_percent Position adjustment percent + # @option kwargs [Integer] :adjust_position_time_interval Position adjustment time interval + # @option kwargs [String] :sl_percent Stop loss percent + # @option kwargs [String] :tp_percent Take profit percent + # @option kwargs [Boolean] :need_to_slippage Whether to include slippage in validation + # @option kwargs [String] :app_name Application name identifier + # @option kwargs [String] :trailing_stop_percent Trailing stop percent + def get_combo_limit(leverage:, init_margin:, adjust_position_mode:, symbol_settings:, **kwargs) + body = kwargs.merge( + leverage: leverage, + init_margin: init_margin, + adjust_position_mode: adjust_position_mode, + symbol_settings: symbol_settings + ) + @session.sign_request(method: :post, path: '/v5/fcombobot/getlimit', body: body) + end + + # Close a running futures grid bot by bot ID + # + # POST /v5/fgridbot/close + # + # @param bot_id [Integer] Bot ID of the futures grid bot to close + def close_futures_grid_bot(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fgridbot/close', body: body) + end + + # Create a new futures grid trading bot with specified parameters + # + # POST /v5/fgridbot/create + # + # @param symbol [String] Trading symbol + # @param grid_mode [Integer] Grid mode selector + # @param min_price [String] Lower bound of the grid price range + # @param max_price [String] Upper bound of the grid price range + # @param cell_number [Integer] Number of grid cells + # @param leverage [String] Leverage to use for the grid bot + # @param grid_type [Integer] Grid type (arithmetic/geometric) + # @param total_investment [String] Total investment amount + # @option kwargs [String] :take_profit_per Take profit percentage + # @option kwargs [String] :stop_loss_per Stop loss percentage + # @option kwargs [String] :entry_price Entry price for the bot + # @option kwargs [Integer] :source Source identifier + # @option kwargs [Integer] :followed_grid_id ID of the grid bot being copied + # @option kwargs [Hash] :tools_discovery_parameter Tools discovery parameter payload + # @option kwargs [String] :stop_loss_price Stop loss trigger price + # @option kwargs [String] :take_profit_price Take profit trigger price + # @option kwargs [Integer] :tp_sl_type Take-profit/stop-loss type + # @option kwargs [Integer] :block_source Block source identifier + # @option kwargs [Integer] :create_type Create type identifier + # @option kwargs [String] :init_bonus Initial bonus amount + # @option kwargs [String] :business_remark Business remark note + # @option kwargs [String] :trailing_stop_per Trailing stop percentage + # @option kwargs [String] :move_up_price Move-up trigger price + # @option kwargs [String] :move_down_price Move-down trigger price + # @option kwargs [String] :channel Client channel identifier + def create_futures_grid_bot(symbol:, grid_mode:, min_price:, max_price:, cell_number:, leverage:, grid_type:, total_investment:, **kwargs) + body = kwargs.merge( + symbol: symbol, + grid_mode: grid_mode, + min_price: min_price, + max_price: max_price, + cell_number: cell_number, + leverage: leverage, + grid_type: grid_type, + total_investment: total_investment + ) + @session.sign_request(method: :post, path: '/v5/fgridbot/create', body: body) + end + + # Get full details of a futures grid bot including PnL, positions, and status + # + # POST /v5/fgridbot/detail + # + # @param bot_id [Integer] Bot ID of the futures grid bot + def get_futures_grid_detail(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fgridbot/detail', body: body) + end + + # Validate futures grid bot input parameters and return allowable ranges + # + # POST /v5/fgridbot/validate + # + # @param symbol [String] Trading symbol + # @param cell_number [Integer] Number of grid cells + # @param min_price [String] Lower bound of the grid price range + # @param max_price [String] Upper bound of the grid price range + # @param leverage [String] Leverage to validate + # @param grid_type [Integer] Grid type (arithmetic/geometric) + # @param grid_mode [Integer] Grid mode selector + # @option kwargs [String] :stop_loss_price Stop loss trigger price + # @option kwargs [String] :take_profit_price Take profit trigger price + # @option kwargs [Integer] :tp_sl_type Take-profit/stop-loss type + # @option kwargs [String] :entry_price Entry price + # @option kwargs [String] :stop_loss_per Stop loss percentage + # @option kwargs [String] :take_profit_per Take profit percentage + # @option kwargs [String] :trailing_stop_per Trailing stop percentage + # @option kwargs [String] :init_margin Initial margin amount + # @option kwargs [String] :move_up_price Move-up trigger price + # @option kwargs [String] :move_down_price Move-down trigger price + def validate_futures_grid_input(symbol:, cell_number:, min_price:, max_price:, leverage:, grid_type:, grid_mode:, **kwargs) + body = kwargs.merge( + symbol: symbol, + cell_number: cell_number, + min_price: min_price, + max_price: max_price, + leverage: leverage, + grid_type: grid_type, + grid_mode: grid_mode + ) + @session.sign_request(method: :post, path: '/v5/fgridbot/validate', body: body) + end + + # Close a running futures Martingale bot by bot ID + # + # POST /v5/fmartingalebot/close + # + # @param bot_id [Integer] Bot ID + # @option kwargs [String] :stop_type Stop type + def close_futures_martingale_bot(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fmartingalebot/close', body: body) + end + + # Create a new futures Martingale bot with DCA averaging strategy + # + # POST /v5/fmartingalebot/create + # + # @param symbol [String] Trading symbol + # @param martingale_mode [String] Martingale mode + # @param leverage [String] Leverage + # @param price_float_percent [String] Price float percent triggering next add position + # @param add_position_percent [String] Add position percent per averaging step + # @param add_position_num [Integer] Number of add position steps + # @param init_margin [String] Initial margin + # @param round_tp_percent [String] Round take-profit percent + # @option kwargs [String] :auto_cycle_toggle Auto cycle toggle + # @option kwargs [String] :sl_percent Stop-loss percent + # @option kwargs [String] :entry_price Entry price + # @option kwargs [String] :source Source + # @option kwargs [Integer] :followed_bot_id Followed bot ID + # @option kwargs [String] :block_source Block source + # @option kwargs [String] :create_type Create type + # @option kwargs [String] :init_bonus Initial bonus + # @option kwargs [String] :channel Channel + def create_futures_martingale_bot(symbol:, martingale_mode:, leverage:, price_float_percent:, add_position_percent:, add_position_num:, init_margin:, round_tp_percent:, **kwargs) + body = kwargs.merge( + symbol: symbol, + martingale_mode: martingale_mode, + leverage: leverage, + price_float_percent: price_float_percent, + add_position_percent: add_position_percent, + add_position_num: add_position_num, + init_margin: init_margin, + round_tp_percent: round_tp_percent + ) + @session.sign_request(method: :post, path: '/v5/fmartingalebot/create', body: body) + end + + # Get full details of a futures Martingale bot including PnL, positions, and round progress + # + # POST /v5/fmartingalebot/detail + # + # @param bot_id [Integer] Bot ID + def get_futures_martingale_detail(bot_id:, **kwargs) + body = kwargs.merge(bot_id: bot_id) + @session.sign_request(method: :post, path: '/v5/fmartingalebot/detail', body: body) + end + + # Validate Martingale bot input parameters and return allowable ranges + # + # POST /v5/fmartingalebot/getlimit + # + # @param symbol [String] Trading symbol + # @param martingale_mode [String] Martingale mode + # @param leverage [String] Leverage + # @option kwargs [String] :price_float_percent Price float percent + # @option kwargs [String] :add_position_percent Add position percent + # @option kwargs [Integer] :add_position_num Number of add position steps + # @option kwargs [String] :init_margin Initial margin + # @option kwargs [String] :round_tp_percent Round take-profit percent + # @option kwargs [String] :sl_percent Stop-loss percent + # @option kwargs [String] :entry_price Entry price + # @option kwargs [Boolean] :need_to_slippage Whether to include slippage + # @option kwargs [String] :app_name App name + def get_futures_martingale_limit(symbol:, martingale_mode:, leverage:, **kwargs) + body = kwargs.merge( + symbol: symbol, + martingale_mode: martingale_mode, + leverage: leverage + ) + @session.sign_request(method: :post, path: '/v5/fmartingalebot/getlimit', body: body) + end + + # Close a running spot grid bot with a specified settlement mode + # + # POST /v5/grid/close-grid + # + # @param grid_id [Integer] Grid bot ID + # @param close_mode [Integer] Close/settlement mode + def close_grid_bot(grid_id:, close_mode:, **kwargs) + body = kwargs.merge(grid_id: grid_id, close_mode: close_mode) + @session.sign_request(method: :post, path: '/v5/grid/close-grid', body: body) + end + + # Create a new spot grid trading bot + # + # POST /v5/grid/create-grid + # + # @param symbol [String] Trading pair symbol + # @param max_price [String] Upper bound price of the grid + # @param min_price [String] Lower bound price of the grid + # @param total_investment [String] Total investment amount + # @param cell_number [Integer] Number of grid cells + # @option kwargs [Integer] :followed_grid_id ID of the grid bot being copied/followed + # @option kwargs [Integer] :source Creation source identifier + # @option kwargs [String] :entry_price Entry price for the bot + # @option kwargs [String] :stop_loss_price Stop loss price + # @option kwargs [String] :take_profit_price Take profit price + # @option kwargs [Hash] :tools_discovery_parameter Tools discovery parameter object + # @option kwargs [String] :base_investment Base asset investment amount + # @option kwargs [String] :quote_investment Quote asset investment amount + # @option kwargs [Integer] :invest_mode Investment mode + # @option kwargs [Integer] :block_source Block source identifier + # @option kwargs [Integer] :create_type Creation type + # @option kwargs [String] :ts_percent Trailing stop percent + # @option kwargs [Boolean] :enable_trailing Whether to enable trailing + # @option kwargs [String] :limit_up_price Upper limit price + # @option kwargs [String] :channel Channel identifier + def create_grid_bot(symbol:, max_price:, min_price:, total_investment:, cell_number:, **kwargs) + body = kwargs.merge( + symbol: symbol, + max_price: max_price, + min_price: min_price, + total_investment: total_investment, + cell_number: cell_number + ) + @session.sign_request(method: :post, path: '/v5/grid/create-grid', body: body) + end + + # Query full details of a specific grid bot by grid_id + # + # POST /v5/grid/query-grid-detail + # + # @param grid_id [Integer] Grid bot ID + def query_grid_detail(grid_id:, **kwargs) + body = kwargs.merge(grid_id: grid_id) + @session.sign_request(method: :post, path: '/v5/grid/query-grid-detail', body: body) + end + + # Validate spot grid bot parameters before creation + # + # POST /v5/grid/validate-input + # + # @param symbol [String] Trading pair symbol + # @param cell_number [Integer] Number of grid cells + # @param min_price [String] Lower bound price of the grid + # @param max_price [String] Upper bound price of the grid + # @param total_investment [String] Total investment amount + # @option kwargs [String] :stop_loss Stop loss price + # @option kwargs [String] :take_profit Take profit price + # @option kwargs [String] :entry_price Entry price for the bot + # @option kwargs [String] :base_investment Base asset investment amount + # @option kwargs [String] :quote_investment Quote asset investment amount + # @option kwargs [Integer] :invest_mode Investment mode + # @option kwargs [String] :ts_percent Trailing stop percent + # @option kwargs [Boolean] :enable_trailing Whether to enable trailing + # @option kwargs [String] :limit_up_price Upper limit price + def validate_grid_input(symbol:, cell_number:, min_price:, max_price:, total_investment:, **kwargs) + body = kwargs.merge( + symbol: symbol, + cell_number: cell_number, + min_price: min_price, + max_price: max_price, + total_investment: total_investment + ) + @session.sign_request(method: :post, path: '/v5/grid/validate-input', body: body) + end + end + end +end diff --git a/lib/bybit/rest_api/broker_service.rb b/lib/bybit/rest_api/broker_service.rb new file mode 100644 index 0000000..4bc39c2 --- /dev/null +++ b/lib/bybit/rest_api/broker_service.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class BrokerService < BaseService + # Distribute voucher + # + # POST /v5/broker/award/distribute-award + # + # @param account_id [String] Account ID + # @param award_id [String] Award ID + # @param spec_code [String] Spec code + # @param amount [String] Distribution amount + # @param broker_id [String] Broker ID + def distribute_award(account_id:, award_id:, spec_code:, amount:, broker_id:, **kwargs) + params = kwargs.merge(account_id: account_id, award_id: award_id, spec_code: spec_code, amount: amount, broker_id: broker_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/broker/award/distribute-award', body: params) + end + + # Get voucher details + # + # POST /v5/broker/award/info + # + # @param id [String] Voucher ID + def get_award_info(id:, **kwargs) + params = kwargs.merge(id: id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/broker/award/info', body: params) + end + + # Query voucher distribution record + # + # POST /v5/broker/award/distribution-record + # + # @param account_id [String] Account ID + # @param award_id [String] Award ID + # @param spec_code [String] Spec code + # @option kwargs [Boolean] :with_used_amount Include used amount flag + def get_distribution_record(account_id:, award_id:, spec_code:, **kwargs) + params = kwargs.merge(account_id: account_id, award_id: award_id, spec_code: spec_code) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/broker/award/distribution-record', body: params) + end + + # Get Broker Account Info + # + # GET /v5/broker/account-info + def query_account_info + @session.sign_request(method: :get, path: '/v5/broker/account-info') + end + + # Query Broker All UID Rate Limits + # + # GET /v5/broker/apilimit/query-all + # + # @option kwargs [String] :uids UIDs to query + # @option kwargs [Integer] :limit Result limit + # @option kwargs [String] :cursor Pagination cursor + def query_all_uid_details(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/broker/apilimit/query-all', params: params) + end + + # Query Broker Rate Limit Cap + # + # GET /v5/broker/apilimit/query-cap + def query_cap + @session.sign_request(method: :get, path: '/v5/broker/apilimit/query-cap') + end + + # Get Broker Earnings Info + # + # GET /v5/broker/earnings-info + # + # @option kwargs [String] :biz_type Business type + # @option kwargs [String] :begin_ Begin date + # @option kwargs [String] :end_ End date + # @option kwargs [String] :uid Sub UID + # @option kwargs [Integer] :limit Result limit + # @option kwargs [String] :cursor Pagination cursor + def query_earning(**kwargs) + params = kwargs.dup + params[:begin] = params.delete(:begin_) if params.key?(:begin_) + params[:end] = params.delete(:end_) if params.key?(:end_) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/broker/earnings-info', params: params) + end + + # Set Broker API Rate Limit + # + # POST /v5/broker/apilimit/set + # + # @option kwargs [Array] :list List of rate limit config entries + def set_api_limit(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/broker/apilimit/set', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/crypto_loan_service.rb b/lib/bybit/rest_api/crypto_loan_service.rb new file mode 100644 index 0000000..4edd51f --- /dev/null +++ b/lib/bybit/rest_api/crypto_loan_service.rb @@ -0,0 +1,350 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class CryptoLoanService < BaseService + # Adjust collateral by adding or removing amount for crypto loan. + # + # POST /v5/crypto-loan-common/adjust-ltv + # + # @param currency [String] Collateral currency + # @param amount [String] Amount to adjust + # @param direction [Integer] Adjustment direction: add or remove collateral + def adjust_ltv(currency:, amount:, direction:, **kwargs) + params = kwargs.merge(currency: currency, amount: amount, direction: direction) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-common/adjust-ltv', body: params) + end + + # Get collateral adjustment history for crypto loan positions. + # + # GET /v5/crypto-loan-common/adjustment-history + # + # @option kwargs [Integer] :adjust_id Adjustment record ID + # @option kwargs [String] :collateral_currency Collateral currency filter + # @option kwargs [Integer] :limit Number of records per page + # @option kwargs [Integer] :cursor Pagination cursor + def get_adjustment_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-common/adjustment-history', params: params) + end + + # Get collateral currency data for crypto loan. + # + # GET /v5/crypto-loan-common/collateral-data + # + # @option kwargs [String] :currency Collateral currency filter + def get_collateral_data(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/crypto-loan-common/collateral-data', params: params) + end + + # Get loanable currency data for crypto loan. + # + # GET /v5/crypto-loan-common/loanable-data + # + # @option kwargs [String] :currency Loanable currency filter + # @option kwargs [String] :vip_level VIP level filter + def get_loanable_data(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-common/loanable-data', params: params) + end + + # Get maximum collateral redeem amount for a currency. + # + # GET /v5/crypto-loan-common/max-collateral-amount + # + # @param currency [String] Collateral currency + def get_max_collateral_amount(currency:, **kwargs) + params = kwargs.merge(currency: currency) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-common/max-collateral-amount', params: params) + end + + # Calculate the maximum borrowable amount given collateral list. + # + # POST /v5/crypto-loan-common/max-loan + # + # @param currency [String] Loan currency + # @param collateral_list [Array] List of collateral currencies and amounts + def get_max_loan(currency:, collateral_list:, **kwargs) + params = kwargs.merge(currency: currency, collateral_list: collateral_list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-common/max-loan', body: params) + end + + # Get current crypto loan position. + # + # GET /v5/crypto-loan-common/position + def get_position + @session.sign_request(method: :get, path: '/v5/crypto-loan-common/position') + end + + # Get Borrow Contract Info + # + # GET /v5/crypto-loan-fixed/borrow-contract-info + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :loan_id Loan ID + # @option kwargs [String] :order_currency Order currency + # @option kwargs [String] :term Loan term + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor used for pagination + def get_fixed_borrow_contract_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/borrow-contract-info', params: params) + end + + # Get Borrow Order Info + # + # GET /v5/crypto-loan-fixed/borrow-order-info + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_currency Order currency + # @option kwargs [String] :state Order state + # @option kwargs [String] :term Loan term + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor used for pagination + def get_fixed_borrow_order_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/borrow-order-info', params: params) + end + + # Get Borrow Market Quotes + # + # GET /v5/crypto-loan-fixed/borrow-order-quote + # + # @option kwargs [String] :order_currency Order currency + # @option kwargs [String] :term Loan term + # @option kwargs [String] :order_by Order by field + # @option kwargs [Integer] :sort Sort direction + # @option kwargs [Integer] :limit Limit for data size per page + def get_fixed_borrow_order_quote(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/crypto-loan-fixed/borrow-order-quote', params: params) + end + + # Get Renewal Information + # + # GET /v5/crypto-loan-fixed/renew-info + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_currency Order currency + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor used for pagination + def get_fixed_renew_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/renew-info', params: params) + end + + # Get Supply Contract Info + # + # GET /v5/crypto-loan-fixed/supply-contract-info + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :supply_id Supply ID + # @option kwargs [String] :supply_currency Supply currency + # @option kwargs [String] :term Loan term + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor used for pagination + def get_fixed_supply_contract_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/supply-contract-info', params: params) + end + + # Get Supply Order Info + # + # GET /v5/crypto-loan-fixed/supply-order-info + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_currency Order currency + # @option kwargs [String] :state Order state + # @option kwargs [String] :term Loan term + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor used for pagination + def get_fixed_supply_order_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/supply-order-info', params: params) + end + + # Get Supply Market Quotes + # + # GET /v5/crypto-loan-fixed/supply-order-quote + # + # @option kwargs [String] :order_currency Order currency + # @option kwargs [String] :term Loan term + # @option kwargs [String] :order_by Order by field + # @option kwargs [Integer] :sort Sort direction + # @option kwargs [Integer] :limit Limit for data size per page + def get_fixed_supply_order_quote(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/crypto-loan-fixed/supply-order-quote', params: params) + end + + # Create Fixed-Term Borrow Order + # + # POST /v5/crypto-loan-fixed/borrow + # + # @param order_currency [String] Order currency + # @param order_amount [String] Order amount + # @param annual_rate [String] Annual interest rate + # @param term [String] Loan term + # @param collateral_list [Array] List of collateral + # @option kwargs [String] :auto_repay Auto repay flag + # @option kwargs [String] :repay_type Repay type + def borrow_fixed(order_currency:, order_amount:, annual_rate:, term:, collateral_list:, **kwargs) + params = kwargs.merge(order_currency: order_currency, order_amount: order_amount, annual_rate: annual_rate, term: term, collateral_list: collateral_list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/borrow', body: params) + end + + # Cancel Borrow Order + # + # POST /v5/crypto-loan-fixed/borrow-order-cancel + # + # @param order_id [String] Order ID + def cancel_fixed_borrow_order(order_id:, **kwargs) + params = kwargs.merge(order_id: order_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/borrow-order-cancel', body: params) + end + + # Fully Repay Loan + # + # POST /v5/crypto-loan-fixed/fully-repay + # + # @param loan_id [String] Loan ID + # @param loan_currency [String] Loan currency + def repay_fixed_fully(loan_id:, loan_currency:, **kwargs) + params = kwargs.merge(loan_id: loan_id, loan_currency: loan_currency) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/fully-repay', body: params) + end + + # Renew Loan + # + # POST /v5/crypto-loan-fixed/renew + # + # @param loan_id [String] Loan ID + # @param collateral_list [Array] List of collateral + def renew_fixed(loan_id:, collateral_list:, **kwargs) + params = kwargs.merge(loan_id: loan_id, collateral_list: collateral_list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/renew', body: params) + end + + # Repay with Collateral + # + # POST /v5/crypto-loan-fixed/repay-collateral + # + # @param loan_id [Integer] Loan ID + # @param loan_currency [String] Loan currency + # @param collateral_coin [String] Collateral coin + # @param amount [String] Repay amount + def post_crypto_loan_fixed_repay_collateral(loan_id:, loan_currency:, collateral_coin:, amount:, **kwargs) + params = kwargs.merge(loan_id: loan_id, loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/repay-collateral', body: params) + end + + # Cancel Supply Order + # + # POST /v5/crypto-loan-fixed/supply-order-cancel + # + # @param order_id [String] Order ID + # @option kwargs [Integer] :refunded_account Refunded account + def cancel_fixed_supply_order(order_id:, **kwargs) + params = kwargs.merge(order_id: order_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/supply-order-cancel', body: params) + end + + # Get Flexible Borrow History + # + # GET /v5/crypto-loan-flexible/borrow-history + # + # @option kwargs [String] :order_id Order ID of the borrow order + # @option kwargs [String] :loan_currency Loan currency + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor for pagination + def get_flexible_borrow_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-flexible/borrow-history', params: params) + end + + # Get Ongoing Flexible Borrow Info + # + # GET /v5/crypto-loan-flexible/ongoing-coin + # + # @option kwargs [String] :loan_currency Loan currency + def get_flexible_ongoing_coin(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-flexible/ongoing-coin', params: params) + end + + # Get Flexible Repayment History + # + # GET /v5/crypto-loan-flexible/repayment-history + # + # @option kwargs [String] :repay_id Repayment ID + # @option kwargs [String] :loan_currency Loan currency + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [Integer] :cursor Cursor for pagination + def get_flexible_repayment_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/crypto-loan-flexible/repayment-history', params: params) + end + + # Create Flexible Borrow Order + # + # POST /v5/crypto-loan-flexible/borrow + # + # @param loan_currency [String] Loan currency + # @param loan_amount [String] Loan amount + # @param collateral_list [Array] Collateral coin list + def borrow_flexible(loan_currency:, loan_amount:, collateral_list:, **kwargs) + params = kwargs.merge(loan_currency: loan_currency, loan_amount: loan_amount, collateral_list: collateral_list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-flexible/borrow', body: params) + end + + # Repay Flexible Loan + # + # POST /v5/crypto-loan-flexible/repay + # + # @param loan_currency [String] Loan currency + # @param amount [String] Repayment amount + def repay_flexible(loan_currency:, amount:, **kwargs) + params = kwargs.merge(loan_currency: loan_currency, amount: amount) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-flexible/repay', body: params) + end + + # Repay with Collateral + # + # POST /v5/crypto-loan-flexible/repay-collateral + # + # @param loan_currency [String] Loan currency + # @param collateral_coin [String] Collateral coin + # @param amount [String] Repayment amount + def post_crypto_loan_flexible_repay_collateral(loan_currency:, collateral_coin:, amount:, **kwargs) + params = kwargs.merge(loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/crypto-loan-flexible/repay-collateral', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/earn_service.rb b/lib/bybit/rest_api/earn_service.rb new file mode 100644 index 0000000..3f6b92b --- /dev/null +++ b/lib/bybit/rest_api/earn_service.rb @@ -0,0 +1,981 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class EarnService < BaseService + # Add Liquidity to a liquidity mining position. + # + # POST /v5/earn/liquidity-mining/add-liquidity + # + # @param product_id [String] Product ID + # @param order_link_id [String] Client order id + # @option kwargs [String] :quote_account_type Quote coin account type + # @option kwargs [String] :base_account_type Base coin account type + # @option kwargs [String] :quote_amount Quote coin amount + # @option kwargs [String] :base_amount Base coin amount + # @option kwargs [String] :leverage Leverage + def add_liquidity(product_id:, order_link_id:, **kwargs) + params = kwargs.merge(product_id: product_id, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/liquidity-mining/add-liquidity', body: params) + end + + # Add Margin to a liquidity mining position. + # + # POST /v5/earn/liquidity-mining/add-margin + # + # @param product_id [String] Product ID + # @param order_link_id [String] Client order id + # @param position_id [String] Position ID + # @param amount [String] Margin amount + # @param quote_account_type [String] Quote coin account type + def add_margin(product_id:, order_link_id:, position_id:, amount:, quote_account_type:, **kwargs) + params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id, amount: amount, quote_account_type: quote_account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/liquidity-mining/add-margin', body: params) + end + + # Claim Interest earned on liquidity mining product. + # + # POST /v5/earn/liquidity-mining/claim-interest + # + # @param product_id [String] Product ID + def claim_liquidity_interest(product_id:, **kwargs) + params = kwargs.merge(product_id: product_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/liquidity-mining/claim-interest', body: params) + end + + # Get advance earn orders. + # + # GET /v5/earn/advance/order + # + # @param category [String] Product category + # @option kwargs [Integer] :product_id Product ID + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User-defined order link ID + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Page size limit + # @option kwargs [String] :cursor Pagination cursor + def get_advance_earn_order(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/advance/order', params: params) + end + + # Get advance earn positions. + # + # GET /v5/earn/advance/position + # + # @param category [String] Product category + # @option kwargs [Integer] :product_id Product ID + # @option kwargs [String] :coin Coin + # @option kwargs [Integer] :limit Page size limit + # @option kwargs [String] :cursor Pagination cursor + def get_advance_earn_position(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/advance/position', params: params) + end + + # Get advance earn product info. + # + # GET /v5/earn/advance/product + # + # @param category [String] Product category + # @option kwargs [String] :coin Coin + # @option kwargs [String] :duration Product duration + def get_advance_earn_product(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/advance/product', params: params) + end + + # Get advance earn product extra info. + # + # GET /v5/earn/advance/product-extra-info + # + # @param category [String] Product category + # @option kwargs [Integer] :product_id Product ID + def get_advance_earn_product_extra_info(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/advance/product-extra-info', params: params) + end + + # Get double win leverage information. + # + # GET /v5/earn/advance/double-win-leverage + # + # @param product_id [Integer] Product ID + # @param initial_price [String] Initial price + # @param lower_price [String] Lower price bound + # @param upper_price [String] Upper price bound + def get_double_win_leverage(product_id:, initial_price:, lower_price:, upper_price:, **kwargs) + params = kwargs.merge(product_id: product_id, initial_price: initial_price, lower_price: lower_price, upper_price: upper_price) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/advance/double-win-leverage', params: params) + end + + # Get earn APR history. + # + # GET /v5/earn/apr-history + # + # @param category [String] Product category + # @param product_id [String] Product ID + # @param start_time [Integer] Start timestamp in milliseconds + # @param end_time [Integer] End timestamp in milliseconds + def get_apr_history(category:, product_id:, start_time:, end_time:, **kwargs) + params = kwargs.merge(category: category, product_id: product_id, start_time: start_time, end_time: end_time) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/apr-history', params: params) + end + + # Get earn hourly yield history. + # + # GET /v5/earn/hourly-yield + # + # @param category [String] Product category + # @option kwargs [String] :product_id Product ID + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Page size limit + # @option kwargs [String] :cursor Pagination cursor + def get_hourly_yield_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/hourly-yield', params: params) + end + + # Get stake or redeem order history. + # + # GET /v5/earn/order + # + # @param category [String] Product category + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User-defined order link ID + # @option kwargs [String] :product_id Product ID + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Page size limit + # @option kwargs [String] :cursor Pagination cursor + def get_order_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/order', params: params) + end + + # Get staked positions. + # + # GET /v5/earn/position + # + # @param category [String] Product category + # @option kwargs [String] :product_id Product ID + # @option kwargs [String] :coin Coin + def get_position_get(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/position', params: params) + end + + # Get earn product info. + # + # GET /v5/earn/product + # + # @param category [String] Product category + # @option kwargs [String] :coin Coin + def get_product(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/product', params: params) + end + + # Get earn yield history. + # + # GET /v5/earn/yield + # + # @param category [String] Product category + # @option kwargs [Integer] :product_id Product ID + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Page size limit + # @option kwargs [String] :cursor Pagination cursor + def get_yield_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/yield', params: params) + end + + # Get fixed-term earn order history. + # + # GET /v5/earn/fixed-term/order + # + # @option kwargs [String] :order_type Order type + # @option kwargs [String] :product_id Product ID + # @option kwargs [String] :category Product category + # @option kwargs [String] :order_id Order ID + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [Integer] :limit Result limit per page + # @option kwargs [String] :cursor Cursor for pagination + def get_fixed_term_order(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/fixed-term/order', params: params) + end + + # Get fixed-term earn positions. + # + # GET /v5/earn/fixed-term/position + # + # @option kwargs [String] :product_id Product ID + # @option kwargs [String] :category Product category + # @option kwargs [String] :coin Coin name + def get_fixed_term_position(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/fixed-term/position', params: params) + end + + # Get fixed-term earn product list. + # + # GET /v5/earn/fixed-term/product + # + # @option kwargs [String] :coin Coin name + def get_fixed_term_product(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/fixed-term/product', params: params) + end + + # Get Hold-to-Earn Product List. + # + # GET /v5/earn/hold-to-earn/product + def get_hold_to_earn_product + @session.public_request(path: '/v5/earn/hold-to-earn/product') + end + + # Get Hold-to-Earn Yield History. + # + # GET /v5/earn/hold-to-earn/yield-history + # + # @param limit [Integer] Limit for data size per page + # @option kwargs [Integer] :time_start Start time in ms + # @option kwargs [Integer] :time_end End time in ms + # @option kwargs [String] :cursor Cursor for pagination + def get_hold_to_earn_yield_history(limit:, **kwargs) + params = kwargs.merge(limit: limit) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/hold-to-earn/yield-history', params: params) + end + + # Get liquidity mining liquidation records with optional filters and pagination. + # + # GET /v5/earn/liquidity-mining/liquidation-records + # + # @option kwargs [String] :base_coin Base coin filter + # @option kwargs [String] :quote_coin Quote coin filter + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Result page size + # @option kwargs [String] :cursor Pagination cursor + def get_liquidity_mining_liquidation_records(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/liquidity-mining/liquidation-records', params: params) + end + + # Get Liquidity Mining Order History. + # + # GET /v5/earn/liquidity-mining/order + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id Client order id + # @option kwargs [String] :product_id Product ID + # @option kwargs [String] :order_type Order type + # @option kwargs [String] :status Order status + # @option kwargs [Integer] :start_time Start time in ms + # @option kwargs [Integer] :end_time End time in ms + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + def get_liquidity_mining_orders(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/liquidity-mining/order', params: params) + end + + # Get Liquidity Mining Active Positions. + # + # GET /v5/earn/liquidity-mining/position + # + # @option kwargs [String] :product_id Product ID + # @option kwargs [String] :base_coin Base coin + def get_liquidity_mining_positions(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/liquidity-mining/position', params: params) + end + + # Get Liquidity Mining Product List. + # + # GET /v5/earn/liquidity-mining/product + # + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :quote_coin Quote coin + def get_liquidity_mining_products(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/liquidity-mining/product', params: params) + end + + # Get Liquidity Mining Yield Claim Records. + # + # GET /v5/earn/liquidity-mining/yield-records + # + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :quote_coin Quote coin + # @option kwargs [Integer] :start_time Start time in ms + # @option kwargs [Integer] :end_time End time in ms + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + def get_liquidity_mining_yield_records(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/liquidity-mining/yield-records', params: params) + end + + # Get RWA NAV Chart. + # + # GET /v5/earn/rwa/nav-chart + # + # @param product_id [Integer] Product ID + # @option kwargs [Integer] :start_time Start time in ms + # @option kwargs [Integer] :end_time End time in ms + def get_rwa_nav_chart(product_id:, **kwargs) + params = kwargs.merge(product_id: product_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/rwa/nav-chart', params: params) + end + + # Get RWA Order List. + # + # GET /v5/earn/rwa/order + # + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id Client order id + # @option kwargs [String] :order_type Order type: Stake / Redeem + # @option kwargs [Integer] :product_id Product ID + # @option kwargs [Integer] :start_time Start time in ms + # @option kwargs [Integer] :end_time End time in ms + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + def get_rwa_order_list(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/rwa/order', params: params) + end + + # Get RWA Position List. + # + # GET /v5/earn/rwa/position + def get_rwa_position_list + @session.sign_request(method: :get, path: '/v5/earn/rwa/position') + end + + # Get RWA earn product list. + # + # GET /v5/earn/rwa/product + # + # @option kwargs [String] :coin Coin name + def get_rwa_product_list(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/rwa/product', params: params) + end + + # Get smart leverage redeem estimation amount list. + # + # GET /v5/earn/advance/get-redeem-est-amount-list + # + # @param category [String] Product category + # @param position_ids [String] Position IDs + def get_smart_leverage_redeem_est_amount_list(category:, position_ids:, **kwargs) + params = kwargs.merge(category: category, position_ids: position_ids) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/advance/get-redeem-est-amount-list', params: params) + end + + # Get daily yield records for a token earn position. + # + # GET /v5/earn/token/yield + # + # @param coin [String] Coin name + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [String] :cursor Cursor for pagination + # @option kwargs [Integer] :limit Result limit per page + def get_token_daily_yield(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/token/yield', params: params) + end + + # Get historical APR for a token earn product. + # + # GET /v5/earn/token/history-apr + # + # @param coin [String] Coin name + # @param range [Integer] Range window for APR history + def get_token_historical_apr(coin:, range:, **kwargs) + params = kwargs.merge(coin: coin, range: range) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/token/history-apr', params: params) + end + + # Get hourly yield records for a token earn position. + # + # GET /v5/earn/token/hourly-yield + # + # @param coin [String] Coin name + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [String] :cursor Cursor for pagination + # @option kwargs [Integer] :limit Result limit per page + def get_token_hourly_yield(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/token/hourly-yield', params: params) + end + + # Query token earn order history. + # + # GET /v5/earn/token/order + # + # @param coin [String] Coin name + # @option kwargs [String] :order_link_id Client-supplied order link ID + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_type Order type (mint or redeem) + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [String] :cursor Cursor for pagination + # @option kwargs [Integer] :limit Result limit per page + def get_token_order_list(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/token/order', params: params) + end + + # Get current token earn position for a coin. + # + # GET /v5/earn/token/position + # + # @param coin [String] Coin name + def get_token_position(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/token/position', params: params) + end + + # Get info for a token earn product. + # + # GET /v5/earn/token/product + # + # @param coin [String] Coin name + def get_token_product(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/earn/token/product', params: params) + end + + # List available earn coupons for the given category. + # + # GET /v5/earn/coupons + # + # @param category [String] Product category + def list_coupons(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/coupons', params: params) + end + + # Modify an earn position. + # + # POST /v5/earn/position/modify + # + # @param category [String] Product category + # @param product_id [Integer] Product ID + # @param position_id [Integer] Position ID + # @param auto_reinvest [Integer] Auto-reinvest flag + def modify_earn_position(category:, product_id:, position_id:, auto_reinvest:, **kwargs) + params = kwargs.merge(category: category, product_id: product_id, position_id: position_id, auto_reinvest: auto_reinvest) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/position/modify', body: params) + end + + # Place an advance earn order. + # + # POST /v5/earn/advance/place-order + # + # @param category [String] Product category + # @param product_id [Integer] Product ID + # @param order_type [String] Order type + # @param amount [String] Order amount + # @param account_type [String] Account type + # @param coin [String] Coin + # @param order_link_id [String] User-defined order link ID + # @option kwargs [Hash] :dual_assets_extra Dual assets extra info + # @option kwargs [Hash] :interest_card Interest card info + # @option kwargs [Hash] :smart_leverage_stake_extra Smart leverage stake extra info + # @option kwargs [Hash] :smart_leverage_redeem_extra Smart leverage redeem extra info + # @option kwargs [Hash] :double_win_stake_extra Double win stake extra info + # @option kwargs [Hash] :double_win_redeem_extra Double win redeem extra info + # @option kwargs [Hash] :discount_buy_extra Discount buy extra info + def place_advance_earn_order(category:, product_id:, order_type:, amount:, account_type:, coin:, order_link_id:, **kwargs) + params = kwargs.merge(category: category, product_id: product_id, order_type: order_type, amount: amount, account_type: account_type, coin: coin, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/advance/place-order', body: params) + end + + # Stake or redeem an earn order. + # + # POST /v5/earn/place-order + # + # @param category [String] Product category + # @param order_type [String] Order type: Stake or Redeem + # @param account_type [String] Account type + # @param amount [String] Order amount + # @param coin [String] Coin + # @param product_id [String] Product ID + # @param order_link_id [String] User-defined order link ID + # @option kwargs [String] :redeem_position_id Redeem position ID + # @option kwargs [String] :to_account_type Destination account type + # @option kwargs [Hash] :interest_card Interest card info + def place_order(category:, order_type:, account_type:, amount:, coin:, product_id:, order_link_id:, **kwargs) + params = kwargs.merge(category: category, order_type: order_type, account_type: account_type, amount: amount, coin: coin, product_id: product_id, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/place-order', body: params) + end + + # Place a fixed-term earn order. + # + # POST /v5/earn/fixed-term/place-order + # + # @param product_id [String] Product ID + # @param category [String] Product category + # @param coin [String] Coin name + # @param amount [String] Order amount + # @param account_type [String] Account type + # @param order_link_id [String] Client-supplied order link ID + # @option kwargs [Boolean] :auto_invest Whether to enable auto-invest + def place_fixed_term_order(product_id:, category:, coin:, amount:, account_type:, order_link_id:, **kwargs) + params = kwargs.merge(product_id: product_id, category: category, coin: coin, amount: amount, account_type: account_type, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/fixed-term/place-order', body: params) + end + + # Place Order (Stake / Redeem) for RWA earn products. + # + # POST /v5/earn/rwa/place-order + # + # @param product_id [Integer] Product ID + # @param order_type [String] Order type: Stake / Redeem + # @param coin [String] Coin name + # @param order_link_id [String] Client order id + # @option kwargs [String] :stake_amount Stake amount, required when orderType is Stake + # @option kwargs [String] :redeem_shares Redeem shares, required when orderType is Redeem + # @option kwargs [String] :account_type Account type + def place_rwa_order(product_id:, order_type:, coin:, order_link_id:, **kwargs) + params = kwargs.merge(product_id: product_id, order_type: order_type, coin: coin, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/rwa/place-order', body: params) + end + + # Place a mint or redeem order for a token earn product. + # + # POST /v5/earn/token/place-order + # + # @param coin [String] Coin name + # @param order_link_id [String] Client-supplied order link ID + # @param order_type [String] Order type (mint or redeem) + # @param amount [String] Order amount + # @param account_type [String] Account type + def place_token_order(coin:, order_link_id:, order_type:, amount:, account_type:, **kwargs) + params = kwargs.merge(coin: coin, order_link_id: order_link_id, order_type: order_type, amount: amount, account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/token/place-order', body: params) + end + + # Get PWM investment plan asset trend over time. + # + # GET /v5/earn/pwm/investment-plan/asset-trend + # + # @param plan_id [String] Investment plan ID + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + def pwm_asset_trend(plan_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/asset-trend', params: params) + end + + # Claim available funds from a PWM investment plan. + # + # POST /v5/earn/pwm/investment-plan/claim + # + # @param plan_id [String] Investment plan ID + # @param order_link_id [String] Client-generated unique order link ID + # @option kwargs [String] :to_account_type Destination account type + def pwm_claim(plan_id:, order_link_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/investment-plan/claim', body: params) + end + + # Create a custom PWM investment plan in Direct Mode. + # + # POST /v5/earn/pwm/customize-plan/create + # + # @param products [Array] List of products to include in the plan + # @param order_link_id [String] Client-generated unique order link ID + # @option kwargs [String] :account_type Source account type + def pwm_create_custom_plan(products:, order_link_id:, **kwargs) + params = kwargs.merge(products: products, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/customize-plan/create', body: params) + end + + # Get historical NAV data for a PWM fund. + # + # GET /v5/earn/pwm/investment-plan/fund-nav + # + # @param fund_id [String] Fund ID + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + def pwm_fund_nav(fund_id:, **kwargs) + params = kwargs.merge(fund_id: fund_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/fund-nav', params: params) + end + + # Transfer funds between custody sub-accounts. + # + # POST /v5/earn/pwm/fund-transfer + # + # @param transfer_id [String] Client-supplied transfer identifier + # @param from_user_id [Integer] Source sub-account UID + # @param to_user_id [Integer] Destination sub-account UID + # @param amount [String] Transfer amount + # @param coin [String] Coin symbol + def pwm_fund_transfer(transfer_id:, from_user_id:, to_user_id:, amount:, coin:, **kwargs) + params = kwargs.merge(transfer_id: transfer_id, from_user_id: from_user_id, to_user_id: to_user_id, amount: amount, coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/fund-transfer', body: params) + end + + # Get the detail of a pending-subscription investment plan. + # + # GET /v5/earn/pwm/investment-plan/new-plan + # + # @param plan_id [String] Investment plan identifier + def pwm_get_new_plan_detail(plan_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/new-plan', params: params) + end + + # Get the detail of an active or closed investment plan. + # + # GET /v5/earn/pwm/investment-plan/detail + # + # @param plan_id [String] Investment plan identifier + def pwm_get_plan_detail(plan_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/detail', params: params) + end + + # Create a pending-subscription fund for the institution. + # + # POST /v5/earn/pwm/asset-manager/create-fund + # + # @param fund_name [String] Fund display name + # @param coin [String] Base coin of the fund + # @param profit_share_rate [String] Profit share rate + # @param management_fee_rate [String] Management fee rate + # @param req_link_id [String] Client-supplied idempotency key + # @option kwargs [String] :fund_introduction Fund introduction text + def pwm_inst_create_fund(fund_name:, coin:, profit_share_rate:, management_fee_rate:, req_link_id:, **kwargs) + params = kwargs.merge(fund_name: fund_name, coin: coin, profit_share_rate: profit_share_rate, management_fee_rate: management_fee_rate, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/create-fund', body: params) + end + + # Create an investment plan for a client account. + # + # POST /v5/earn/pwm/asset-manager/create-investment-plan + # + # @param account_uid [String] Client account UID + # @param plan_name [String] Investment plan name + # @param plan_type [String] Investment plan type + # @param investment_distribution [Array] Fund allocation distribution + # @param req_link_id [String] Client-supplied idempotency key + def pwm_inst_create_investment_plan(account_uid:, plan_name:, plan_type:, investment_distribution:, req_link_id:, **kwargs) + params = kwargs.merge(account_uid: account_uid, plan_name: plan_name, plan_type: plan_type, investment_distribution: investment_distribution, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/create-investment-plan', body: params) + end + + # Create a fund custody sub-account. + # + # POST /v5/earn/pwm/asset-manager/create-sub-account + # + # @param fund_id [String] Fund identifier + # @param req_link_id [String] Client-supplied idempotency key + def pwm_inst_create_sub_account(fund_id:, req_link_id:, **kwargs) + params = kwargs.merge(fund_id: fund_id, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/create-sub-account', body: params) + end + + # Query the institution's investment plans with optional filters. + # + # GET /v5/earn/pwm/asset-manager/get-investment-plan + # + # @option kwargs [String] :plan_id Investment plan identifier + # @option kwargs [String] :status Plan status filter + # @option kwargs [String] :subscription_uid Subscription account UID + # @option kwargs [Integer] :limit Result page size + # @option kwargs [String] :cursor Pagination cursor + def pwm_inst_get_investment_plans(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/asset-manager/get-investment-plan', params: params) + end + + # Query the institution's managed funds with optional filters. + # + # GET /v5/earn/pwm/asset-manager/all-funds + # + # @option kwargs [String] :fund_id Fund identifier + # @option kwargs [String] :coin Coin filter + # @option kwargs [String] :status Fund status filter + # @option kwargs [Integer] :limit Result page size + # @option kwargs [String] :cursor Pagination cursor + def pwm_inst_list_funds(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/asset-manager/all-funds', params: params) + end + + # Query fund subscription and redemption orders. + # + # GET /v5/earn/pwm/asset-manager/all-order + # + # @option kwargs [String] :fund_id Fund identifier + # @option kwargs [String] :order_type Order type filter + # @option kwargs [String] :status Order status filter + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Result page size + # @option kwargs [String] :cursor Pagination cursor + def pwm_inst_list_orders(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/asset-manager/all-order', params: params) + end + + # Update the status and fund allocation of an investment plan. + # + # POST /v5/earn/pwm/asset-manager/manage-investment-plan + # + # @param plan_id [String] Investment plan identifier + # @param req_link_id [String] Client-supplied idempotency key + # @option kwargs [String] :update_status New plan status + # @option kwargs [Array] :update_funds Updated fund allocation + def pwm_inst_manage_investment_plan(plan_id:, req_link_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/manage-investment-plan', body: params) + end + + # Approve or reject a fund subscription or redemption order. + # + # POST /v5/earn/pwm/asset-manager/manage-order + # + # @param order_id [String] Order identifier + # @param action [String] Approve or reject action + # @param req_link_id [String] Client-supplied idempotency key + def pwm_inst_manage_order(order_id:, action:, req_link_id:, **kwargs) + params = kwargs.merge(order_id: order_id, action: action, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/manage-order', body: params) + end + + # Execute profit settlement for the specified fund. + # + # POST /v5/earn/pwm/asset-manager/settle-profit + # + # @param fund_id [String] Fund identifier + # @param req_link_id [String] Client-supplied idempotency key + def pwm_inst_settle_profit(fund_id:, req_link_id:, **kwargs) + params = kwargs.merge(fund_id: fund_id, req_link_id: req_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/asset-manager/settle-profit', body: params) + end + + # Invest more funds into an active PWM investment plan. + # + # POST /v5/earn/pwm/investment-plan/invest-more + # + # @param plan_id [String] Investment plan ID + # @param category [String] Product category + # @param product_id [String] Product ID + # @param amount [String] Investment amount + # @param order_link_id [String] Client-generated unique order link ID + # @option kwargs [String] :account_type Source account type + def pwm_invest_more(plan_id:, category:, product_id:, amount:, order_link_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id, category: category, product_id: product_id, amount: amount, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/investment-plan/invest-more', body: params) + end + + # List investment plans for the account. + # + # GET /v5/earn/pwm/investment-plan/all + # + # @option kwargs [String] :plan_id Investment plan identifier + # @option kwargs [String] :status Plan status filter + # @option kwargs [Integer] :limit Result page size + # @option kwargs [String] :cursor Pagination cursor + def pwm_list_investment_plans(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/all', params: params) + end + + # List PWM investment plan orders with optional filters. + # + # GET /v5/earn/pwm/investment-plan/order + # + # @option kwargs [String] :plan_id Investment plan ID + # @option kwargs [String] :category Product category + # @option kwargs [String] :type Order type + # @option kwargs [String] :status Order status + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + # @option kwargs [Integer] :limit Result limit per page + # @option kwargs [String] :cursor Pagination cursor + # @option kwargs [String] :order_link_id Client order link ID + def pwm_list_order(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/investment-plan/order', params: params) + end + + # List available product cards for PWM Direct Mode custom plans. + # + # GET /v5/earn/pwm/customize-plan/product + def pwm_list_product_cards + @session.public_request(path: '/v5/earn/pwm/customize-plan/product') + end + + # Query fund transfer records between custody sub-accounts. + # + # GET /v5/earn/pwm/query-fund-transfer-result + # + # @option kwargs [String] :transfer_id Client-supplied transfer identifier + # @option kwargs [Integer] :from_user_id Source sub-account UID + def pwm_query_fund_transfer_result(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/earn/pwm/query-fund-transfer-result', params: params) + end + + # Redeem shares or amount from a PWM investment plan. + # + # POST /v5/earn/pwm/investment-plan/redeem + # + # @param plan_id [String] Investment plan ID + # @param category [String] Product category + # @param product_id [String] Product ID + # @param order_link_id [String] Client-generated unique order link ID + # @option kwargs [String] :shares Number of shares to redeem + # @option kwargs [String] :amount Amount to redeem + # @option kwargs [Integer] :position_id Position ID + def pwm_redeem(plan_id:, category:, product_id:, order_link_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id, category: category, product_id: product_id, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/investment-plan/redeem', body: params) + end + + # One-click subscribe to a pending PWM investment plan. + # + # POST /v5/earn/pwm/investment-plan/subscribe + # + # @param plan_id [String] Investment plan ID + # @param order_link_id [String] Client-generated unique order link ID + # @option kwargs [String] :account_type Source account type + def pwm_subscribe(plan_id:, order_link_id:, **kwargs) + params = kwargs.merge(plan_id: plan_id, order_link_id: order_link_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/pwm/investment-plan/subscribe', body: params) + end + + # Redeem a fixed-term earn position. + # + # POST /v5/earn/fixed-term/redeem + # + # @param product_id [String] Product ID + # @param category [String] Product category + # @param position_id [String] Position ID + def redeem_fixed_term(product_id:, category:, position_id:, **kwargs) + params = kwargs.merge(product_id: product_id, category: category, position_id: position_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/fixed-term/redeem', body: params) + end + + # Reinvest Interest for a liquidity mining position. + # + # POST /v5/earn/liquidity-mining/reinvest + # + # @param product_id [String] Product ID + # @param order_link_id [String] Client order id + # @param position_id [String] Position ID + def reinvest_liquidity(product_id:, order_link_id:, position_id:, **kwargs) + params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/liquidity-mining/reinvest', body: params) + end + + # Remove Liquidity from a liquidity mining position. + # + # POST /v5/earn/liquidity-mining/remove-liquidity + # + # @param product_id [String] Product ID + # @param order_link_id [String] Client order id + # @param position_id [String] Position ID + # @option kwargs [Integer] :remove_rate Remove rate percent + # @option kwargs [String] :remove_type Remove type + def remove_liquidity(product_id:, order_link_id:, position_id:, **kwargs) + params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/liquidity-mining/remove-liquidity', body: params) + end + + # Enable or disable auto-invest for a fixed-term earn position. + # + # POST /v5/earn/fixed-term/position/auto-invest + # + # @param product_id [String] Product ID + # @param category [String] Product category + # @param position_id [String] Position ID + # @param status [String] Auto-invest status + def set_fixed_term_auto_invest(product_id:, category:, position_id:, status:, **kwargs) + params = kwargs.merge(product_id: product_id, category: category, position_id: position_id, status: status) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/earn/fixed-term/position/auto-invest', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/market_service.rb b/lib/bybit/rest_api/market_service.rb new file mode 100644 index 0000000..bd52abd --- /dev/null +++ b/lib/bybit/rest_api/market_service.rb @@ -0,0 +1,338 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class MarketService < BaseService + # Get ADL alert information for the specified symbol. + # + # GET /v5/market/adlalert + # + # @option kwargs [String] :symbol Symbol name + # @see https://bybit-exchange.github.io/docs/v5/market/adl-alert + def get_adl_alert(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/adlalert', params: params) + end + + # Get the delivery price for delivery contracts. + # + # GET /v5/market/delivery-price + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/market/delivery-price + def get_delivery_price(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/delivery-price', params: params) + end + + # Get the fee group structure information. + # + # GET /v5/market/fee-group-info + # + # @param product_type [String] Product type + # @option kwargs [String] :group_id Fee group id + # @see https://bybit-exchange.github.io/docs/v5/market/fee-group-info + def get_fee_group_info(product_type:, **kwargs) + params = kwargs.merge(product_type: product_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/fee-group-info', params: params) + end + + # Get the historical funding rate for a symbol. + # + # GET /v5/market/funding/history + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @see https://bybit-exchange.github.io/docs/v5/market/history-fund-rate + def get_funding_rate_history(category:, symbol:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/funding/history', params: params) + end + + # Get the historical volatility for options. + # + # GET /v5/market/historical-volatility + # + # @param category [String] Product type + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :quote_coin Quote coin + # @option kwargs [Integer] :period Period + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + # @see https://bybit-exchange.github.io/docs/v5/market/iv + def get_historical_volatility(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/historical-volatility', params: params) + end + + # Get the components of the index price. + # + # GET /v5/market/index-price-components + # + # @param index_name [String] Index name + def get_index_price_components(index_name:, **kwargs) + params = kwargs.merge(index_name: index_name) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/index-price-components', params: params) + end + + # Get index price kline data for a symbol. + # + # GET /v5/market/index-price-kline + # + # @param symbol [String] Symbol name + # @param interval [String] Kline interval + # @option kwargs [String] :category Product type + # @option kwargs [Integer] :start Start timestamp (ms) + # @option kwargs [Integer] :end_ End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @see https://bybit-exchange.github.io/docs/v5/market/index-kline + def get_index_price_kline(symbol:, interval:, **kwargs) + params = kwargs.merge(symbol: symbol, interval: interval) + params[:end] = params.delete(:end_) if params.key?(:end_) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/index-price-kline', params: params) + end + + # Get the specification of instruments. + # + # GET /v5/market/instruments-info + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :status Symbol status filter + # @option kwargs [String] :base_coin Base coin + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/market/instrument + def get_instruments_info(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/instruments-info', params: params) + end + + # Get the insurance pool balance data. + # + # GET /v5/market/insurance + # + # @option kwargs [String] :coin Coin name + # @see https://bybit-exchange.github.io/docs/v5/market/insurance + def get_insurance_pool(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/insurance', params: params) + end + + # Get the long/short account ratio for a symbol. + # + # GET /v5/market/account-ratio + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param period [String] Data recording period + # @option kwargs [String] :start_time Start timestamp (ms) + # @option kwargs [String] :end_time End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/market/long-short-ratio + def get_long_short_ratio(category:, symbol:, period:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, period: period) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/account-ratio', params: params) + end + + # Get mark price kline data for a symbol. + # + # GET /v5/market/mark-price-kline + # + # @param symbol [String] Symbol name + # @param interval [String] Kline interval + # @option kwargs [String] :category Product type + # @option kwargs [Integer] :start Start timestamp (ms) + # @option kwargs [Integer] :end_ End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @see https://bybit-exchange.github.io/docs/v5/market/mark-kline + def get_mark_price_kline(symbol:, interval:, **kwargs) + params = kwargs.merge(symbol: symbol, interval: interval) + params[:end] = params.delete(:end_) if params.key?(:end_) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/mark-price-kline', params: params) + end + + # Get kline (candlestick) data for a symbol. + # + # GET /v5/market/kline + # + # @param symbol [String] Symbol name + # @param interval [String] Kline interval + # @option kwargs [String] :category Product type + # @option kwargs [Integer] :start Start timestamp (ms) + # @option kwargs [Integer] :end_ End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @see https://bybit-exchange.github.io/docs/v5/market/kline + def get_kline(symbol:, interval:, **kwargs) + params = kwargs.merge(symbol: symbol, interval: interval) + params[:end] = params.delete(:end_) if params.key?(:end_) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/kline', params: params) + end + + # Get the new delivery price for delivery contracts. + # + # GET /v5/market/new-delivery-price + # + # @param category [String] Product type + # @param base_coin [String] Base coin + # @option kwargs [String] :settle_coin Settle coin + # @see https://bybit-exchange.github.io/docs/v5/market/new-delivery-price + def get_new_delivery_price(category:, base_coin:, **kwargs) + params = kwargs.merge(category: category, base_coin: base_coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/new-delivery-price', params: params) + end + + # Get the open interest data for a symbol. + # + # GET /v5/market/open-interest + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param interval_time [String] Interval time + # @option kwargs [Integer] :start_time Start timestamp (ms) + # @option kwargs [Integer] :end_time End timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/market/open-interest + def get_open_interest(category:, symbol:, interval_time:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, interval_time: interval_time) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/open-interest', params: params) + end + + # Get the order price limit for a symbol. + # + # GET /v5/market/price-limit + # + # @param symbol [String] Symbol name + # @option kwargs [String] :category Product type + def get_order_price_limit(symbol:, **kwargs) + params = kwargs.merge(symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/price-limit', params: params) + end + + # Get orderbook data for a specified symbol and category. + # + # GET /v5/market/orderbook + # + # @param category [String] Product type: spot, linear, inverse, option + # @param symbol [String] Symbol name + # @option kwargs [Integer] :limit Limit size for each bid/ask + # @see https://bybit-exchange.github.io/docs/v5/market/orderbook + def get_orderbook(category:, symbol:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/orderbook', params: params) + end + + # Get premium index price kline data. + # + # GET /v5/market/premium-index-price-kline + # + # @param symbol [String] Symbol name + # @param interval [String] Kline interval + # @option kwargs [String] :category Product type: linear + # @option kwargs [Integer] :start The start timestamp (ms) + # @option kwargs [Integer] :end_ The end timestamp (ms) + # @option kwargs [Integer] :limit Limit for data size per page. [1, 1000]. Default: 200 + # @see https://bybit-exchange.github.io/docs/v5/market/premium-index-kline + def get_premium_index_price_kline(symbol:, interval:, **kwargs) + params = kwargs.merge(symbol: symbol, interval: interval) + params[:end] = params.delete(:end_) if params.key?(:end_) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/premium-index-price-kline', params: params) + end + + # Get recent public trades. + # + # GET /v5/market/recent-trade + # + # @param category [String] Product type: spot, linear, inverse, option + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin, for option only + # @option kwargs [String] :option_type Option type: Call or Put, for option only + # @option kwargs [Integer] :limit Limit for data size per page + # @see https://bybit-exchange.github.io/docs/v5/market/recent-trade + def get_recent_public_trades(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/recent-trade', params: params) + end + + # Get risk limit information. + # + # GET /v5/market/risk-limit + # + # @param category [String] Product type: linear, inverse + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/market/risk-limit + def get_risk_limit(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/risk-limit', params: params) + end + + # Get RPI (Retail Price Improvement) orderbook data. + # + # GET /v5/market/rpi_orderbook + # + # @param symbol [String] Symbol name + # @param limit [Integer] Limit size for each bid/ask + # @option kwargs [String] :category Product type: spot + # @see https://bybit-exchange.github.io/docs/v5/market/rpi-orderbook + def get_rpi_orderbook(symbol:, limit:, **kwargs) + params = kwargs.merge(symbol: symbol, limit: limit) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/rpi_orderbook', params: params) + end + + # Get Bybit server time. + # + # GET /v5/market/time + # + # @see https://bybit-exchange.github.io/docs/v5/market/time + def get_server_time + @session.public_request(path: '/v5/market/time') + end + + # Get tickers information for a specified category. + # + # GET /v5/market/tickers + # + # @param category [String] Product type: spot, linear, inverse, option + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin, for option only + # @option kwargs [String] :exp_date Expiry date, for option only + # @see https://bybit-exchange.github.io/docs/v5/market/tickers + def get_tickers(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/market/tickers', params: params) + end + end + end +end diff --git a/lib/bybit/rest_api/p2p_service.rb b/lib/bybit/rest_api/p2p_service.rb new file mode 100644 index 0000000..edef8b2 --- /dev/null +++ b/lib/bybit/rest_api/p2p_service.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class P2pService < BaseService + # Get the P2P user account information for the caller. + # + # POST /v5/p2p/user/personal/info + def get_account_info + @session.sign_request(method: :post, path: '/v5/p2p/user/personal/info', body: {}) + end + + # Get online P2P advertisement list filtered by token, currency and side. + # + # POST /v5/p2p/item/online + # + # @param token_id [String] Token ID + # @param currency_id [String] Currency ID + # @param side [String] Side: 0 buy, 1 sell + # @option kwargs [String] :page Page number + # @option kwargs [String] :size Page size + def get_ads(token_id:, currency_id:, side:, **kwargs) + params = kwargs.merge(token_id: token_id, currency_id: currency_id, side: side) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/online', body: params) + end + + # Get simplified list of all P2P orders with optional filters. + # + # POST /v5/p2p/order/simplifylist + # + # @param page [Integer] Page number + # @param size [Integer] Page size + # @option kwargs [Integer] :status Order status filter + # @option kwargs [String] :begin_time Begin time in milliseconds + # @option kwargs [String] :end_time End time in milliseconds + # @option kwargs [String] :token_id Token ID + # @option kwargs [Integer] :side Side: 0 buy, 1 sell + def get_all_orders(page:, size:, **kwargs) + params = kwargs.merge(page: page, size: size) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/simplifylist', body: params) + end + + # Get the paginated chat message list for a P2P order. + # + # POST /v5/p2p/order/message/listpage + # + # @param order_id [String] Order ID + # @param size [String] Page size + # @option kwargs [String] :current_page Current page number + def get_chat_messages(order_id:, size:, **kwargs) + params = kwargs.merge(order_id: order_id, size: size) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/message/listpage', body: params) + end + + # Get counterparty user info for a P2P order. + # + # POST /v5/p2p/user/order/personal/info + # + # @option kwargs [String] :original_uid Original user ID of the counterparty. + # @option kwargs [String] :order_id P2P order ID. + def get_counterparty_user_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/user/order/personal/info', body: params) + end + + # Get details of a specific P2P advertisement owned by the caller. + # + # POST /v5/p2p/item/info + # + # @param item_id [String] Advertisement ID + def get_my_ad_details(item_id:, **kwargs) + params = kwargs.merge(item_id: item_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/info', body: params) + end + + # Get the list of the caller's own P2P advertisements. + # + # POST /v5/p2p/item/personal/list + # + # @option kwargs [String] :item_id Advertisement ID + # @option kwargs [String] :status Ad status filter + # @option kwargs [String] :side Side: 0 buy, 1 sell + # @option kwargs [String] :token_id Token ID + # @option kwargs [String] :page Page number + # @option kwargs [String] :size Page size + # @option kwargs [String] :currency_id Currency ID + def get_my_ads(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/personal/list', body: params) + end + + # Get the details of a specific P2P order. + # + # POST /v5/p2p/order/info + # + # @param order_id [String] Order ID + def get_order_detail(order_id:, **kwargs) + params = kwargs.merge(order_id: order_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/info', body: params) + end + + # Get simplified list of pending P2P orders with optional filters. + # + # POST /v5/p2p/order/pending/simplifylist + # + # @param page [Integer] Page number + # @param size [Integer] Page size + # @option kwargs [Integer] :status Order status filter + # @option kwargs [String] :begin_time Begin time in milliseconds + # @option kwargs [String] :end_time End time in milliseconds + # @option kwargs [String] :token_id Token ID + # @option kwargs [Integer] :side Side: 0 buy, 1 sell + def get_pending_orders(page:, size:, **kwargs) + params = kwargs.merge(page: page, size: size) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/pending/simplifylist', body: params) + end + + # Get the current user's P2P payment method list. + # + # POST /v5/p2p/user/payment/list + def get_user_payment + @session.sign_request(method: :post, path: '/v5/p2p/user/payment/list', body: {}) + end + + # Mark a P2P order as paid by the buyer. + # + # POST /v5/p2p/order/pay + # + # @param order_id [String] Order ID + # @param payment_type [String] Payment method type + # @param payment_id [String] Payment method ID + def mark_order_as_paid(order_id:, payment_type:, payment_id:, **kwargs) + params = kwargs.merge(order_id: order_id, payment_type: payment_type, payment_id: payment_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/pay', body: params) + end + + # Create a new P2P advertisement. + # + # POST /v5/p2p/item/create + # + # @param token_id [String] Token ID + # @param currency_id [String] Currency ID + # @param side [String] Side: 0 buy, 1 sell + # @param price_type [String] Price type: 0 fixed, 1 floating + # @param premium [String] Premium value + # @param price [String] Ad price + # @param min_amount [String] Minimum order amount + # @param max_amount [String] Maximum order amount + # @param remark [String] Ad remark + # @param trading_preference_set [Hash] Trading preference settings + # @param payment_ids [Array] Payment method IDs + # @param quantity [String] Ad quantity + # @param payment_period [String] Payment period in minutes + # @param item_type [String] Item type + def post_ad(token_id:, currency_id:, side:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, quantity:, payment_period:, item_type:, **kwargs) + params = kwargs.merge(token_id: token_id, currency_id: currency_id, side: side, price_type: price_type, premium: premium, price: price, min_amount: min_amount, max_amount: max_amount, remark: remark, trading_preference_set: trading_preference_set, payment_ids: payment_ids, quantity: quantity, payment_period: payment_period, item_type: item_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/create', body: params) + end + + # Release the assets held for a P2P order after payment is confirmed. + # + # POST /v5/p2p/order/finish + # + # @param order_id [String] Order ID + def release_assets(order_id:, **kwargs) + params = kwargs.merge(order_id: order_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/finish', body: params) + end + + # Cancel/remove an existing P2P advertisement. + # + # POST /v5/p2p/item/cancel + # + # @param item_id [String] Advertisement ID + def remove_ad(item_id:, **kwargs) + params = kwargs.merge(item_id: item_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/cancel', body: params) + end + + # Send a chat message in a P2P order conversation. + # + # POST /v5/p2p/order/message/send + # + # @param message [String] Message content + # @param content_type [String] Message content type + # @param order_id [String] Order ID + # @param msg_uuid [String] Message UUID + # @option kwargs [String] :file_name File name when sending a file + def send_chat_message(message:, content_type:, order_id:, msg_uuid:, **kwargs) + params = kwargs.merge(message: message, content_type: content_type, order_id: order_id, msg_uuid: msg_uuid) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/order/message/send', body: params) + end + + # Update or relist an existing P2P advertisement. + # + # POST /v5/p2p/item/update + # + # @param id [String] Advertisement ID + # @param price_type [String] Price type: 0 fixed, 1 floating + # @param premium [String] Premium value + # @param price [String] Ad price + # @param min_amount [String] Minimum order amount + # @param max_amount [String] Maximum order amount + # @param remark [String] Ad remark + # @param trading_preference_set [Hash] Trading preference settings + # @param payment_ids [Array] Payment method IDs + # @param action_type [String] Action type: MODIFY or ACTIVE + # @param quantity [String] Ad quantity + # @param payment_period [String] Payment period in minutes + def update_ad(id:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, action_type:, quantity:, payment_period:, **kwargs) + params = kwargs.merge(id: id, price_type: price_type, premium: premium, price: price, min_amount: min_amount, max_amount: max_amount, remark: remark, trading_preference_set: trading_preference_set, payment_ids: payment_ids, action_type: action_type, quantity: quantity, payment_period: payment_period) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/item/update', body: params) + end + + # Upload a file for use in a P2P order chat. + # + # POST /v5/p2p/oss/upload_file + # + # @param upload_file [String] File to upload + def upload_chat_file(upload_file:, **kwargs) + params = kwargs.merge(upload_file: upload_file) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/p2p/oss/upload_file', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/position_service.rb b/lib/bybit/rest_api/position_service.rb new file mode 100644 index 0000000..c6fee0b --- /dev/null +++ b/lib/bybit/rest_api/position_service.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class PositionService < BaseService + # Manually add or reduce margin for an isolated margin position + # + # POST /v5/position/add-margin + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param margin [String] Add or reduce. To add, then 10; to reduce, then -10 + # @option kwargs [Integer] :position_idx Used to identify positions in different position modes + # @see https://bybit-exchange.github.io/docs/v5/position/manual-add-margin + def add_reduce_margin(category:, symbol:, margin:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, margin: margin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/add-margin', body: params) + end + + # Confirm new risk limit to remove reduce-only restriction + # + # POST /v5/position/confirm-pending-mmr + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @see https://bybit-exchange.github.io/docs/v5/position/confirm-mmr + def confirm_new_risk_limit(category:, symbol:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/confirm-pending-mmr', body: params) + end + + # Get closed option position records + # + # GET /v5/position/get-closed-positions + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/position/close-position + def get_close_position(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/position/get-closed-positions', params: params) + end + + # Get closed profit and loss records + # + # GET /v5/position/closed-pnl + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/position/close-pnl + def get_closed_pnl(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/position/closed-pnl', params: params) + end + + # Get move position (block trade) history + # + # GET /v5/position/move-history + # + # @option kwargs [String] :category Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [String] :status Move position status + # @option kwargs [String] :block_trade_id Block trade ID + # @option kwargs [String] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/position/move-position-history + def get_move_position_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/position/move-history', params: params) + end + + # Get position info (real-time) + # + # GET /v5/position/list + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/position + def get_info_get(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/position/list', params: params) + end + + # Move positions between UIDs via block trade + # + # POST /v5/position/move-positions + # + # @param from_uid [String] Original UID (from which the position is moved) + # @param to_uid [String] Target UID (to which the position is moved) + # @param list [Array] Positions to move (array of objects) + # @see https://bybit-exchange.github.io/docs/v5/position/move-position + def move_position(from_uid:, to_uid:, list:, **kwargs) + params = kwargs.merge(from_uid: from_uid, to_uid: to_uid, list: list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/move-positions', body: params) + end + + # Enable or disable auto-add-margin for a position + # + # POST /v5/position/set-auto-add-margin + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param auto_add_margin [Integer] 0: disable, 1: enable + # @option kwargs [Integer] :position_idx Used to identify positions in different position modes + # @see https://bybit-exchange.github.io/docs/v5/position/auto-add-margin + def set_auto_add_margin(category:, symbol:, auto_add_margin:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, auto_add_margin: auto_add_margin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/set-auto-add-margin', body: params) + end + + # Set leverage for a position + # + # POST /v5/position/set-leverage + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param buy_leverage [String] Buy leverage + # @param sell_leverage [String] Sell leverage + # @see https://bybit-exchange.github.io/docs/v5/position/leverage + def set_leverage(category:, symbol:, buy_leverage:, sell_leverage:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, buy_leverage: buy_leverage, sell_leverage: sell_leverage) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/set-leverage', body: params) + end + + # Set take profit, stop loss, and trailing stop for a position + # + # POST /v5/position/trading-stop + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param tpsl_mode [String] TP/SL mode: Full or Partial + # @param position_idx [Integer] Used to identify positions in different position modes + # @option kwargs [String] :take_profit Take profit price + # @option kwargs [String] :stop_loss Stop loss price + # @option kwargs [String] :trailing_stop Trailing stop + # @option kwargs [String] :tp_trigger_by Take profit trigger price type + # @option kwargs [String] :sl_trigger_by Stop loss trigger price type + # @option kwargs [String] :active_price Trailing stop trigger price + # @option kwargs [String] :tp_size Take profit size (Partial mode) + # @option kwargs [String] :sl_size Stop loss size (Partial mode) + # @option kwargs [String] :tp_limit_price The limit order price when take profit price is triggered + # @option kwargs [String] :sl_limit_price The limit order price when stop loss price is triggered + # @option kwargs [String] :tp_order_type Take profit order type: Market or Limit + # @option kwargs [String] :sl_order_type Stop loss order type: Market or Limit + # @see https://bybit-exchange.github.io/docs/v5/position/trading-stop + def set_trading_stop(category:, symbol:, tpsl_mode:, position_idx:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, tpsl_mode: tpsl_mode, position_idx: position_idx) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/trading-stop', body: params) + end + + # Switch position mode between one-way and hedge mode + # + # POST /v5/position/switch-mode + # + # @param category [String] Product type + # @param mode [Integer] Position mode: 0 (Merged Single), 3 (Both Sides) + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :coin Coin + # @see https://bybit-exchange.github.io/docs/v5/position/position-mode + def switch_position_mode(category:, mode:, **kwargs) + params = kwargs.merge(category: category, mode: mode) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/position/switch-mode', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/rfq_service.rb b/lib/bybit/rest_api/rfq_service.rb new file mode 100644 index 0000000..666f9f1 --- /dev/null +++ b/lib/bybit/rest_api/rfq_service.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class RfqService < BaseService + # Accept a non-LP quote for an RFQ. + # + # POST /v5/rfq/accept-other-quote + # + # @param rfq_id [String] RFQ ID + def accept_non_lp_quote(rfq_id:, **kwargs) + params = kwargs.merge(rfq_id: rfq_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/accept-other-quote', body: params) + end + + # Cancel all active quotes. + # + # POST /v5/rfq/cancel-all-quotes + def cancel_all_quotes(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/cancel-all-quotes', body: params) + end + + # Cancel all active RFQs. + # + # POST /v5/rfq/cancel-all-rfq + def cancel_all_rfqs(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/cancel-all-rfq', body: params) + end + + # Cancel a specific quote by quote ID, RFQ ID, or quote link ID. + # + # POST /v5/rfq/cancel-quote + # + # @option kwargs [String] :quote_id Quote ID + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :quote_link_id User-defined quote link ID + def cancel_quote(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/cancel-quote', body: params) + end + + # Cancel a specific RFQ by RFQ ID or RFQ link ID. + # + # POST /v5/rfq/cancel-rfq + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + def cancel_rfq(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/cancel-rfq', body: params) + end + + # Create a quote in response to an RFQ. + # + # POST /v5/rfq/create-quote + # + # @param rfq_id [String] RFQ ID + # @option kwargs [String] :quote_link_id User-defined quote link ID + # @option kwargs [Boolean] :anonymous Whether to submit the quote anonymously + # @option kwargs [Integer] :expire_in Expiration time in seconds + # @option kwargs [Array] :quote_buy_list Buy side quote entries + # @option kwargs [Array] :quote_sell_list Sell side quote entries + def create_quote(rfq_id:, **kwargs) + params = kwargs.merge(rfq_id: rfq_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/create-quote', body: params) + end + + # Create a new RFQ (Request for Quote). + # + # POST /v5/rfq/create-rfq + # + # @param counterparties [Array] List of counterparties to receive the RFQ + # @param list [Array] List of legs for the RFQ + # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + # @option kwargs [Boolean] :anonymous Whether to submit the RFQ anonymously + # @option kwargs [String] :strategy_type Strategy type + def create_rfq(counterparties:, list:, **kwargs) + params = kwargs.merge(counterparties: counterparties, list: list) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/create-rfq', body: params) + end + + # Execute a quote to complete the trade. + # + # POST /v5/rfq/execute-quote + # + # @param rfq_id [String] RFQ ID + # @param quote_id [String] Quote ID + # @param quote_side [String] Quote side (Buy or Sell) + def execute_quote(rfq_id:, quote_id:, quote_side:, **kwargs) + params = kwargs.merge(rfq_id: rfq_id, quote_id: quote_id, quote_side: quote_side) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/rfq/execute-quote', body: params) + end + + # Query public RFQ trades. + # + # GET /v5/rfq/public-trades + # + # @option kwargs [Integer] :start_time Start timestamp in milliseconds + # @option kwargs [Integer] :end_time End timestamp in milliseconds + # @option kwargs [Integer] :limit Maximum number of records to return + # @option kwargs [String] :cursor Pagination cursor + def get_public_trades(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/public-trades', params: params) + end + + # Query historical quotes. + # + # GET /v5/rfq/quote-list + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :quote_id Quote ID + # @option kwargs [String] :quote_link_id User-defined quote link ID + # @option kwargs [String] :trader_type Trader type (Taker or Maker) + # @option kwargs [String] :status Quote status filter + # @option kwargs [Integer] :limit Maximum number of records to return + # @option kwargs [String] :cursor Pagination cursor + def get_quotes(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/quote-list', params: params) + end + + # Query realtime (active) quotes. + # + # GET /v5/rfq/quote-realtime + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :quote_id Quote ID + # @option kwargs [String] :quote_link_id User-defined quote link ID + # @option kwargs [String] :trader_type Trader type (Taker or Maker) + def get_quotes_realtime(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/quote-realtime', params: params) + end + + # Query RFQ configuration information. + # + # GET /v5/rfq/config + def get_config(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/config', params: params) + end + + # Query historical RFQs. + # + # GET /v5/rfq/rfq-list + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + # @option kwargs [String] :trader_type Trader type (Taker or Maker) + # @option kwargs [String] :status RFQ status filter + # @option kwargs [Integer] :limit Maximum number of records to return + # @option kwargs [String] :cursor Pagination cursor + def get_rfqs(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/rfq-list', params: params) + end + + # Query realtime (active) RFQs. + # + # GET /v5/rfq/rfq-realtime + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + # @option kwargs [String] :trader_type Trader type (Taker or Maker) + def get_rfqs_realtime(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/rfq-realtime', params: params) + end + + # Query RFQ trade history. + # + # GET /v5/rfq/trade-list + # + # @option kwargs [String] :rfq_id RFQ ID + # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + # @option kwargs [String] :quote_id Quote ID + # @option kwargs [String] :quote_link_id User-defined quote link ID + # @option kwargs [String] :trader_type Trader type (Taker or Maker) + # @option kwargs [String] :status Trade status filter + # @option kwargs [Integer] :limit Maximum number of records to return + # @option kwargs [String] :cursor Pagination cursor + def get_trade_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/rfq/trade-list', params: params) + end + end + end +end diff --git a/lib/bybit/rest_api/spot_margin_service.rb b/lib/bybit/rest_api/spot_margin_service.rb new file mode 100644 index 0000000..dc045c3 --- /dev/null +++ b/lib/bybit/rest_api/spot_margin_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class SpotMarginService < BaseService + # Get historical interest rate for spot margin trading. + # + # GET /v5/spot-margin-trade/interest-rate-history + # + # @param currency [String] Currency + # @option kwargs [String] :vip_level VIP level + # @option kwargs [Integer] :start_time Start time in milliseconds + # @option kwargs [Integer] :end_time End time in milliseconds + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/historical-interest + def get_historical_interest_rate(currency:, **kwargs) + params = kwargs.merge(currency: currency) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/interest-rate-history', params: params) + end + + # Get spot margin position tiers. + # + # GET /v5/spot-margin-trade/position-tiers + # + # @option kwargs [String] :currency Currency + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/position-tiers + def get_position_tiers(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/position-tiers', params: params) + end + + # Get tiered collateral ratio for spot margin trading. + # + # GET /v5/spot-margin-trade/collateral + # + # @option kwargs [String] :currency Currency + def get_tiered_collateral_ratio(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/spot-margin-trade/collateral', params: params) + end + + # Get VIP margin data for spot margin trading. + # + # GET /v5/spot-margin-trade/data + # + # @option kwargs [String] :vip_level VIP level + # @option kwargs [String] :currency Currency + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/vip-margin + def get_vip_margin_data(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/spot-margin-trade/data', params: params) + end + end + end +end diff --git a/lib/bybit/rest_api/trade_service.rb b/lib/bybit/rest_api/trade_service.rb new file mode 100644 index 0000000..af85064 --- /dev/null +++ b/lib/bybit/rest_api/trade_service.rb @@ -0,0 +1,276 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class TradeService < BaseService + # Get Trade History + # + # GET /v5/execution/list + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [Integer] :start_time The start timestamp (ms) + # @option kwargs [Integer] :end_time The end timestamp (ms) + # @option kwargs [String] :exec_type Execution type + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/order/execution + def get_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/execution/list', params: params) + end + + # Amend an open unfilled or partially filled order. + # + # POST /v5/order/amend + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :order_iv Implied volatility (options) + # @option kwargs [String] :trigger_price Trigger price + # @option kwargs [String] :qty Order quantity + # @option kwargs [String] :price Order price + # @option kwargs [String] :tpsl_mode TP/SL mode + # @option kwargs [String] :take_profit Take profit price + # @option kwargs [String] :stop_loss Stop loss price + # @option kwargs [String] :tp_trigger_by Take profit trigger price type + # @option kwargs [String] :sl_trigger_by Stop loss trigger price type + # @option kwargs [String] :trigger_by Trigger price type + # @option kwargs [String] :tp_limit_price Take profit limit price + # @option kwargs [String] :sl_limit_price Stop loss limit price + # @see https://bybit-exchange.github.io/docs/v5/order/amend-order + def amend_order(category:, symbol:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/amend', body: params) + end + + # Batch amend multiple orders in a single request. + # + # POST /v5/order/amend-batch + # + # @param category [String] Product type + # @param request [Array] Array of order amend objects + # @see https://bybit-exchange.github.io/docs/v5/order/batch-amend + def batch_amend_orders(category:, request:, **kwargs) + params = kwargs.merge(category: category, request: request) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/amend-batch', body: params) + end + + # Batch cancel multiple orders in a single request. + # + # POST /v5/order/cancel-batch + # + # @param category [String] Product type + # @param request [Array] Array of order cancel objects + # @see https://bybit-exchange.github.io/docs/v5/order/batch-cancel + def batch_cancel_orders(category:, request:, **kwargs) + params = kwargs.merge(category: category, request: request) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/cancel-batch', body: params) + end + + # Batch place multiple orders in a single request. + # + # POST /v5/order/create-batch + # + # @param category [String] Product type + # @param request [Array] Array of order create objects + # @see https://bybit-exchange.github.io/docs/v5/order/batch-place + def batch_create_orders(category:, request:, **kwargs) + params = kwargs.merge(category: category, request: request) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/create-batch', body: params) + end + + # Cancel all open orders for a product line, symbol, or coin. + # + # POST /v5/order/cancel-all + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [String] :order_filter Order filter + # @option kwargs [String] :stop_order_type Stop order type + # @see https://bybit-exchange.github.io/docs/v5/order/cancel-all + def cancel_all_orders(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/cancel-all', body: params) + end + + # Cancel a single open order by orderId or orderLinkId. + # + # POST /v5/order/cancel + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :order_filter Order filter + # @see https://bybit-exchange.github.io/docs/v5/order/cancel-order + def cancel_order(category:, symbol:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/cancel', body: params) + end + + # Place a new order for spot, linear, inverse, or option products. + # + # POST /v5/order/create + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param side [String] Buy or Sell + # @param order_type [String] Market or Limit + # @param qty [String] Order quantity + # @option kwargs [Integer] :is_leverage Whether to borrow (spot margin) + # @option kwargs [String] :market_unit Unit for spot market order quantity + # @option kwargs [String] :slippage_tolerance_type Slippage tolerance type + # @option kwargs [String] :slippage_tolerance Slippage tolerance value + # @option kwargs [String] :price Order price + # @option kwargs [Integer] :trigger_direction Conditional order trigger direction + # @option kwargs [String] :order_filter Order filter + # @option kwargs [String] :trigger_price Trigger price + # @option kwargs [String] :trigger_by Trigger price type + # @option kwargs [String] :order_iv Implied volatility (options) + # @option kwargs [String] :time_in_force Time in force + # @option kwargs [Integer] :position_idx Position mode index + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :take_profit Take profit price + # @option kwargs [String] :stop_loss Stop loss price + # @option kwargs [String] :tp_trigger_by Take profit trigger price type + # @option kwargs [String] :sl_trigger_by Stop loss trigger price type + # @option kwargs [Boolean] :reduce_only Reduce-only flag + # @option kwargs [Boolean] :close_on_trigger Close on trigger flag + # @option kwargs [String] :smp_type Self match prevention type + # @option kwargs [Boolean] :mmp Market maker protection flag (options) + # @option kwargs [String] :tpsl_mode TP/SL mode + # @option kwargs [String] :tp_limit_price Take profit limit price + # @option kwargs [String] :sl_limit_price Stop loss limit price + # @option kwargs [String] :tp_order_type Take profit order type + # @option kwargs [String] :sl_order_type Stop loss order type + # @option kwargs [String] :bbo_side_type BBO side type + # @option kwargs [String] :bbo_level BBO level + # @option kwargs [Boolean] :rpi_taker_access RPI taker access flag + # @see https://bybit-exchange.github.io/docs/v5/order/create-order + def create_order(category:, symbol:, side:, order_type:, qty:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, side: side, order_type: order_type, qty: qty) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/create', body: params) + end + + # Set DCP time window (alias endpoint for Disconnected Cancel All Protection). + # + # POST /v5/order/disconnected-cancel-all + # + # @param time_window [Integer] DCP time window in seconds + # @option kwargs [String] :product Product type for DCP + # @see https://bybit-exchange.github.io/docs/v5/order/dcp + def dcp_set_timewindow(time_window:, **kwargs) + params = kwargs.merge(time_window: time_window) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/disconnected-cancel-all', body: params) + end + + # Query unfilled or partially filled orders in real-time. + # + # GET /v5/order/realtime + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [Integer] :open_only Open-only filter + # @option kwargs [String] :order_filter Order filter + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/order/open-order + def get_open_orders(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/order/realtime', params: params) + end + + # Query historical orders that have been fully filled or cancelled. + # + # GET /v5/order/history + # + # @param category [String] Product type + # @option kwargs [String] :symbol Symbol name + # @option kwargs [String] :base_coin Base coin + # @option kwargs [String] :settle_coin Settle coin + # @option kwargs [String] :order_id Order ID + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :order_filter Order filter + # @option kwargs [String] :order_status Order status + # @option kwargs [Integer] :start_time Start time (ms) + # @option kwargs [Integer] :end_time End time (ms) + # @option kwargs [Integer] :limit Limit for data size per page + # @option kwargs [String] :cursor Cursor for pagination + # @see https://bybit-exchange.github.io/docs/v5/order/order-list + def get_order_history_get(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/order/history', params: params) + end + + # Query the available borrow quota for spot margin trading. + # + # GET /v5/order/spot-borrow-check + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param side [String] Buy or Sell + # @see https://bybit-exchange.github.io/docs/v5/order/spot-borrow-quota + def get_spot_borrow_quota(category:, symbol:, side:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, side: side) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/order/spot-borrow-check', params: params) + end + + # Pre-check an order before submission to validate parameters and margin. + # + # POST /v5/order/pre-check + # + # @param category [String] Product type + # @param symbol [String] Symbol name + # @param side [String] Buy or Sell + # @param order_type [String] Market or Limit + # @param qty [String] Order quantity + # @option kwargs [String] :price Order price + # @option kwargs [Integer] :is_leverage Whether to borrow (spot margin) + # @option kwargs [String] :time_in_force Time in force + # @option kwargs [Integer] :position_idx Position mode index + # @option kwargs [String] :order_link_id User customised order ID + # @option kwargs [String] :take_profit Take profit price + # @option kwargs [String] :stop_loss Stop loss price + # @option kwargs [String] :tp_trigger_by Take profit trigger price type + # @option kwargs [String] :sl_trigger_by Stop loss trigger price type + # @option kwargs [Boolean] :reduce_only Reduce-only flag + # @option kwargs [String] :tpsl_mode TP/SL mode + # @option kwargs [String] :tp_limit_price Take profit limit price + # @option kwargs [String] :sl_limit_price Stop loss limit price + # @option kwargs [String] :tp_order_type Take profit order type + # @option kwargs [String] :sl_order_type Stop loss order type + # @option kwargs [String] :order_iv Implied volatility (options) + # @see https://bybit-exchange.github.io/docs/v5/order/pre-check-order + def pre_check_order(category:, symbol:, side:, order_type:, qty:, **kwargs) + params = kwargs.merge(category: category, symbol: symbol, side: side, order_type: order_type, qty: qty) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/order/pre-check', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/user_service.rb b/lib/bybit/rest_api/user_service.rb new file mode 100644 index 0000000..9bcc51a --- /dev/null +++ b/lib/bybit/rest_api/user_service.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + class UserService < BaseService + # Create Sub API Key. + # + # POST /v5/user/create-sub-api + # + # @param subuid [Integer] Sub UID + # @param read_only [Integer] Read-only permission flag (0: read-write, 1: read-only) + # @param permissions [Hash] API key permission settings + # @option kwargs [String] :ips Bound IP addresses (comma-separated) + # @option kwargs [String] :note API key note + # @see https://bybit-exchange.github.io/docs/v5/user/create-subuid-apikey + def create_sub_api_key(subuid:, read_only:, permissions:, **kwargs) + params = kwargs.merge(subuid: subuid, read_only: read_only, permissions: permissions) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/create-sub-api', body: params) + end + + # Create Sub UID. + # + # POST /v5/user/create-sub-member + # + # @param username [String] Sub-account username + # @param member_type [Integer] Sub-account type (1: normal, 6: custodial) + # @option kwargs [String] :password Sub-account login password + # @option kwargs [Integer] :switch Quick login toggle + # @option kwargs [Boolean] :is_uta Whether to create as UTA account + # @option kwargs [String] :note Sub-account note + # @see https://bybit-exchange.github.io/docs/v5/user/create-subuid + def create_sub_member(username:, member_type:, **kwargs) + params = kwargs.merge(username: username, member_type: member_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/create-sub-member', body: params) + end + + # Delete Master API Key. + # + # POST /v5/user/delete-api + # + # @option kwargs [String] :apikey API key to delete + # @see https://bybit-exchange.github.io/docs/v5/user/rm-master-apikey + def delete_api_key(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/delete-api', body: params) + end + + # Delete Sub-account API Key. + # + # POST /v5/user/delete-sub-api + # + # @param subuid [Integer] Sub UID + # @option kwargs [String] :apikey Sub-account API key to delete + # @see https://bybit-exchange.github.io/docs/v5/user/rm-sub-apikey + def delete_sub_api_key(subuid:, **kwargs) + params = kwargs.merge(subuid: subuid) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/delete-sub-api', body: params) + end + + # Delete Sub-account. + # + # POST /v5/user/del-submember + # + # @param subuid [Integer] Sub UID to delete + def delete_sub_member_v5(subuid:, **kwargs) + params = kwargs.merge(subuid: subuid) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/del-submember', body: params) + end + + # Freeze or unfreeze a Sub UID. + # + # POST /v5/user/frozen-sub-member + # + # @param subuid [Integer] Sub UID + # @param frozen [Integer] Freeze flag (0: unfreeze, 1: freeze) + # @see https://bybit-exchange.github.io/docs/v5/user/froze-subuid + def frozen_sub_member(subuid:, frozen:, **kwargs) + params = kwargs.merge(subuid: subuid, frozen: frozen) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/frozen-sub-member', body: params) + end + + # Get Affiliate User Info. + # + # GET /v5/user/aff-customer-info + # + # @param uid [String] Affiliate user UID + # @option kwargs [String] :coin Coin filter + # @option kwargs [String] :business Business type filter + def get_affiliate_custom_open_info_v5(uid:, **kwargs) + params = kwargs.merge(uid: uid) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/aff-customer-info', params: params) + end + + # Get Member Account Type. + # + # GET /v5/user/get-member-type + # + # @option kwargs [String] :member_ids Comma-separated member IDs + def get_member_account_type(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/get-member-type', params: params) + end + + # List Sub-account API Keys. + # + # GET /v5/user/sub-apikeys + # + # @param subuid [Integer] Sub UID + # @option kwargs [Integer] :limit Page limit + # @option kwargs [String] :cursor Pagination cursor + # @see https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys + def list_sub_api_keys_v5(subuid:, **kwargs) + params = kwargs.merge(subuid: subuid) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/sub-apikeys', params: params) + end + + # Get API Key Information. + # + # GET /v5/user/query-api + # + # @see https://bybit-exchange.github.io/docs/v5/user/apikey-info + def query_api_key + @session.sign_request(method: :get, path: '/v5/user/query-api') + end + + # Query Escrow Sub-accounts (Fund Management). + # + # GET /v5/user/escrow_sub_members + # + # @option kwargs [Integer] :next_cursor Pagination cursor + # @option kwargs [Integer] :page_size Page size + def query_escrow_sub_members_v5(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/escrow_sub_members', params: params) + end + + # Query Referrals. + # + # GET /v5/user/invitation/referrals + # + # @option kwargs [String] :cursor Pagination cursor + # @option kwargs [Integer] :size Page size + # @option kwargs [String] :status Referral status filter + def query_referrals(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/invitation/referrals', params: params) + end + + # Query Sub UID List. + # + # GET /v5/user/query-sub-members + # + # @see https://bybit-exchange.github.io/docs/v5/user/subuid-list + def query_sub_members + @session.sign_request(method: :get, path: '/v5/user/query-sub-members') + end + + # Query Sub-accounts List. + # + # GET /v5/user/submembers + # + # @option kwargs [Integer] :page_size Page size + # @option kwargs [Integer] :next_cursor Pagination cursor + def query_sub_members_v5(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/user/submembers', params: params) + end + + # Sign Agreement + # + # POST /v5/user/agreement + # + # @param category [Integer] Agreement category + # @param agree [Boolean] Whether to agree + def sign_agreement(category:, agree:, **kwargs) + params = kwargs.merge(category: category, agree: agree) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/agreement', body: params) + end + + # Modify Master API Key + # + # POST /v5/user/update-api + # + # @option kwargs [Integer] :read_only Read-only flag + # @option kwargs [String] :ips Bound IP list + # @option kwargs [Hash] :permissions Permissions object + # @see https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey + def update_api_key(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/update-api', body: params) + end + + # Modify Sub-account API Key + # + # POST /v5/user/update-sub-api + # + # @param subuid [Integer] Sub UID + # @param read_only [Integer] Read-only flag + # @option kwargs [String] :apikey Sub-account API key + # @option kwargs [String] :ips Bound IP list + # @option kwargs [Hash] :permissions Permissions object + # @option kwargs [String] :note Note + # @see https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey + def update_sub_api_key(subuid:, read_only:, **kwargs) + params = kwargs.merge(subuid: subuid, read_only: read_only) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/user/update-sub-api', body: params) + end + end + end +end diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb new file mode 100644 index 0000000..75b709e --- /dev/null +++ b/lib/bybit/session.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'faraday' +require 'json' +require 'uri' + +module Bybit + # Session owns the Faraday connection + auth-header assembly. Every service + # class receives a Session instance and dispatches through public_request / + # sign_request. Modeled after binance-connector-ruby's Session pattern. + class Session + SENSITIVE_HEADERS = %w[ + x-bapi-api-key x-bapi-sign x-bapi-timestamp x-bapi-recv-window + x-bapi-sign-type authorization cookie set-cookie + ].freeze + + def initialize(config) + @config = config + @conn = config.faraday_connection || build_connection + end + + # Public unsigned endpoint — no X-BAPI-* headers attached. + # session.public_request(path: '/v5/market/kline', params: {...}) + def public_request(method: :get, path:, params: nil, body: nil) + dispatch(method: method, path: path, signed: false, params: params, body: body) + end + + # Signed endpoint — X-BAPI-* headers computed via Authentication. + def sign_request(method:, path:, params: nil, body: nil) + dispatch(method: method, path: path, signed: true, params: params, body: body) + end + + private + + def dispatch(method:, path:, signed:, params:, body:) + clean_params = compact(params) + clean_body = compact(body) + query_str = clean_params ? URI.encode_www_form(clean_params) : '' + body_str = clean_body ? JSON.generate(clean_body) : '' + headers = build_headers(signed: signed, method: method, query_str: query_str, body_str: body_str) + + resp = @conn.send(method) do |req| + req.url path + req.params = clean_params if clean_params && [:get, :delete].include?(method) + req.headers.update(headers) + req.body = body_str if clean_body + end + parse_response(resp) + rescue Faraday::TimeoutError => e + raise Bybit::TimeoutError, e.message + rescue Faraday::ConnectionFailed, Faraday::SSLError => e + raise Bybit::NetworkError, e.message + end + + def build_headers(signed:, method:, query_str:, body_str:) + h = {} + h['Content-Type'] = 'application/json' unless body_str.empty? + return h unless signed + if @config.api_key.nil? || @config.api_secret.nil? + raise ArgumentError, 'signed endpoint requires api_key + api_secret' + end + ts = (Time.now.to_f * 1000).to_i.to_s + payload = method == :get ? query_str : body_str + h['X-BAPI-API-KEY'] = @config.api_key + h['X-BAPI-TIMESTAMP'] = ts + h['X-BAPI-RECV-WINDOW'] = @config.recv_window.to_s + h['X-BAPI-SIGN'] = Authentication.sign_v5( + @config.api_secret, ts, @config.api_key, @config.recv_window.to_s, payload + ) + h['X-BAPI-SIGN-TYPE'] = '2' + h + end + + def parse_response(response) + body = response.body + body = safe_parse_json(body) if body.is_a?(String) + unless body.is_a?(Hash) && body['retCode'].is_a?(Integer) + raise Bybit::ParseError.new( + "Response is not a valid Bybit V5 ApiResponse (status=#{response.status})", + body: body, http_status: response.status + ) + end + return body if body['retCode'] == 0 + raise Bybit.api_error_from(body, http_status: response.status) + end + + def safe_parse_json(str) + JSON.parse(str) + rescue JSON::ParserError + nil + end + + def compact(hash) + return nil if hash.nil? + hash.reject { |_, v| v.nil? } + end + + def build_connection + Faraday.new(url: @config.resolved_base_url, request: { timeout: @config.timeout }) + end + end +end diff --git a/lib/bybit/utils/wire_keys.rb b/lib/bybit/utils/wire_keys.rb new file mode 100644 index 0000000..6aa4db2 --- /dev/null +++ b/lib/bybit/utils/wire_keys.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Bybit + module Utils + # snake_case <-> camelCase key translation for wire payloads. + # Bot-family endpoints keep snake_case (identity mapping) — call sites + # skip WireKeys.camelize. Everyone else runs camelize on the params/body + # hash right before dispatch. + module WireKeys + module_function + + # Convert every key of a hash from :some_key (Symbol) or 'some_key' (String) + # to camelCase :someKey / 'someKey'. Non-string / non-symbol keys pass through. + def camelize(hash) + return hash if hash.nil? + hash.each_with_object({}) do |(k, v), out| + out[to_camel(k)] = v + end + end + + def to_camel(key) + return key unless key.is_a?(Symbol) || key.is_a?(String) + parts = key.to_s.split('_') + return key if parts.size < 2 + camel = parts[0] + parts[1..].map { |p| p.empty? ? '' : (p[0].upcase + p[1..].to_s) }.join + key.is_a?(Symbol) ? camel.to_sym : camel + end + end + end +end diff --git a/lib/bybit/version.rb b/lib/bybit/version.rb new file mode 100644 index 0000000..e10b1aa --- /dev/null +++ b/lib/bybit/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Bybit + VERSION = '0.1.0' +end diff --git a/spec/bybit_spec.rb b/spec/bybit_spec.rb new file mode 100644 index 0000000..128cb8d --- /dev/null +++ b/spec/bybit_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.describe Bybit do + it 'has a version number' do + expect(Bybit::VERSION).not_to be_nil + end + + it 'builds a client with defaults' do + Bybit.configure { |c| c.testnet = true } + client = Bybit::Client.new + expect(client).to be_a(Bybit::Client) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..459a5e4 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bybit' + +RSpec.configure do |config| + config.expect_with :rspec do |c| + c.syntax = :expect + end + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end +end From c122ffd695df448d3038152a80a801b755bb5d19 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 15:01:11 +0800 Subject: [PATCH 02/18] optimization --- .ruby-version | 2 +- Gemfile | 2 + README.md | 174 +++++++++++++++++++++- Rakefile | 2 + bybit-connector-ruby.gemspec | 12 +- examples/quickstart.rb | 25 +++- lib/bybit/configuration.rb | 28 +++- lib/bybit/error.rb | 26 +++- lib/bybit/rest_api/crypto_loan_service.rb | 4 +- lib/bybit/rest_api/earn_service.rb | 2 +- lib/bybit/rest_api/p2p_service.rb | 2 +- lib/bybit/rest_api/position_service.rb | 2 +- lib/bybit/rest_api/trade_service.rb | 4 +- lib/bybit/rest_api/user_service.rb | 13 +- lib/bybit/session.rb | 91 ++++++++--- lib/bybit/utils/wire_keys.rb | 39 ++++- spec/authentication_spec.rb | 36 +++++ spec/session_spec.rb | 100 +++++++++++++ spec/spec_helper.rb | 11 ++ spec/wire_keys_spec.rb | 32 ++++ 20 files changed, 560 insertions(+), 47 deletions(-) create mode 100644 spec/authentication_spec.rb create mode 100644 spec/session_spec.rb create mode 100644 spec/wire_keys_spec.rb diff --git a/.ruby-version b/.ruby-version index 4a36342..fa7adc7 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.0 +3.3.5 diff --git a/Gemfile b/Gemfile index fa75df1..7f4f5e9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/README.md b/README.md index 69e4a4e..63761a4 100644 --- a/README.md +++ b/README.md @@ -1 +1,173 @@ -# bybit.ruby.api \ No newline at end of file +# bybit-connector-ruby + +Official lightweight Ruby connector for the [Bybit V5 REST API](https://bybit-exchange.github.io/docs/v5/intro). + +`bybit-connector-ruby` wraps the Bybit V5 HTTP endpoints as a set of typed Ruby methods with keyword-arg signatures. Its goal is the same as [`pybit`](https://github.com/bybit-exchange/pybit) on the Python side: an easy-to-use, high-performance connector with a small dependency footprint. + +## Installation + +Ruby ≥ 3.0 is required (Ruby 3.3.x recommended). + +``` +gem install bybit-connector-ruby +``` + +Or in your Gemfile: + +```ruby +gem 'bybit-connector-ruby' +``` + +## Quick Start + +```ruby +require 'bybit' + +Bybit.configure do |c| + c.api_key = ENV['BYBIT_KEY'] + c.api_secret = ENV['BYBIT_SECRET'] + c.testnet = true # omit / false for mainnet +end + +client = Bybit::Client.new + +# Public endpoint — no auth needed +puts client.market.get_server_time + +# Signed endpoint — apiKey + apiSecret required +wallet = client.account.get_wallet_balance(account_type: 'UNIFIED') +puts wallet['result']['list'] + +# Place an order +order = client.trade.create_order( + category: 'linear', symbol: 'BTCUSDT', + side: 'Buy', order_type: 'Limit', qty: '0.01', + price: '30000', time_in_force: 'GTC', +) +puts "orderId: #{order['result']['orderId']}" +``` + +See `examples/quickstart.rb` for a runnable script. + +## Configuration + +All options live on `Bybit::Configuration`. Either configure globally with the block: + +```ruby +Bybit.configure do |c| + c.api_key = ENV['BYBIT_KEY'] + c.api_secret = ENV['BYBIT_SECRET'] + c.testnet = false # default false + c.recv_window = '5000' # ms, X-BAPI-RECV-WINDOW header + c.timeout = 10 # Faraday timeout, seconds +end +``` + +Or pass overrides to `Client.new`: + +```ruby +client = Bybit::Client.new(api_key: '...', api_secret: '...', testnet: true) +``` + +Bring your own Faraday connection to inject retries / logging / adapters: + +```ruby +conn = Faraday.new('https://api-testnet.bybit.com') do |f| + f.request :retry, max: 3 + f.response :logger +end +Bybit.configure { |c| c.faraday_connection = conn } +``` + +Base URLs (exported constants): + +- `Bybit::BASE_URL_MAINNET` — `https://api.bybit.com` +- `Bybit::BASE_URL_TESTNET` — `https://api-testnet.bybit.com` + +## Services + +Each API group is a property on `Bybit::Client`: + +- `client.market` — public market data (kline, tickers, orderbook, instruments-info, ...) +- `client.trade` — orders (create / amend / cancel / batch / history) +- `client.position` — positions, leverage, TP/SL, move-position +- `client.account` — wallet, margin, collateral, fee-rate, transaction log +- `client.asset` — coin balance, coin greeks, funding history +- `client.user` — sub-accounts, API-key management +- `client.affiliate` — sub-affiliate lists +- `client.broker` — broker earnings, distributions +- `client.crypto_loan` — flexible / fixed crypto loans +- `client.rfq` — request-for-quote (block trades) +- `client.spot_margin` — UTA spot margin +- `client.earn` — earn, liquidity mining, RWA, PWM, hold-to-earn +- `client.p2p` — P2P advertise / order / chat +- `client.bot` — DCA / grid / futures-combo / futures-grid / martingale + +## Error Handling + +Every failure is a subclass of `Bybit::Error`: + +```ruby +begin + client.trade.create_order(category: 'linear', symbol: 'BTCUSDT', side: 'Buy', ...) +rescue Bybit::AuthError => e # retCode 10003/10004/10005/... or HTTP 401/403 + # bad key / bad sign / permission +rescue Bybit::RateLimitError => e # retCode 10006 / 10018 or HTTP 429 + sleep 1 && retry +rescue Bybit::TimeoutError => e # Faraday::TimeoutError +rescue Bybit::NetworkError => e # Faraday::ConnectionFailed / SSLError +rescue Bybit::ServerError => e # HTTP 5xx w/ non-JSON body +rescue Bybit::ClientError => e # HTTP 4xx w/ non-JSON body (WAF, CDN, etc.) +rescue Bybit::ParseError => e # unrecognized body shape; e.body holds raw payload +rescue Bybit::ApiError => e # any other retCode != 0 — catch-all API error +end +``` + +Full hierarchy: + +- `Bybit::Error` (StandardError) + - `Bybit::ConfigurationError` — missing api_key / conflicting options + - `Bybit::TransportError` + - `Bybit::TimeoutError` + - `Bybit::NetworkError` + - `Bybit::ServerError` (5xx w/o body) + - `Bybit::ClientError` (non-auth 4xx w/o body) + - `Bybit::ParseError` — body did not parse or shape mismatch (has `#body`, `#http_status`) + - `Bybit::ApiError` — Bybit V5 body with retCode != 0 + - `Bybit::AuthError` + - `Bybit::RateLimitError` + +Every `ApiError` exposes `#ret_code`, `#ret_msg`, `#result`, `#time`, `#http_status`. See the [Bybit V5 error-code list](https://bybit-exchange.github.io/docs/v5/error) for meanings. + +## Return Value + +Every service method returns the raw parsed JSON as a `Hash`: + +```ruby +response = client.market.get_kline(category: 'spot', symbol: 'BTCUSDT', interval: '1') +response['retCode'] # => 0 +response['retMsg'] # => 'OK' +response['result'] # => { 'category' => 'spot', 'symbol' => 'BTCUSDT', 'list' => [...] } +response['time'] # => 1234567890000 +``` + +## Testnet + +Toggle `testnet: true` for [https://testnet.bybit.com](https://testnet.bybit.com): + +```ruby +client = Bybit::Client.new(api_key: ENV['BYBIT_TESTNET_KEY'], api_secret: ENV['BYBIT_TESTNET_SECRET'], testnet: true) +``` + +## Development + +``` +bundle install +bundle exec rspec # tests +bundle exec rubocop # lint +bundle exec rake # both +``` + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/Rakefile b/Rakefile index d9ebf5b..9166c5e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'bundler/gem_tasks' require 'rspec/core/rake_task' require 'rubocop/rake_task' diff --git a/bybit-connector-ruby.gemspec b/bybit-connector-ruby.gemspec index f27a075..fc80369 100644 --- a/bybit-connector-ruby.gemspec +++ b/bybit-connector-ruby.gemspec @@ -6,12 +6,12 @@ Gem::Specification.new do |s| s.name = 'bybit-connector-ruby' s.version = Bybit::VERSION s.summary = 'Official Bybit V5 REST API connector for Ruby' - s.description = 'Ruby SDK for the Bybit V5 REST API with typed keyword-arg method signatures.' + s.description = 'Ruby connector for the Bybit V5 REST API — keyword-arg method signatures, HMAC-SHA256 signing, typed error hierarchy, Faraday-based transport.' s.authors = ['Bybit'] s.license = 'MIT' s.homepage = 'https://github.com/bybit-exchange/bybit.ruby.api' s.required_ruby_version = '>= 3.0' - s.files = Dir['lib/**/*.rb', 'LICENSE*', 'README.md', 'CHANGELOG.md'] + s.files = Dir['lib/**/*.rb', 'LICENSE*', 'README.md', 'CHANGELOG.md', 'examples/**/*.rb'] s.require_paths = ['lib'] s.metadata = { 'source_code_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api', @@ -20,7 +20,9 @@ Gem::Specification.new do |s| 'rubygems_mfa_required' => 'true' } s.add_dependency 'faraday', '~> 2.0' - s.add_development_dependency 'rspec', '~> 3.12' - s.add_development_dependency 'rubocop', '~> 1.60' - s.add_development_dependency 'yard', '~> 0.9' + s.add_development_dependency 'rspec', '~> 3.12' + s.add_development_dependency 'rubocop', '~> 1.60' + s.add_development_dependency 'yard', '~> 0.9' + s.add_development_dependency 'webmock', '~> 3.0' + s.add_development_dependency 'simplecov', '~> 0.22' end diff --git a/examples/quickstart.rb b/examples/quickstart.rb index caf4688..90ba83a 100644 --- a/examples/quickstart.rb +++ b/examples/quickstart.rb @@ -1,12 +1,35 @@ # frozen_string_literal: true +# End-to-end quickstart — public + signed calls + rescue matrix. +# Run with: BYBIT_KEY=... BYBIT_SECRET=... ruby examples/quickstart.rb + require 'bybit' Bybit.configure do |c| c.api_key = ENV['BYBIT_KEY'] c.api_secret = ENV['BYBIT_SECRET'] - c.testnet = true + c.testnet = true # flip to false for mainnet end client = Bybit::Client.new + +# 1. Public endpoint — server time (no auth needed) +puts '--- server time ---' puts client.market.get_server_time.inspect + +# 2. Signed endpoint — wallet balance +puts '--- wallet balance ---' +begin + wallet = client.account.get_wallet_balance(account_type: 'UNIFIED') + puts wallet['result']['list'].inspect +rescue Bybit::AuthError => e + warn "auth failed: [#{e.ret_code}] #{e.ret_msg}" +rescue Bybit::RateLimitError => e + warn "rate-limited: #{e.ret_msg}" +rescue Bybit::TimeoutError => e + warn "timeout: #{e.message}" +rescue Bybit::NetworkError => e + warn "network: #{e.message}" +rescue Bybit::ApiError => e + warn "api error [#{e.ret_code}]: #{e.ret_msg}" +end diff --git a/lib/bybit/configuration.rb b/lib/bybit/configuration.rb index a8b3ac5..3d2f527 100644 --- a/lib/bybit/configuration.rb +++ b/lib/bybit/configuration.rb @@ -7,6 +7,11 @@ module Bybit DEFAULT_TIMEOUT = 10 class Configuration + # Attributes safe to expose in serialized form. Anything not in this + # allowlist is dropped from to_json / as_json / marshal_dump — no accidental + # secret leakage through pino/winston-style default serializers. + SAFE_ATTRS = %i[testnet base_url recv_window timeout].freeze + attr_accessor :api_key, :api_secret, :testnet, :base_url, :recv_window, :timeout, :faraday_connection @@ -20,6 +25,25 @@ def resolved_base_url base_url || (testnet ? BASE_URL_TESTNET : BASE_URL_MAINNET) end + # Serialization-safe hash used by as_json / to_json / marshal_dump. + def to_h_safe + { + api_key: redact(@api_key), + api_secret: redact(@api_secret), + testnet: @testnet, + base_url: resolved_base_url, + recv_window: @recv_window, + timeout: @timeout, + } + end + + def to_json(*args); require 'json'; to_h_safe.to_json(*args); end + def as_json(*_); to_h_safe.transform_keys(&:to_s); end + def marshal_dump; to_h_safe; end + # Un-marshalling a redacted Configuration is intentionally lossy — callers + # should never round-trip credentials through Marshal. + def marshal_load(h); h.each { |k, v| instance_variable_set("@#{k}", v) }; end + # Redact credentials from #inspect and #to_s so a stray puts/logger call # doesn't leak the secret into log aggregation. def inspect @@ -30,7 +54,9 @@ def inspect private def redact(v) - v.nil? || v.empty? ? '(unset)' : '[REDACTED]' + # Use .to_s.empty? so an Integer / other non-String credential (rare but + # possible via misconfig) doesn't crash with NoMethodError. + v.nil? || v.to_s.empty? ? '(unset)' : '[REDACTED]' end end end diff --git a/lib/bybit/error.rb b/lib/bybit/error.rb index b0b7c2b..285654f 100644 --- a/lib/bybit/error.rb +++ b/lib/bybit/error.rb @@ -1,8 +1,25 @@ # frozen_string_literal: true module Bybit + # Root class for every failure the SDK raises. `rescue Bybit::Error` + # catches everything — auth, rate-limit, timeout, network, parse, misconfig. class Error < StandardError; end + # Configuration mistake caught before the network call (missing api_key, + # invalid combination of options). Distinct from AuthError which is a + # server-side rejection. + class ConfigurationError < Error; end + + # Transport-level errors that don't fit Timeout / Network / a Bybit body. + # Faraday::ParsingError / ClientError / ServerError land here. + class TransportError < Error; end + + # 5xx HTTP without a decodable Bybit body — infra outage / gateway error. + class ServerError < TransportError; end + + # Non-auth 4xx HTTP without a decodable Bybit body — usually WAF / CDN. + class ClientError < TransportError; end + # Raised when the server returns HTTP 200 + retCode != 0 (the V5 norm), # or when a transport-level error is enriched with a Bybit body payload. class ApiError < Error @@ -20,10 +37,13 @@ def initialize(response, http_status: nil) class AuthError < ApiError; end class RateLimitError < ApiError; end - class TimeoutError < Error; end - class NetworkError < Error; end + class TimeoutError < TransportError; end + class NetworkError < TransportError; end - class ParseError < Error + # Body did not parse or didn't match Bybit V5 ApiResponse shape. `body` + # holds the raw payload (truncated in the message but full in the attr) + # so consumers can log CDN / maintenance-page HTML for post-mortem. + class ParseError < TransportError attr_reader :body, :http_status def initialize(message, body: nil, http_status: nil) @body = body diff --git a/lib/bybit/rest_api/crypto_loan_service.rb b/lib/bybit/rest_api/crypto_loan_service.rb index 4edd51f..ee4601e 100644 --- a/lib/bybit/rest_api/crypto_loan_service.rb +++ b/lib/bybit/rest_api/crypto_loan_service.rb @@ -251,7 +251,7 @@ def renew_fixed(loan_id:, collateral_list:, **kwargs) # @param loan_currency [String] Loan currency # @param collateral_coin [String] Collateral coin # @param amount [String] Repay amount - def post_crypto_loan_fixed_repay_collateral(loan_id:, loan_currency:, collateral_coin:, amount:, **kwargs) + def repay_fixed_with_collateral(loan_id:, loan_currency:, collateral_coin:, amount:, **kwargs) params = kwargs.merge(loan_id: loan_id, loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/repay-collateral', body: params) @@ -340,7 +340,7 @@ def repay_flexible(loan_currency:, amount:, **kwargs) # @param loan_currency [String] Loan currency # @param collateral_coin [String] Collateral coin # @param amount [String] Repayment amount - def post_crypto_loan_flexible_repay_collateral(loan_currency:, collateral_coin:, amount:, **kwargs) + def repay_flexible_with_collateral(loan_currency:, collateral_coin:, amount:, **kwargs) params = kwargs.merge(loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/crypto-loan-flexible/repay-collateral', body: params) diff --git a/lib/bybit/rest_api/earn_service.rb b/lib/bybit/rest_api/earn_service.rb index 3f6b92b..58726f7 100644 --- a/lib/bybit/rest_api/earn_service.rb +++ b/lib/bybit/rest_api/earn_service.rb @@ -173,7 +173,7 @@ def get_order_history(category:, **kwargs) # @param category [String] Product category # @option kwargs [String] :product_id Product ID # @option kwargs [String] :coin Coin - def get_position_get(category:, **kwargs) + def get_position(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/earn/position', params: params) diff --git a/lib/bybit/rest_api/p2p_service.rb b/lib/bybit/rest_api/p2p_service.rb index edef8b2..1f4e3d2 100644 --- a/lib/bybit/rest_api/p2p_service.rb +++ b/lib/bybit/rest_api/p2p_service.rb @@ -161,7 +161,7 @@ def mark_order_as_paid(order_id:, payment_type:, payment_id:, **kwargs) # @param quantity [String] Ad quantity # @param payment_period [String] Payment period in minutes # @param item_type [String] Item type - def post_ad(token_id:, currency_id:, side:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, quantity:, payment_period:, item_type:, **kwargs) + def create_ad(token_id:, currency_id:, side:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, quantity:, payment_period:, item_type:, **kwargs) params = kwargs.merge(token_id: token_id, currency_id: currency_id, side: side, price_type: price_type, premium: premium, price: price, min_amount: min_amount, max_amount: max_amount, remark: remark, trading_preference_set: trading_preference_set, payment_ids: payment_ids, quantity: quantity, payment_period: payment_period, item_type: item_type) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/p2p/item/create', body: params) diff --git a/lib/bybit/rest_api/position_service.rb b/lib/bybit/rest_api/position_service.rb index c6fee0b..068bca6 100644 --- a/lib/bybit/rest_api/position_service.rb +++ b/lib/bybit/rest_api/position_service.rb @@ -95,7 +95,7 @@ def get_move_position_history(**kwargs) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination # @see https://bybit-exchange.github.io/docs/v5/position - def get_info_get(category:, **kwargs) + def get_positions(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/position/list', params: params) diff --git a/lib/bybit/rest_api/trade_service.rb b/lib/bybit/rest_api/trade_service.rb index af85064..8ac06ca 100644 --- a/lib/bybit/rest_api/trade_service.rb +++ b/lib/bybit/rest_api/trade_service.rb @@ -19,7 +19,7 @@ class TradeService < BaseService # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination # @see https://bybit-exchange.github.io/docs/v5/order/execution - def get_history(category:, **kwargs) + def get_execution_history(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/execution/list', params: params) @@ -220,7 +220,7 @@ def get_open_orders(category:, **kwargs) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination # @see https://bybit-exchange.github.io/docs/v5/order/order-list - def get_order_history_get(category:, **kwargs) + def get_order_history(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/order/history', params: params) diff --git a/lib/bybit/rest_api/user_service.rb b/lib/bybit/rest_api/user_service.rb index 9bcc51a..cf9202e 100644 --- a/lib/bybit/rest_api/user_service.rb +++ b/lib/bybit/rest_api/user_service.rb @@ -66,7 +66,7 @@ def delete_sub_api_key(subuid:, **kwargs) # POST /v5/user/del-submember # # @param subuid [Integer] Sub UID to delete - def delete_sub_member_v5(subuid:, **kwargs) + def delete_sub_member(subuid:, **kwargs) params = kwargs.merge(subuid: subuid) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/user/del-submember', body: params) @@ -92,7 +92,7 @@ def frozen_sub_member(subuid:, frozen:, **kwargs) # @param uid [String] Affiliate user UID # @option kwargs [String] :coin Coin filter # @option kwargs [String] :business Business type filter - def get_affiliate_custom_open_info_v5(uid:, **kwargs) + def get_affiliate_custom_open_info(uid:, **kwargs) params = kwargs.merge(uid: uid) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/aff-customer-info', params: params) @@ -117,7 +117,7 @@ def get_member_account_type(**kwargs) # @option kwargs [Integer] :limit Page limit # @option kwargs [String] :cursor Pagination cursor # @see https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys - def list_sub_api_keys_v5(subuid:, **kwargs) + def list_sub_api_keys(subuid:, **kwargs) params = kwargs.merge(subuid: subuid) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/sub-apikeys', params: params) @@ -138,7 +138,7 @@ def query_api_key # # @option kwargs [Integer] :next_cursor Pagination cursor # @option kwargs [Integer] :page_size Page size - def query_escrow_sub_members_v5(**kwargs) + def query_escrow_sub_members(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/escrow_sub_members', params: params) @@ -166,13 +166,14 @@ def query_sub_members @session.sign_request(method: :get, path: '/v5/user/query-sub-members') end - # Query Sub-accounts List. + # List Sub-accounts (paginated variant — /v5/user/submembers). + # Distinct from #query_sub_members (/v5/user/query-sub-members). # # GET /v5/user/submembers # # @option kwargs [Integer] :page_size Page size # @option kwargs [Integer] :next_cursor Pagination cursor - def query_sub_members_v5(**kwargs) + def list_sub_members(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/submembers', params: params) diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index 75b709e..aa75ea3 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -8,11 +8,15 @@ module Bybit # Session owns the Faraday connection + auth-header assembly. Every service # class receives a Session instance and dispatches through public_request / # sign_request. Modeled after binance-connector-ruby's Session pattern. + # + # Signing invariant: the payload signed and the query string sent on the + # wire MUST be byte-identical. We enforce this by building ONE query_str + # (via encode_query after key-sort) and using it verbatim for both + # the signature payload AND the request URL — never letting Faraday's + # NestedParamsEncoder re-serialize the params. This avoids the classic + # array-value / nested-hash reorder bug. class Session - SENSITIVE_HEADERS = %w[ - x-bapi-api-key x-bapi-sign x-bapi-timestamp x-bapi-recv-window - x-bapi-sign-type authorization cookie set-cookie - ].freeze + PAYLOAD_QUERY_METHODS = %i[get delete].freeze def initialize(config) @config = config @@ -33,15 +37,31 @@ def sign_request(method:, path:, params: nil, body: nil) private def dispatch(method:, path:, signed:, params:, body:) + # Only GET / DELETE carry params on the query string. Silently dropping + # params on POST / PUT / PATCH used to leave codegen typos undiagnosed + # for weeks — now they raise loudly. + if params && !PAYLOAD_QUERY_METHODS.include?(method) && !body.nil? + raise Bybit::ConfigurationError, + "params: is only valid on GET/DELETE — #{method.to_s.upcase} must pass data via body:" + end clean_params = compact(params) clean_body = compact(body) - query_str = clean_params ? URI.encode_www_form(clean_params) : '' + query_str = clean_params ? encode_query(clean_params) : '' body_str = clean_body ? JSON.generate(clean_body) : '' headers = build_headers(signed: signed, method: method, query_str: query_str, body_str: body_str) + # Build URL manually with our own query_str so the wire bytes match + # what we signed. `req.params =` would re-serialize through + # Faraday::NestedParamsEncoder which key-orders and array-brackets + # differently, breaking the signature. + full_url = if !query_str.empty? && PAYLOAD_QUERY_METHODS.include?(method) + "#{path}?#{query_str}" + else + path + end + resp = @conn.send(method) do |req| - req.url path - req.params = clean_params if clean_params && [:get, :delete].include?(method) + req.url full_url req.headers.update(headers) req.body = body_str if clean_body end @@ -50,6 +70,19 @@ def dispatch(method:, path:, signed:, params:, body:) raise Bybit::TimeoutError, e.message rescue Faraday::ConnectionFailed, Faraday::SSLError => e raise Bybit::NetworkError, e.message + rescue Faraday::Error => e + # Catch-all for Faraday::ParsingError / ClientError / ServerError etc. + # that surface when the caller wires their own error-raising middleware. + raise Bybit::TransportError, e.message + end + + # Deterministic `&`-joined encoding, keys sorted, values URI-escaped. + # Arrays become repeated keys (`symbol=BTCUSDT&symbol=ETHUSDT`) — this + # matches Bybit V5's flat-list expectation. + def encode_query(params) + params.sort_by { |k, _| k.to_s }.flat_map { |k, v| + Array(v).map { |single| "#{URI.encode_www_form_component(k.to_s)}=#{URI.encode_www_form_component(single.to_s)}" } + }.join('&') end def build_headers(signed:, method:, query_str:, body_str:) @@ -57,10 +90,12 @@ def build_headers(signed:, method:, query_str:, body_str:) h['Content-Type'] = 'application/json' unless body_str.empty? return h unless signed if @config.api_key.nil? || @config.api_secret.nil? - raise ArgumentError, 'signed endpoint requires api_key + api_secret' + raise Bybit::ConfigurationError, 'signed endpoint requires api_key + api_secret' end ts = (Time.now.to_f * 1000).to_i.to_s - payload = method == :get ? query_str : body_str + # payload for GET/DELETE is query string; for POST/PUT/PATCH it's the + # JSON body. Both branches use the SAME predicate as dispatch above. + payload = PAYLOAD_QUERY_METHODS.include?(method) ? query_str : body_str h['X-BAPI-API-KEY'] = @config.api_key h['X-BAPI-TIMESTAMP'] = ts h['X-BAPI-RECV-WINDOW'] = @config.recv_window.to_s @@ -72,16 +107,28 @@ def build_headers(signed:, method:, query_str:, body_str:) end def parse_response(response) - body = response.body - body = safe_parse_json(body) if body.is_a?(String) - unless body.is_a?(Hash) && body['retCode'].is_a?(Integer) - raise Bybit::ParseError.new( - "Response is not a valid Bybit V5 ApiResponse (status=#{response.status})", - body: body, http_status: response.status - ) + status = response.status + raw = response.body + body = raw.is_a?(String) ? safe_parse_json(raw) : raw + + # HTTP status wins over retCode when the body isn't a valid ApiResponse. + # 5xx / non-auth 4xx get their own class so retries and pager logic can + # tell them apart from client / auth errors. + if !body.is_a?(Hash) || !body['retCode'].is_a?(Integer) + preview = truncate_for_error(raw) + if status >= 500 + raise Bybit::ServerError, "Bybit server error (status=#{status}): #{preview}" + elsif status >= 400 + raise Bybit::ClientError, "Bybit client error (status=#{status}): #{preview}" + else + raise Bybit::ParseError.new( + "Response is not a valid Bybit V5 ApiResponse (status=#{status}): #{preview}", + body: raw, http_status: status + ) + end end - return body if body['retCode'] == 0 - raise Bybit.api_error_from(body, http_status: response.status) + return body if body['retCode'].zero? + raise Bybit.api_error_from(body, http_status: status) end def safe_parse_json(str) @@ -90,6 +137,14 @@ def safe_parse_json(str) nil end + def truncate_for_error(raw) + return '(nil body)' if raw.nil? + s = raw.is_a?(String) ? raw : raw.inspect + s.length > 2048 ? "#{s[0, 2048]}…(truncated)" : s + end + + # Shallow compact only — inner Hash / Array-of-Hash values pass through. + # Recursion happens on the wire-key side via WireKeys.camelize. def compact(hash) return nil if hash.nil? hash.reject { |_, v| v.nil? } diff --git a/lib/bybit/utils/wire_keys.rb b/lib/bybit/utils/wire_keys.rb index 6aa4db2..8e943a7 100644 --- a/lib/bybit/utils/wire_keys.rb +++ b/lib/bybit/utils/wire_keys.rb @@ -6,23 +6,54 @@ module Utils # Bot-family endpoints keep snake_case (identity mapping) — call sites # skip WireKeys.camelize. Everyone else runs camelize on the params/body # hash right before dispatch. + # + # Recursion: batch endpoints (trade#batch_amend_orders, + # account#batch_set_collateral, etc.) carry Array or nested Hash + # payloads whose inner keys must ALSO be camelized. A non-recursive + # camelize was the PR#1 blocker (retCode 10001/10004 on every batch call). module WireKeys + # Ruby-reserved-word arg aliases: :end_ → :end on the wire. Codegen + # emits arg names with the trailing '_'; camelize normalizes them back + # to the spec name so service methods no longer need per-method + # `params[:end] = params.delete(:end_)` shims. + RESERVED_ALIASES = { + end_: :end, begin_: :begin, class_: :class, next_: :next, + return_: :return, do_: :do, if_: :if, else_: :else, + }.freeze + module_function - # Convert every key of a hash from :some_key (Symbol) or 'some_key' (String) - # to camelCase :someKey / 'someKey'. Non-string / non-symbol keys pass through. + # Convert every key of a hash from :some_key / 'some_key' to camelCase. + # Recurses into Hash and Array-of-Hash values. Non-hash/array values + # pass through unchanged. Reserved-word aliases (:end_ etc.) get + # rewritten to their bare form. def camelize(hash) return hash if hash.nil? hash.each_with_object({}) do |(k, v), out| - out[to_camel(k)] = v + out[to_camel(unalias(k))] = camelize_value(v) + end + end + + def camelize_value(v) + case v + when Hash then camelize(v) + when Array then v.map { |el| el.is_a?(Hash) ? camelize(el) : el } + else v end end + def unalias(key) + return key unless key.is_a?(Symbol) || key.is_a?(String) + aliased = RESERVED_ALIASES[key.to_s.to_sym] + return key unless aliased + key.is_a?(Symbol) ? aliased : aliased.to_s + end + def to_camel(key) return key unless key.is_a?(Symbol) || key.is_a?(String) parts = key.to_s.split('_') return key if parts.size < 2 - camel = parts[0] + parts[1..].map { |p| p.empty? ? '' : (p[0].upcase + p[1..].to_s) }.join + camel = parts[0] + parts[1..].reject(&:empty?).map(&:capitalize).join key.is_a?(Symbol) ? camel.to_sym : camel end end diff --git a/spec/authentication_spec.rb b/spec/authentication_spec.rb new file mode 100644 index 0000000..d3be8a1 --- /dev/null +++ b/spec/authentication_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Regression tests around `Bybit::Authentication.sign_v5`. The known-vector +# assertions below LOCK the exact byte concatenation order — timestamp, +# api_key, recv_window, payload — plus HMAC-SHA256 hex digest. If any of +# these ever drift, every signed call goes 401. +RSpec.describe Bybit::Authentication do + describe '.sign_v5' do + # Fixed inputs — signature is deterministic given these inputs. + let(:api_secret) { 'test-secret' } + let(:api_key) { 'test-key' } + let(:timestamp) { '1700000000000' } + let(:recv_window) { '5000' } + + it 'HMAC-SHA256 hex-digests timestamp + apiKey + recvWindow + payload for GET query strings' do + payload = 'category=spot&symbol=BTCUSDT' + expected = OpenSSL::HMAC.hexdigest( + 'SHA256', api_secret, "#{timestamp}#{api_key}#{recv_window}#{payload}" + ) + expect(described_class.sign_v5(api_secret, timestamp, api_key, recv_window, payload)).to eq(expected) + end + + it 'HMAC-SHA256 hex-digests the JSON body verbatim for POST payloads' do + payload = '{"category":"linear","symbol":"BTCUSDT"}' + expected = OpenSSL::HMAC.hexdigest( + 'SHA256', api_secret, "#{timestamp}#{api_key}#{recv_window}#{payload}" + ) + expect(described_class.sign_v5(api_secret, timestamp, api_key, recv_window, payload)).to eq(expected) + end + + it 'produces a 64-char lowercase hex string' do + sig = described_class.sign_v5(api_secret, timestamp, api_key, recv_window, '') + expect(sig).to match(/\A[0-9a-f]{64}\z/) + end + end +end diff --git a/spec/session_spec.rb b/spec/session_spec.rb new file mode 100644 index 0000000..5a9e731 --- /dev/null +++ b/spec/session_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# Session-level regressions: signature payload branch by HTTP verb, header +# presence, error-class mapping. +RSpec.describe Bybit::Session do + let(:config) do + c = Bybit::Configuration.new + c.api_key = 'test-key' + c.api_secret = 'test-secret' + c.base_url = 'https://api-testnet.bybit.com' + c.recv_window = '5000' + c + end + let(:session) { described_class.new(config) } + + describe 'signed dispatch' do + it 'signs GET query-string exactly as it appears on the wire' do + stub = stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance}).to_return( + status: 200, body: '{"retCode":0,"retMsg":"OK","result":{},"retExtInfo":{},"time":0}', + headers: { 'Content-Type' => 'application/json' }, + ) + session.sign_request(method: :get, path: '/v5/account/wallet-balance', params: { accountType: 'UNIFIED' }) + expect(stub).to have_been_requested.with { |req| + %w[X-BAPI-API-KEY X-BAPI-TIMESTAMP X-BAPI-RECV-WINDOW X-BAPI-SIGN X-BAPI-SIGN-TYPE] + .all? { |h| req.headers.key?(h) } + } + end + + it 'signs POST body-string, does NOT send params on query string' do + stub = stub_request(:post, 'https://api-testnet.bybit.com/v5/order/create').to_return( + status: 200, body: '{"retCode":0,"retMsg":"OK","result":{},"retExtInfo":{},"time":0}', + headers: { 'Content-Type' => 'application/json' }, + ) + session.sign_request(method: :post, path: '/v5/order/create', body: { category: 'linear', symbol: 'BTCUSDT' }) + expect(stub).to have_been_requested + end + + it 'signs DELETE-with-params using the query string (not empty body)' do + stub = stub_request(:delete, %r{https://api-testnet\.bybit\.com/v5/order/cancel\?}).to_return( + status: 200, body: '{"retCode":0,"retMsg":"OK","result":{},"retExtInfo":{},"time":0}', + headers: { 'Content-Type' => 'application/json' }, + ) + session.sign_request(method: :delete, path: '/v5/order/cancel', params: { orderId: 'abc123' }) + expect(stub).to have_been_requested + end + + it 'raises ConfigurationError when api_key is missing' do + config.api_key = nil + expect { session.sign_request(method: :get, path: '/v5/account/wallet-balance') } + .to raise_error(Bybit::ConfigurationError, /api_key/) + end + end + + describe 'error mapping' do + it 'raises AuthError on retCode 10004' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance}).to_return( + status: 200, body: '{"retCode":10004,"retMsg":"error sign","result":{},"retExtInfo":{},"time":0}', + headers: { 'Content-Type' => 'application/json' }, + ) + expect { session.sign_request(method: :get, path: '/v5/account/wallet-balance') } + .to raise_error(Bybit::AuthError) + end + + it 'raises RateLimitError on retCode 10006' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance}).to_return( + status: 200, body: '{"retCode":10006,"retMsg":"too many","result":{},"retExtInfo":{},"time":0}', + headers: { 'Content-Type' => 'application/json' }, + ) + expect { session.sign_request(method: :get, path: '/v5/account/wallet-balance') } + .to raise_error(Bybit::RateLimitError) + end + + it 'raises ServerError on 5xx with non-JSON body' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_return( + status: 502, body: 'Bad Gateway', + ) + expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::ServerError) + end + + it 'raises ClientError on non-auth 4xx with non-JSON body' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_return( + status: 400, body: 'bad request', + ) + expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::ClientError) + end + + it 'maps Faraday::TimeoutError to Bybit::TimeoutError' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_timeout + expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::TimeoutError) + end + end + + describe 'POST + params guard' do + it 'raises ConfigurationError when caller mixes body: + params: on POST' do + expect { + session.sign_request(method: :post, path: '/v5/order/create', params: { a: 1 }, body: { b: 2 }) + }.to raise_error(Bybit::ConfigurationError, /params:/) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 459a5e4..412fab6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,17 @@ # frozen_string_literal: true +require 'simplecov' +SimpleCov.start do + add_filter '/spec/' + add_filter '/examples/' + # Signing + transport layer must stay tight — fail CI if either drops. + minimum_coverage_by_file 'lib/bybit/authentication.rb' => 100 + minimum_coverage_by_file 'lib/bybit/session.rb' => 90 +end + require 'bybit' +require 'webmock/rspec' +WebMock.disable_net_connect!(allow_localhost: true) RSpec.configure do |config| config.expect_with :rspec do |c| diff --git a/spec/wire_keys_spec.rb b/spec/wire_keys_spec.rb new file mode 100644 index 0000000..0250458 --- /dev/null +++ b/spec/wire_keys_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.describe Bybit::Utils::WireKeys do + describe '.camelize' do + it 'converts top-level snake_case keys to camelCase (symbols)' do + expect(described_class.camelize(order_type: 'Limit', symbol: 'BTCUSDT')) + .to eq(orderType: 'Limit', symbol: 'BTCUSDT') + end + + it 'recurses into nested Hash values' do + input = { extra: { stake_amount: '1', account_type: 'UNIFIED' } } + output = { extra: { stakeAmount: '1', accountType: 'UNIFIED' } } + expect(described_class.camelize(input)).to eq(output) + end + + it 'recurses into Array-of-Hash values (batch endpoints)' do + input = { request: [{ order_type: 'Limit', symbol: 'BTCUSDT' }, { order_type: 'Market', symbol: 'ETHUSDT' }] } + output = { request: [{ orderType: 'Limit', symbol: 'BTCUSDT' }, { orderType: 'Market', symbol: 'ETHUSDT' }] } + expect(described_class.camelize(input)).to eq(output) + end + + it 'passes non-Hash / non-Array-of-Hash values through unchanged' do + expect(described_class.camelize(symbols: %w[BTCUSDT ETHUSDT], count: 5)) + .to eq(symbols: %w[BTCUSDT ETHUSDT], count: 5) + end + + it 'rewrites reserved-word aliases (end_ → end)' do + expect(described_class.camelize(end_: 123)).to eq(end: 123) + expect(described_class.camelize(begin_: 1, end_: 2)).to eq(begin: 1, end: 2) + end + end +end From 0f89b6b03922a800f5ec93094329a4a66f74ca56 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 15:13:31 +0800 Subject: [PATCH 03/18] add ci --- .github/workflows/ci.yml | 153 ++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 77 +++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..314ed23 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Ruby ${{ matrix.ruby }} — install + rubocop + rspec + smoke + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Matches gemspec `required_ruby_version >= 3.0`. 3.0 (EOL 2024-03-31) + # is kept here so we catch regressions for consumers still on it; + # 3.2 is the current LTS-ish; 3.4 is latest stable. + ruby: ['3.0', '3.2', '3.4'] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby ${{ matrix.ruby }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs `bundle install` and caches gems + + - name: Gemfile.lock resolves rubygems.org only (block private-registry drift) + run: | + if [ -f Gemfile.lock ]; then + bad=$(grep -E 'remote:' Gemfile.lock | grep -vE 'https?://rubygems\.org/?' || true) + if [ -n "$bad" ]; then + echo "::error::Gemfile.lock references a non-rubygems.org source. Re-generate with Gemfile pointing at 'https://rubygems.org' only." + echo "$bad" | head -5 + exit 1 + fi + fi + + - name: RuboCop + run: bundle exec rubocop --parallel + + - name: RSpec (with SimpleCov gates) + run: bundle exec rspec + + # Smoke: require the top-level module and construct a Client. Catches + # broken require chain / missing constant / class-name typo BEFORE any + # consumer sees them. Analogous to JS `dist smoke` step. + - name: Load smoke (require + Client.new) + run: | + bundle exec ruby -Ilib -rbybit -e ' + raise "Bybit::VERSION missing" unless defined?(Bybit::VERSION) + client = Bybit::Client.new(testnet: true) + raise "Client init failed" unless client.is_a?(Bybit::Client) + puts "smoke OK: bybit-connector-ruby #{Bybit::VERSION} on Ruby #{RUBY_VERSION}" + ' + + audit: + name: bundler-audit (CVE scan on runtime deps) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Install bundler-audit + run: gem install bundler-audit --no-document + + # `--update` refreshes the ruby-advisory-db before scanning. Fails on + # any advisory with severity >= high (ignore lower-severity noise with + # --ignore-severity-below high once bundler-audit ships that flag; for + # now we take the default 'fail on any'). + - name: Audit Gemfile.lock + run: bundle audit check --update + + secret-scan: + name: gitleaks (CLI) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Run the gitleaks binary directly. The `gitleaks/gitleaks-action@v2` + # wrapper requires a paid license for organizations and pins a deprecated + # node20 runtime — the underlying MIT-licensed CLI has neither restriction. + - name: Install gitleaks + env: + GITLEAKS_VERSION: '8.21.2' + run: | + set -eu + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + run: gitleaks detect --source . --redact --verbose --exit-code 1 --no-banner + + pack-check: + name: gem build --dry-run (LICENSE / README / lib present, no spec leak) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Build .gem + run: gem build bybit-connector-ruby.gemspec + + - name: Verify tarball contents + run: | + set -eu + GEM=$(ls bybit-connector-ruby-*.gem | head -1) + echo "Inspecting $GEM" + # `gem contents` after install lists files, but we haven't installed + # yet; extract the data tarball inside the .gem and list files. + mkdir -p /tmp/gem-extract && cd /tmp/gem-extract + tar -xf "$GITHUB_WORKSPACE/$GEM" + # data.tar.gz holds the packed files, metadata.gz holds the spec. + tar -tzf data.tar.gz | sort > /tmp/gem-files.txt + echo "--- packed files ---" + cat /tmp/gem-files.txt + + # Required entries + for need in LICENSE README.md CHANGELOG.md lib/bybit.rb lib/bybit/version.rb lib/bybit/session.rb lib/bybit/rest_api/base_service.rb; do + if ! grep -qx "$need" /tmp/gem-files.txt; then + echo "::error::Missing from gem tarball: $need" + exit 1 + fi + done + + # Files that must NEVER ship in the gem + for bad in spec Gemfile Gemfile.lock Rakefile .rubocop.yml .rspec .yardopts; do + if grep -q "^${bad}\(/\|$\)" /tmp/gem-files.txt; then + echo "::error::Gem tarball contains unexpected entry: $bad" + exit 1 + fi + done + echo "pack check OK" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6dee3f1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,77 @@ +name: Publish to rubygems + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + publish: + runs-on: ubuntu-latest + # id-token: write enables rubygems.org OIDC trusted publishing if the gem + # is configured for it (Aug-2024 feature). Falls back to RUBYGEMS_API_KEY + # when a trusted-publisher config isn't set up — see the "Publish" step. + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Validate tag is on main branch + run: | + git fetch origin main --depth=50 + if ! git branch -r --contains "$GITHUB_SHA" | grep -q 'origin/main'; then + echo "Error: tag '$GITHUB_REF_NAME' is not reachable from origin/main" + exit 1 + fi + echo "Branch check passed: tag is on main" + + - name: Validate tag matches lib/bybit/version.rb VERSION + run: | + set -eu + GEM_VERSION=$(bundle exec ruby -Ilib -rbybit/version -e 'print Bybit::VERSION') + TAG_VERSION="${GITHUB_REF_NAME}" + if [ "$GEM_VERSION" != "$TAG_VERSION" ]; then + echo "Error: tag '$TAG_VERSION' does not match Bybit::VERSION '$GEM_VERSION'" + exit 1 + fi + echo "Version check passed: $TAG_VERSION" + + - name: RuboCop + RSpec must pass before publish + run: | + bundle exec rubocop --parallel + bundle exec rspec + + - name: Build .gem + run: gem build bybit-connector-ruby.gemspec + + # Trusted publishing (OIDC) — no long-lived token required. If the gem + # is NOT yet configured for trusted publishing on rubygems.org, set + # RUBYGEMS_API_KEY as a repo secret and this step will fall back to it. + - name: Publish to rubygems + env: + RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} + run: | + set -eu + GEM=$(ls bybit-connector-ruby-*.gem | head -1) + if command -v rubygems-await >/dev/null 2>&1; then :; fi + if [ -n "${RUBYGEMS_API_KEY:-}" ]; then + echo "Using RUBYGEMS_API_KEY auth" + mkdir -p ~/.gem + echo -e ":rubygems_api_key: ${RUBYGEMS_API_KEY}" > ~/.gem/credentials + chmod 600 ~/.gem/credentials + gem push "$GEM" + else + echo "No RUBYGEMS_API_KEY; assuming OIDC trusted publisher is configured on rubygems.org" + gem exec rubygems-trusted-publishing --push "$GEM" || { + echo "::error::Neither RUBYGEMS_API_KEY nor a trusted-publisher config found. Set one of them." + exit 1 + } + fi From dbb2e85d2bc78eba9b0b17f48c6c55fa1560d1e9 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 16:16:21 +0800 Subject: [PATCH 04/18] optimization --- lib/bybit/rest_api/account_service.rb | 23 + lib/bybit/rest_api/affiliate_service.rb | 2 + lib/bybit/rest_api/asset_service.rb | 594 +++++++++++++++++++++- lib/bybit/rest_api/bot_service.rb | 20 +- lib/bybit/rest_api/broker_service.rb | 14 +- lib/bybit/rest_api/crypto_loan_service.rb | 25 + lib/bybit/rest_api/earn_service.rb | 66 +++ lib/bybit/rest_api/market_service.rb | 21 + lib/bybit/rest_api/p2p_service.rb | 15 + lib/bybit/rest_api/position_service.rb | 13 +- lib/bybit/rest_api/rfq_service.rb | 15 + lib/bybit/rest_api/spot_margin_service.rb | 4 + lib/bybit/rest_api/trade_service.rb | 15 +- lib/bybit/rest_api/user_service.rb | 44 +- spec/services/account_service_spec.rb | 58 +++ spec/services/affiliate_service_spec.rb | 37 ++ spec/services/asset_service_spec.rb | 77 +++ spec/services/bot_service_spec.rb | 53 ++ spec/services/broker_service_spec.rb | 43 ++ spec/services/crypto_loan_service_spec.rb | 61 +++ spec/services/earn_service_spec.rb | 104 ++++ spec/services/market_service_spec.rb | 57 +++ spec/services/p2p_service_spec.rb | 52 ++ spec/services/position_service_spec.rb | 46 ++ spec/services/rfq_service_spec.rb | 50 ++ spec/services/spot_margin_service_spec.rb | 39 ++ spec/services/trade_service_spec.rb | 48 ++ spec/services/user_service_spec.rb | 52 ++ 28 files changed, 1632 insertions(+), 16 deletions(-) create mode 100644 spec/services/account_service_spec.rb create mode 100644 spec/services/affiliate_service_spec.rb create mode 100644 spec/services/asset_service_spec.rb create mode 100644 spec/services/bot_service_spec.rb create mode 100644 spec/services/broker_service_spec.rb create mode 100644 spec/services/crypto_loan_service_spec.rb create mode 100644 spec/services/earn_service_spec.rb create mode 100644 spec/services/market_service_spec.rb create mode 100644 spec/services/p2p_service_spec.rb create mode 100644 spec/services/position_service_spec.rb create mode 100644 spec/services/rfq_service_spec.rb create mode 100644 spec/services/spot_margin_service_spec.rb create mode 100644 spec/services/trade_service_spec.rb create mode 100644 spec/services/user_service_spec.rb diff --git a/lib/bybit/rest_api/account_service.rb b/lib/bybit/rest_api/account_service.rb index 7aeac61..c96452f 100644 --- a/lib/bybit/rest_api/account_service.rb +++ b/lib/bybit/rest_api/account_service.rb @@ -8,6 +8,7 @@ class AccountService < BaseService # POST /v5/account/set-collateral-switch-batch # # @param request [Array] Array of collateral switch objects + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/batch-set-collateral def batch_set_collateral(request:, **kwargs) params = kwargs.merge(request: request) @@ -19,6 +20,7 @@ def batch_set_collateral(request:, **kwargs) # # GET /v5/account/info # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/account-info def get_info(**kwargs) params = kwargs.dup @@ -34,6 +36,7 @@ def get_info(**kwargs) # @option kwargs [String] :symbol Symbol name # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_instruments(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -49,6 +52,7 @@ def get_instruments(category:, **kwargs) # @option kwargs [Integer] :end_time The end timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/borrow-history def get_borrow_history(**kwargs) params = kwargs.dup @@ -61,6 +65,7 @@ def get_borrow_history(**kwargs) # GET /v5/account/collateral-info # # @option kwargs [String] :currency Asset currency of all current collateral + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/collateral-info def get_collateral_info(**kwargs) params = kwargs.dup @@ -72,6 +77,7 @@ def get_collateral_info(**kwargs) # # GET /v5/account/query-dcp-info # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/dcp-info def get_dcp_info(**kwargs) params = kwargs.dup @@ -86,6 +92,7 @@ def get_dcp_info(**kwargs) # @param category [String] Product type # @option kwargs [String] :symbol Symbol name # @option kwargs [String] :base_coin Base coin. SOL, BTC, ETH. Apply to option only + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/fee-rate def get_fee_rate(category:, **kwargs) params = kwargs.merge(category: category) @@ -98,6 +105,7 @@ def get_fee_rate(category:, **kwargs) # GET /v5/account/mmp-state # # @param base_coin [String] Base coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/get-mmp-state def get_mmp_state(base_coin:, **kwargs) params = kwargs.merge(base_coin: base_coin) @@ -109,6 +117,7 @@ def get_mmp_state(base_coin:, **kwargs) # # GET /v5/account/smp-group # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/smp-group def get_smp_group(**kwargs) params = kwargs.dup @@ -130,6 +139,7 @@ def get_smp_group(**kwargs) # @option kwargs [Integer] :end_time The end timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor. Use the nextPageCursor token from the response to retrieve the next page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/transaction-log def get_transaction_log(**kwargs) params = kwargs.dup @@ -142,6 +152,7 @@ def get_transaction_log(**kwargs) # GET /v5/account/withdrawal # # @param coin_name [String] Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_transferable_amount(coin_name:, **kwargs) params = kwargs.merge(coin_name: coin_name) params = Bybit::Utils::WireKeys.camelize(params) @@ -151,6 +162,7 @@ def get_transferable_amount(coin_name:, **kwargs) # Get User Settings # # GET /v5/account/user-setting-config + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_user_settings(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -163,6 +175,7 @@ def get_user_settings(**kwargs) # # @param coin [String] Coin name # @param amount [String] The amount to borrow + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def manual_borrow(coin:, amount:, **kwargs) params = kwargs.merge(coin: coin, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) @@ -175,6 +188,7 @@ def manual_borrow(coin:, amount:, **kwargs) # # @option kwargs [String] :coin Coin name. If not passed, repay all liabilities # @option kwargs [String] :amount The amount to repay + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def manual_repay(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -187,6 +201,7 @@ def manual_repay(**kwargs) # # @param coin [String] Coin name # @option kwargs [String] :amount The amount to repay + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/no-convert-repay def no_convert_repay(coin:, **kwargs) params = kwargs.merge(coin: coin) @@ -199,6 +214,7 @@ def no_convert_repay(coin:, **kwargs) # POST /v5/account/quick-repayment # # @option kwargs [String] :coin Coin to repay debt with + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def one_click_repay(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -210,6 +226,7 @@ def one_click_repay(**kwargs) # POST /v5/account/mmp-reset # # @param base_coin [String] Base coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def reset_mmp(base_coin:, **kwargs) params = kwargs.merge(base_coin: base_coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -222,6 +239,7 @@ def reset_mmp(base_coin:, **kwargs) # # @param coin [String] Coin symbol # @param collateral_switch [String] ON or OFF + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def set_collateral_coin(coin:, collateral_switch:, **kwargs) params = kwargs.merge(coin: coin, collateral_switch: collateral_switch) params = Bybit::Utils::WireKeys.camelize(params) @@ -233,6 +251,7 @@ def set_collateral_coin(coin:, collateral_switch:, **kwargs) # POST /v5/account/set-margin-mode # # @param set_margin_mode [String] Margin mode to set + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/set-margin-mode def set_margin_mode(set_margin_mode:, **kwargs) params = kwargs.merge(set_margin_mode: set_margin_mode) @@ -249,6 +268,7 @@ def set_margin_mode(set_margin_mode:, **kwargs) # @param frozen_period [String] Frozen period (ms) # @param qty_limit [String] Quantity limit # @param delta_limit [String] Delta limit + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/set-mmp def set_mmp(base_coin:, window:, frozen_period:, qty_limit:, delta_limit:, **kwargs) params = kwargs.merge(base_coin: base_coin, window: window, frozen_period: frozen_period, qty_limit: qty_limit, delta_limit: delta_limit) @@ -262,6 +282,7 @@ def set_mmp(base_coin:, window:, frozen_period:, qty_limit:, delta_limit:, **kwa # # @param category [String] Product type # @param modify_enable [Boolean] Enable modify price limit check + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def set_price_limit(category:, modify_enable:, **kwargs) params = kwargs.merge(category: category, modify_enable: modify_enable) params = Bybit::Utils::WireKeys.camelize(params) @@ -273,6 +294,7 @@ def set_price_limit(category:, modify_enable:, **kwargs) # POST /v5/account/set-hedging-mode # # @param set_hedging_mode [String] ON or OFF + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/set-spot-hedge def set_spot_hedging(set_hedging_mode:, **kwargs) params = kwargs.merge(set_hedging_mode: set_hedging_mode) @@ -284,6 +306,7 @@ def set_spot_hedging(set_hedging_mode:, **kwargs) # # POST /v5/account/upgrade-to-uta # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/account/upgrade-unified-account def upgrade_to_uta_pro(**kwargs) params = kwargs.dup diff --git a/lib/bybit/rest_api/affiliate_service.rb b/lib/bybit/rest_api/affiliate_service.rb index 0994a75..d132417 100644 --- a/lib/bybit/rest_api/affiliate_service.rb +++ b/lib/bybit/rest_api/affiliate_service.rb @@ -12,6 +12,7 @@ class AffiliateService < BaseService # @option kwargs [String] :start_date Start date filter # @option kwargs [String] :end_date End date filter # @option kwargs [Integer] :sub_aff_id Sub-affiliate ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/affiliate/affiliate-sub-list def get_sub_list(**kwargs) params = kwargs.dup @@ -30,6 +31,7 @@ def get_sub_list(**kwargs) # @option kwargs [Boolean] :need365 Whether to include 365-day statistics # @option kwargs [String] :start_date Start date filter # @option kwargs [String] :end_date End date filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_user_list(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/asset_service.rb b/lib/bybit/rest_api/asset_service.rb index 2b179b7..795a887 100644 --- a/lib/bybit/rest_api/asset_service.rb +++ b/lib/bybit/rest_api/asset_service.rb @@ -11,6 +11,7 @@ class AssetService < BaseService # @option kwargs [String] :member_id User ID. Required when querying sub UID balance with master API key # @option kwargs [String] :coin Coin name(s). Multiple coins separated by comma. If not passed, returns all coins # @option kwargs [Integer] :with_bonus 0(default): not include bonus; 1: include bonus + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_coin_balance(account_type:, **kwargs) params = kwargs.merge(account_type: account_type) params = Bybit::Utils::WireKeys.camelize(params) @@ -22,6 +23,7 @@ def get_coin_balance(account_type:, **kwargs) # GET /v5/asset/coin-greeks # # @option kwargs [String] :base_coin Base coin. Default: return all your coins greek data + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_coin_greeks(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -36,11 +38,601 @@ def get_coin_greeks(**kwargs) # @option kwargs [String] :create_time_to End timestamp (ms) # @option kwargs [String] :limit Limit for data size per page. [1, 50]. Default: 20 # @option kwargs [String] :cursor Cursor. Used for pagination - def query_funding_detail(**kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def list_funding_history(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/asset/fundinghistory', params: params) end + + # Get Coin Info + # + # GET /v5/asset/coin/query-info + # + # @option kwargs [String] :coin Coin name, uppercase only. e.g. BTC, ETH. If not passed, return all coin info. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/coin/query-info + def get_coin_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/coin/query-info', params: params) + end + + # Get Deposit Records (on-chain) + # + # GET /v5/asset/deposit/query-record + # + # @option kwargs [String] :id Internal ID. Takes highest priority when combined with other params. + # @option kwargs [String] :tx_id Transaction ID. Only works for data from Jan 1, 2024 onward. + # @option kwargs [String] :coin Coin symbol (uppercase only), e.g. `BTC`, `USDT`. Empty means query all coins. + # @option kwargs [Integer] :start_time Start timestamp in milliseconds. Defaults to 30 days ago if not provided. + # @option kwargs [Integer] :end_time End timestamp in milliseconds. Defaults to current time if not provided. + # @option kwargs [Integer] :limit Records per page. Range `[1, 50]`, default `50`. + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor` in prior response. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/query-record + def list_deposit_records(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/deposit/query-record', params: params) + end + + # Get Master Deposit Address + # + # GET /v5/asset/deposit/query-address + # + # @param coin [String] Coin symbol (uppercase only), e.g. `USDT`. + # @option kwargs [String] :chain_type Chain type. Use `chain` value from the coin-info endpoint. If not provided, returns all chains. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/query-address + def get_deposit_address(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/deposit/query-address', params: params) + end + + # Get Sub Deposit Address + # + # GET /v5/asset/deposit/query-sub-member-address + # + # @param coin [String] Coin symbol (uppercase only). + # @param chain_type [String] Chain type. Use `chain` value from the coin-info endpoint. + # @param sub_member_id [String] Sub-account user ID. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/query-sub-member-address + def get_sub_member_deposit_address(coin:, chain_type:, sub_member_id:, **kwargs) + params = kwargs.merge(coin: coin, chain_type: chain_type, sub_member_id: sub_member_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/deposit/query-sub-member-address', params: params) + end + + # Get Sub Deposit Records (on-chain) + # + # GET /v5/asset/deposit/query-sub-member-record + # + # @param sub_member_id [String] Sub-account UID. + # @option kwargs [String] :id Internal ID. Takes highest priority when combined with other params. + # @option kwargs [String] :tx_id Transaction ID (data before Jan 1, 2024 not queryable via txID). + # @option kwargs [String] :coin Coin symbol (uppercase only). Empty means query all. + # @option kwargs [Integer] :start_time Start timestamp in milliseconds. Defaults to 30 days ago. + # @option kwargs [Integer] :end_time End timestamp in milliseconds. Defaults to current time. + # @option kwargs [Integer] :limit Records per page. Range `[1, 50]`, default `50`. + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor`. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/query-sub-member-record + def list_sub_member_deposit_records(sub_member_id:, **kwargs) + params = kwargs.merge(sub_member_id: sub_member_id) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/deposit/query-sub-member-record', params: params) + end + + # Get Internal Deposit Records (off-chain) + # + # GET /v5/asset/deposit/query-internal-record + # + # @option kwargs [String] :tx_id Internal transfer transaction ID. + # @option kwargs [Integer] :start_time Start timestamp in milliseconds. Defaults to 30 days ago. + # @option kwargs [Integer] :end_time End timestamp in milliseconds. Defaults to current time. + # @option kwargs [String] :coin Coin symbol (uppercase only). Empty means query all. + # @option kwargs [String] :cursor Pagination cursor. + # @option kwargs [Integer] :limit Records per page. Range `[1, 50]`, default `50`. + # @option kwargs [String] :status Filter by status. `0` = all (default). + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/query-internal-record + def list_internal_deposit_records(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/deposit/query-internal-record', params: params) + end + + # Set Deposit Account + # + # POST /v5/asset/deposit/deposit-to-account + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/deposit/deposit-to-account + def set_default_deposit_to_account(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/deposit/deposit-to-account', body: params) + end + + # Create Internal Transfer + # + # POST /v5/asset/transfer/inter-transfer + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/inter-transfer + def inter_transfer(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/transfer/inter-transfer', body: params) + end + + # Create Universal Transfer + # + # POST /v5/asset/transfer/universal-transfer + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/universal-transfer + def universal_transfer(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/transfer/universal-transfer', body: params) + end + + # Save Transferable Sub Member List + # + # POST /v5/asset/transfer/save-transfer-sub-member + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/save-transfer-sub-member + def transfer_sub_member_save(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/transfer/save-transfer-sub-member', body: params) + end + + # Get Internal Transfer Records + # + # GET /v5/asset/transfer/query-inter-transfer-list + # + # @option kwargs [String] :transfer_id UUID of the transfer. If provided, queries a single record by transferId. + # @option kwargs [String] :coin Coin name, uppercase (e.g. BTC, USDT) + # @option kwargs [String] :status Filter by transfer status + # @option kwargs [Integer] :start_time Start timestamp in milliseconds (effective at second level) + # @option kwargs [Integer] :end_time End timestamp in milliseconds (effective at second level) + # @option kwargs [Integer] :limit Number of records per page. Default: 20, Range: [1, 50] + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor` of previous response + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-inter-transfer-list + def inter_transfer_list_query(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-inter-transfer-list', params: params) + end + + # Get Universal Transfer Records + # + # GET /v5/asset/transfer/query-universal-transfer-list + # + # @option kwargs [String] :transfer_id UUID of the transfer + # @option kwargs [String] :coin Coin name, uppercase + # @option kwargs [String] :status Filter by transfer status: SUCCESS, FAILED, PENDING + # @option kwargs [Integer] :start_time Start timestamp in milliseconds (effective at second level) + # @option kwargs [Integer] :end_time End timestamp in milliseconds (effective at second level) + # @option kwargs [Integer] :limit Number of records per page. Default: 20, Range: [1, 50] + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor` + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-universal-transfer-list + def universal_transfer_list_query(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-universal-transfer-list', params: params) + end + + # Get Transferable Coin List + # + # GET /v5/asset/transfer/query-transfer-coin-list + # + # @param from_account_type [String] Source account type + # @param to_account_type [String] Destination account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-transfer-coin-list + def transfer_coin_list_query(from_account_type:, to_account_type:, **kwargs) + params = kwargs.merge(from_account_type: from_account_type, to_account_type: to_account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-transfer-coin-list', params: params) + end + + # Get Sub UID List + # + # GET /v5/asset/transfer/query-sub-member-list + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-sub-member-list + def sub_member_list_query(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-sub-member-list', params: params) + end + + # Get Single Coin Balance + # + # GET /v5/asset/transfer/query-account-coin-balance + # + # @param account_type [String] Account type + # @param coin [String] Coin name, uppercase (e.g. USDT, BTC). Required. + # @option kwargs [Integer] :member_id UID. Required when querying sub UID balance with master API key + # @option kwargs [Integer] :to_member_id Target UID. Required for cross-UID transferable balance query + # @option kwargs [String] :to_account_type Destination account type. Required when `withLtvTransferSafeAmount=1` + # @option kwargs [Integer] :with_bonus `0` (default): exclude bonus; `1`: include bonus + # @option kwargs [Integer] :with_transfer_safe_amount `0` (default): not query; `1`: query delay-withdraw safe amount + # @option kwargs [Integer] :with_ltv_transfer_safe_amount `0` (default): not query; `1`: query OTC loan transferable amount. Requires `toAccountType` + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-account-coin-balance + def account_coin_balance_query(account_type:, coin:, **kwargs) + params = kwargs.merge(account_type: account_type, coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-account-coin-balance', params: params) + end + + # Get Spot Asset Info + # + # GET /v5/asset/transfer/query-asset-info + # + # @option kwargs [String] :account_type Account type. Currently only SPOT. Defaults to SPOT if empty. + # @option kwargs [String] :coin Coin name, uppercase. Optional; returns all coins if empty. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/transfer/query-asset-info + def asset_info_query(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/transfer/query-asset-info', params: params) + end + + # Withdraw + # + # POST /v5/asset/withdraw/create + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/create + def send_withdraw(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/withdraw/create', body: params) + end + + # Get Withdrawal Records + # + # GET /v5/asset/withdraw/query-record + # + # @option kwargs [String] :withdraw_id Withdrawal ID + # @option kwargs [String] :tx_id Transaction hash ID + # @option kwargs [String] :coin Coin name, uppercase, e.g. USDT + # @option kwargs [Integer] :withdraw_type Withdrawal type: - `0`: On-chain withdrawal (default) - `1`: Off-chain (internal transfer) - `2`: Al + # @option kwargs [Integer] :start_time Start timestamp in milliseconds. Default: 30 days before current time + # @option kwargs [Integer] :end_time End timestamp in milliseconds. Default: current time + # @option kwargs [Integer] :limit Results per page, range [1, 50], default 50 + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor` in prior response + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/query-record + def list_withdraw_records(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/withdraw/query-record', params: params) + end + + # Cancel Withdrawal + # + # POST /v5/asset/withdraw/cancel + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/cancel + def cancel_withdraw(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/withdraw/cancel', body: params) + end + + # Get Withdrawable Amount + # + # GET /v5/asset/withdraw/withdrawable-amount + # + # @param coin [String] Coin name, uppercase, e.g. USDT + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/withdrawable-amount + def get_withdrawable_amount_by_coin(coin:, **kwargs) + params = kwargs.merge(coin: coin) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/withdraw/withdrawable-amount', params: params) + end + + # Get Available VASPs + # + # GET /v5/asset/withdraw/vasp/list + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/vasp/list + def get_vasp_list(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/withdraw/vasp/list', params: params) + end + + # Get Withdrawal Address List + # + # GET /v5/asset/withdraw/query-address + # + # @option kwargs [String] :coin Coin name; use `baseCoin` for universal addresses + # @option kwargs [String] :chain Chain name + # @option kwargs [Integer] :address_type Address type: - `0`: On-chain address (default) - `1`: Internal transfer address (coin/chain ignored + # @option kwargs [Integer] :limit Records per page, range [1, 50], default 50 + # @option kwargs [String] :cursor Pagination cursor from `nextPageCursor` in prior response + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/withdraw/query-address + def list_withdraw_addresses(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/withdraw/query-address', params: params) + end + + # Get Asset Overview + # + # GET /v5/asset/asset-overview + # + # @option kwargs [String] :account_type Account type filter. Multiple values separated by commas. If not passed, returns all account types. + # @option kwargs [String] :member_id Sub-account member ID. Used to query a specific sub-account's assets. - If API key belongs to a sub- + # @option kwargs [String] :valuation_currency Valuation currency. Defaults to `USD` if not provided. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/asset-overview + def get_asset_overview(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/asset-overview', params: params) + end + + # Get Delivery Record + # + # GET /v5/asset/delivery-record + # + # @param category [String] Product type: - `linear`: USDT / USDC futures - `inverse`: Inverse futures - `option`: Options + # @option kwargs [String] :symbol Symbol name, e.g. `BTCUSDT`, `BTC-29DEC22-16000-P` + # @option kwargs [Integer] :start_time Start timestamp in **milliseconds**. Default: 30 days before current time + # @option kwargs [Integer] :end_time End timestamp in **milliseconds**. Default: current time + # @option kwargs [String] :exp_date Expiry date. Format: `25DEC22`. Default: returns all expiry dates + # @option kwargs [Integer] :limit Number of items per page. Default: `20`, Range: [`1`, `50`] + # @option kwargs [String] :cursor Pagination cursor. Use `nextPageCursor` from the response to retrieve the next page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/delivery-record + def get_delivery_record(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/delivery-record', params: params) + end + + # Get Portfolio Margin Info + # + # GET /v5/asset/portfolio-margin + # + # @option kwargs [String] :base_coin Base coin, e.g. `BTC`, `ETH`. If not passed, returns all. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/portfolio-margin + def get_portfolio_margin(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/portfolio-margin', params: params) + end + + # Get USDC Session Settlement + # + # GET /v5/asset/settlement-record + # + # @param category [String] Product type: - `linear`: USDC contract + # @option kwargs [String] :symbol Symbol name, e.g. `BTCPERP` + # @option kwargs [Integer] :start_time Start timestamp in **milliseconds**. Default: 30 days before current time + # @option kwargs [Integer] :end_time End timestamp in **milliseconds**. Default: current time + # @option kwargs [Integer] :limit Number of items per page. Default: `20`, Range: [`1`, `50`] + # @option kwargs [String] :cursor Pagination cursor. Use `nextPageCursor` from the response to retrieve the next page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/settlement-record + def get_settlement_record(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/settlement-record', params: params) + end + + # Get Total Members Assets + # + # GET /v5/asset/total-members-assets + # + # @option kwargs [String] :coin Coin name, e.g. `BTC`, `USDT`. If specified, total assets are denominated in this coin. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/total-members-assets + def get_total_members_assets(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/total-members-assets', params: params) + end + + # Coin list query + # + # GET /v5/asset/exchange/query-coin-list + # + # @param account_type [String] Wallet type. Supported values: eb_convert_funding, eb_convert_uta, eb_convert_spot, eb_convert_contr + # @option kwargs [Integer] :side 0: fromCoin list (coins to sell); 1: toCoin list (coins to buy) + # @option kwargs [String] :coin Coin name, uppercase only. Used as fromCoin filter when side=0 + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/query-coin-list + def coin_list_query(account_type:, **kwargs) + params = kwargs.merge(account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/query-coin-list', params: params) + end + + # Execute conversion + # + # POST /v5/asset/exchange/convert-execute + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/convert-execute + def convert_execute(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/exchange/convert-execute', body: params) + end + + # Conversion history query + # + # GET /v5/asset/exchange/query-convert-history + # + # @option kwargs [String] :account_type Wallet type filter. Supported: eb_convert_funding, eb_convert_uta, funding, funding_fiat, funding_fb + # @option kwargs [Integer] :index Page number, starts at 1, defaults to 1 + # @option kwargs [Integer] :limit Page size, default 20, max 100 + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/query-convert-history + def convert_history_query(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/query-convert-history', params: params) + end + + # Query coin conversion limit + # + # GET /v5/asset/exchange/query-convert-limit + # + # @param from_coin [String] From coin + # @param to_coin [String] To coin + # @param account_type [String] Account type (scene code), pass "funding" for funding account flash conversion + # @option kwargs [Integer] :from_coin_type From coin type: 0=crypto, 1=fiat + # @option kwargs [Integer] :to_coin_type To coin type: 0=crypto, 1=fiat + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/query-convert-limit + def coin_convert_limit_query(from_coin:, to_coin:, account_type:, **kwargs) + params = kwargs.merge(from_coin: from_coin, to_coin: to_coin, account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/query-convert-limit', params: params) + end + + # Query conversion result + # + # GET /v5/asset/exchange/convert-result-query + # + # @param quote_tx_id [String] Quote transaction ID + # @param account_type [String] Wallet type (scene code) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/convert-result-query + def get_convert_result(quote_tx_id:, account_type:, **kwargs) + params = kwargs.merge(quote_tx_id: quote_tx_id, account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/convert-result-query', params: params) + end + + # Paginated conversion order query + # + # GET /v5/asset/exchange/order-record + # + # @option kwargs [String] :cursor Pagination cursor (omit on first request) + # @option kwargs [Integer] :limit Items per page + # @option kwargs [String] :to_coin To coin filter + # @option kwargs [String] :from_coin From coin filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/order-record + def list_convert_orders_by_page(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/order-record', params: params) + end + + # Query conversion order list + # + # GET /v5/asset/exchange/query-order-list + # + # @option kwargs [Integer] :account_type Account type: 0=ASSET, 1=OBU + # @option kwargs [String] :cursor Pagination cursor + # @option kwargs [Integer] :limit Items per page + # @option kwargs [String] :to_coin To coin filter + # @option kwargs [String] :from_coin From coin filter + # @option kwargs [Integer] :start_time Start timestamp (seconds) + # @option kwargs [Integer] :end_time End timestamp (seconds) + # @option kwargs [Integer] :type Conversion type: 0=all, 1=auto conversion, 2=active conversion + # @option kwargs [Integer] :exchange_status Order status: 0=all, 1=init, 2=pending, 3=success, 4=failure + # @option kwargs [String] :direction Pagination direction + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/query-order-list + def list_convert_orders(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/exchange/query-order-list', params: params) + end + + # Apply quote + # + # POST /v5/asset/exchange/quote-apply + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/exchange/quote-apply + def quote_apply(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/exchange/quote-apply', body: params) + end + + # Small asset confirm conversion + # + # POST /v5/asset/covert/small-balance-execute + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/covert/small-balance-execute + def small_asset_convert(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/covert/small-balance-execute', body: params) + end + + # Small asset conversion history query + # + # GET /v5/asset/covert/small-balance-history + # + # @option kwargs [String] :account_type Wallet type: eb_convert_uta or eb_convert_funding + # @option kwargs [String] :quote_id Quote ID. Highest priority filter when provided + # @option kwargs [String] :cursor Page number for pagination + # @option kwargs [String] :size Page size, default 50, max 100 + # @option kwargs [String] :start_time Start timestamp in milliseconds + # @option kwargs [String] :end_time End timestamp in milliseconds + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/covert/small-balance-history + def list_small_balance_history(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/covert/small-balance-history', params: params) + end + + # Small asset conversion list query + # + # GET /v5/asset/covert/small-balance-list + # + # @param account_type [String] Wallet type. Only supports eb_convert_uta (Unified wallet) + # @option kwargs [String] :from_coin Source currency filter (optional). Multiple coins separated by comma, e.g. "BTC,ETH" + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/covert/small-balance-list + def list_small_balance_coins(account_type:, **kwargs) + params = kwargs.merge(account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/asset/covert/small-balance-list', params: params) + end + + # Small asset get quote + # + # POST /v5/asset/covert/get-quote + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/asset/covert/get-quote + def small_asset_quote(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/asset/covert/get-quote', body: params) + end end end end diff --git a/lib/bybit/rest_api/bot_service.rb b/lib/bybit/rest_api/bot_service.rb index fc3c4cc..b9af95a 100644 --- a/lib/bybit/rest_api/bot_service.rb +++ b/lib/bybit/rest_api/bot_service.rb @@ -9,6 +9,7 @@ class BotService < BaseService # # @param bot_id [Integer] Identifier of the DCA bot to close # @param close_mode [Integer] Settlement mode used to close the bot + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def close_dca_bot(bot_id:, close_mode:, **kwargs) body = kwargs.merge(bot_id: bot_id, close_mode: close_mode) @session.sign_request(method: :post, path: '/v5/dca/close-bot', body: body) @@ -21,6 +22,7 @@ def close_dca_bot(bot_id:, close_mode:, **kwargs) # @param parameters [Hash] DCA bot configuration parameters # @option kwargs [Hash] :tools_discovery_parameter Tools discovery parameter object # @option kwargs [String] :channel Channel identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_dca_bot(parameters:, **kwargs) body = kwargs.merge(parameters: parameters) @session.sign_request(method: :post, path: '/v5/dca/create-bot', body: body) @@ -32,6 +34,7 @@ def create_dca_bot(parameters:, **kwargs) # # @param bot_id [Integer] Combo bot ID to close # @option kwargs [Integer] :stop_type Stop type identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def close_combo_bot(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fcombobot/close', body: body) @@ -56,6 +59,7 @@ def close_combo_bot(bot_id:, **kwargs) # @option kwargs [String] :init_bonus Initial bonus amount # @option kwargs [String] :trailing_stop_percent Trailing stop percent # @option kwargs [String] :channel Channel identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_combo_bot(leverage:, init_margin:, adjust_position_mode:, symbol_settings:, **kwargs) body = kwargs.merge( leverage: leverage, @@ -71,6 +75,7 @@ def create_combo_bot(leverage:, init_margin:, adjust_position_mode:, symbol_sett # POST /v5/fcombobot/detail # # @param bot_id [Integer] Combo bot ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_combo_detail(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fcombobot/detail', body: body) @@ -91,6 +96,7 @@ def get_combo_detail(bot_id:, **kwargs) # @option kwargs [Boolean] :need_to_slippage Whether to include slippage in validation # @option kwargs [String] :app_name Application name identifier # @option kwargs [String] :trailing_stop_percent Trailing stop percent + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_combo_limit(leverage:, init_margin:, adjust_position_mode:, symbol_settings:, **kwargs) body = kwargs.merge( leverage: leverage, @@ -106,6 +112,7 @@ def get_combo_limit(leverage:, init_margin:, adjust_position_mode:, symbol_setti # POST /v5/fgridbot/close # # @param bot_id [Integer] Bot ID of the futures grid bot to close + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def close_futures_grid_bot(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fgridbot/close', body: body) @@ -140,6 +147,7 @@ def close_futures_grid_bot(bot_id:, **kwargs) # @option kwargs [String] :move_up_price Move-up trigger price # @option kwargs [String] :move_down_price Move-down trigger price # @option kwargs [String] :channel Client channel identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_futures_grid_bot(symbol:, grid_mode:, min_price:, max_price:, cell_number:, leverage:, grid_type:, total_investment:, **kwargs) body = kwargs.merge( symbol: symbol, @@ -159,6 +167,7 @@ def create_futures_grid_bot(symbol:, grid_mode:, min_price:, max_price:, cell_nu # POST /v5/fgridbot/detail # # @param bot_id [Integer] Bot ID of the futures grid bot + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_futures_grid_detail(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fgridbot/detail', body: body) @@ -185,6 +194,7 @@ def get_futures_grid_detail(bot_id:, **kwargs) # @option kwargs [String] :init_margin Initial margin amount # @option kwargs [String] :move_up_price Move-up trigger price # @option kwargs [String] :move_down_price Move-down trigger price + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def validate_futures_grid_input(symbol:, cell_number:, min_price:, max_price:, leverage:, grid_type:, grid_mode:, **kwargs) body = kwargs.merge( symbol: symbol, @@ -204,6 +214,7 @@ def validate_futures_grid_input(symbol:, cell_number:, min_price:, max_price:, l # # @param bot_id [Integer] Bot ID # @option kwargs [String] :stop_type Stop type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def close_futures_martingale_bot(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fmartingalebot/close', body: body) @@ -230,6 +241,7 @@ def close_futures_martingale_bot(bot_id:, **kwargs) # @option kwargs [String] :create_type Create type # @option kwargs [String] :init_bonus Initial bonus # @option kwargs [String] :channel Channel + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_futures_martingale_bot(symbol:, martingale_mode:, leverage:, price_float_percent:, add_position_percent:, add_position_num:, init_margin:, round_tp_percent:, **kwargs) body = kwargs.merge( symbol: symbol, @@ -249,6 +261,7 @@ def create_futures_martingale_bot(symbol:, martingale_mode:, leverage:, price_fl # POST /v5/fmartingalebot/detail # # @param bot_id [Integer] Bot ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_futures_martingale_detail(bot_id:, **kwargs) body = kwargs.merge(bot_id: bot_id) @session.sign_request(method: :post, path: '/v5/fmartingalebot/detail', body: body) @@ -270,6 +283,7 @@ def get_futures_martingale_detail(bot_id:, **kwargs) # @option kwargs [String] :entry_price Entry price # @option kwargs [Boolean] :need_to_slippage Whether to include slippage # @option kwargs [String] :app_name App name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_futures_martingale_limit(symbol:, martingale_mode:, leverage:, **kwargs) body = kwargs.merge( symbol: symbol, @@ -285,6 +299,7 @@ def get_futures_martingale_limit(symbol:, martingale_mode:, leverage:, **kwargs) # # @param grid_id [Integer] Grid bot ID # @param close_mode [Integer] Close/settlement mode + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def close_grid_bot(grid_id:, close_mode:, **kwargs) body = kwargs.merge(grid_id: grid_id, close_mode: close_mode) @session.sign_request(method: :post, path: '/v5/grid/close-grid', body: body) @@ -314,6 +329,7 @@ def close_grid_bot(grid_id:, close_mode:, **kwargs) # @option kwargs [Boolean] :enable_trailing Whether to enable trailing # @option kwargs [String] :limit_up_price Upper limit price # @option kwargs [String] :channel Channel identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_grid_bot(symbol:, max_price:, min_price:, total_investment:, cell_number:, **kwargs) body = kwargs.merge( symbol: symbol, @@ -330,7 +346,8 @@ def create_grid_bot(symbol:, max_price:, min_price:, total_investment:, cell_num # POST /v5/grid/query-grid-detail # # @param grid_id [Integer] Grid bot ID - def query_grid_detail(grid_id:, **kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def get_grid_detail(grid_id:, **kwargs) body = kwargs.merge(grid_id: grid_id) @session.sign_request(method: :post, path: '/v5/grid/query-grid-detail', body: body) end @@ -353,6 +370,7 @@ def query_grid_detail(grid_id:, **kwargs) # @option kwargs [String] :ts_percent Trailing stop percent # @option kwargs [Boolean] :enable_trailing Whether to enable trailing # @option kwargs [String] :limit_up_price Upper limit price + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def validate_grid_input(symbol:, cell_number:, min_price:, max_price:, total_investment:, **kwargs) body = kwargs.merge( symbol: symbol, diff --git a/lib/bybit/rest_api/broker_service.rb b/lib/bybit/rest_api/broker_service.rb index 4bc39c2..5025c2b 100644 --- a/lib/bybit/rest_api/broker_service.rb +++ b/lib/bybit/rest_api/broker_service.rb @@ -12,6 +12,7 @@ class BrokerService < BaseService # @param spec_code [String] Spec code # @param amount [String] Distribution amount # @param broker_id [String] Broker ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def distribute_award(account_id:, award_id:, spec_code:, amount:, broker_id:, **kwargs) params = kwargs.merge(account_id: account_id, award_id: award_id, spec_code: spec_code, amount: amount, broker_id: broker_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -23,6 +24,7 @@ def distribute_award(account_id:, award_id:, spec_code:, amount:, broker_id:, ** # POST /v5/broker/award/info # # @param id [String] Voucher ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_award_info(id:, **kwargs) params = kwargs.merge(id: id) params = Bybit::Utils::WireKeys.camelize(params) @@ -37,6 +39,7 @@ def get_award_info(id:, **kwargs) # @param award_id [String] Award ID # @param spec_code [String] Spec code # @option kwargs [Boolean] :with_used_amount Include used amount flag + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_distribution_record(account_id:, award_id:, spec_code:, **kwargs) params = kwargs.merge(account_id: account_id, award_id: award_id, spec_code: spec_code) params = Bybit::Utils::WireKeys.camelize(params) @@ -46,7 +49,7 @@ def get_distribution_record(account_id:, award_id:, spec_code:, **kwargs) # Get Broker Account Info # # GET /v5/broker/account-info - def query_account_info + def get_broker_account_info @session.sign_request(method: :get, path: '/v5/broker/account-info') end @@ -57,7 +60,8 @@ def query_account_info # @option kwargs [String] :uids UIDs to query # @option kwargs [Integer] :limit Result limit # @option kwargs [String] :cursor Pagination cursor - def query_all_uid_details(**kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def list_broker_sub_uids(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/broker/apilimit/query-all', params: params) @@ -66,7 +70,7 @@ def query_all_uid_details(**kwargs) # Query Broker Rate Limit Cap # # GET /v5/broker/apilimit/query-cap - def query_cap + def get_broker_rate_limit_cap @session.sign_request(method: :get, path: '/v5/broker/apilimit/query-cap') end @@ -80,7 +84,8 @@ def query_cap # @option kwargs [String] :uid Sub UID # @option kwargs [Integer] :limit Result limit # @option kwargs [String] :cursor Pagination cursor - def query_earning(**kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def list_broker_earnings(**kwargs) params = kwargs.dup params[:begin] = params.delete(:begin_) if params.key?(:begin_) params[:end] = params.delete(:end_) if params.key?(:end_) @@ -93,6 +98,7 @@ def query_earning(**kwargs) # POST /v5/broker/apilimit/set # # @option kwargs [Array] :list List of rate limit config entries + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def set_api_limit(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/crypto_loan_service.rb b/lib/bybit/rest_api/crypto_loan_service.rb index ee4601e..9d53aed 100644 --- a/lib/bybit/rest_api/crypto_loan_service.rb +++ b/lib/bybit/rest_api/crypto_loan_service.rb @@ -10,6 +10,7 @@ class CryptoLoanService < BaseService # @param currency [String] Collateral currency # @param amount [String] Amount to adjust # @param direction [Integer] Adjustment direction: add or remove collateral + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def adjust_ltv(currency:, amount:, direction:, **kwargs) params = kwargs.merge(currency: currency, amount: amount, direction: direction) params = Bybit::Utils::WireKeys.camelize(params) @@ -24,6 +25,7 @@ def adjust_ltv(currency:, amount:, direction:, **kwargs) # @option kwargs [String] :collateral_currency Collateral currency filter # @option kwargs [Integer] :limit Number of records per page # @option kwargs [Integer] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_adjustment_history(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -35,6 +37,7 @@ def get_adjustment_history(**kwargs) # GET /v5/crypto-loan-common/collateral-data # # @option kwargs [String] :currency Collateral currency filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_collateral_data(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -47,6 +50,7 @@ def get_collateral_data(**kwargs) # # @option kwargs [String] :currency Loanable currency filter # @option kwargs [String] :vip_level VIP level filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_loanable_data(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -58,6 +62,7 @@ def get_loanable_data(**kwargs) # GET /v5/crypto-loan-common/max-collateral-amount # # @param currency [String] Collateral currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_max_collateral_amount(currency:, **kwargs) params = kwargs.merge(currency: currency) params = Bybit::Utils::WireKeys.camelize(params) @@ -70,6 +75,7 @@ def get_max_collateral_amount(currency:, **kwargs) # # @param currency [String] Loan currency # @param collateral_list [Array] List of collateral currencies and amounts + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_max_loan(currency:, collateral_list:, **kwargs) params = kwargs.merge(currency: currency, collateral_list: collateral_list) params = Bybit::Utils::WireKeys.camelize(params) @@ -93,6 +99,7 @@ def get_position # @option kwargs [String] :term Loan term # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor used for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_borrow_contract_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -109,6 +116,7 @@ def get_fixed_borrow_contract_info(**kwargs) # @option kwargs [String] :term Loan term # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor used for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_borrow_order_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -124,6 +132,7 @@ def get_fixed_borrow_order_info(**kwargs) # @option kwargs [String] :order_by Order by field # @option kwargs [Integer] :sort Sort direction # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_borrow_order_quote(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -138,6 +147,7 @@ def get_fixed_borrow_order_quote(**kwargs) # @option kwargs [String] :order_currency Order currency # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor used for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_renew_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -154,6 +164,7 @@ def get_fixed_renew_info(**kwargs) # @option kwargs [String] :term Loan term # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor used for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_supply_contract_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -170,6 +181,7 @@ def get_fixed_supply_contract_info(**kwargs) # @option kwargs [String] :term Loan term # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor used for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_supply_order_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -185,6 +197,7 @@ def get_fixed_supply_order_info(**kwargs) # @option kwargs [String] :order_by Order by field # @option kwargs [Integer] :sort Sort direction # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_supply_order_quote(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -202,6 +215,7 @@ def get_fixed_supply_order_quote(**kwargs) # @param collateral_list [Array] List of collateral # @option kwargs [String] :auto_repay Auto repay flag # @option kwargs [String] :repay_type Repay type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def borrow_fixed(order_currency:, order_amount:, annual_rate:, term:, collateral_list:, **kwargs) params = kwargs.merge(order_currency: order_currency, order_amount: order_amount, annual_rate: annual_rate, term: term, collateral_list: collateral_list) params = Bybit::Utils::WireKeys.camelize(params) @@ -213,6 +227,7 @@ def borrow_fixed(order_currency:, order_amount:, annual_rate:, term:, collateral # POST /v5/crypto-loan-fixed/borrow-order-cancel # # @param order_id [String] Order ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_fixed_borrow_order(order_id:, **kwargs) params = kwargs.merge(order_id: order_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -225,6 +240,7 @@ def cancel_fixed_borrow_order(order_id:, **kwargs) # # @param loan_id [String] Loan ID # @param loan_currency [String] Loan currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def repay_fixed_fully(loan_id:, loan_currency:, **kwargs) params = kwargs.merge(loan_id: loan_id, loan_currency: loan_currency) params = Bybit::Utils::WireKeys.camelize(params) @@ -237,6 +253,7 @@ def repay_fixed_fully(loan_id:, loan_currency:, **kwargs) # # @param loan_id [String] Loan ID # @param collateral_list [Array] List of collateral + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def renew_fixed(loan_id:, collateral_list:, **kwargs) params = kwargs.merge(loan_id: loan_id, collateral_list: collateral_list) params = Bybit::Utils::WireKeys.camelize(params) @@ -251,6 +268,7 @@ def renew_fixed(loan_id:, collateral_list:, **kwargs) # @param loan_currency [String] Loan currency # @param collateral_coin [String] Collateral coin # @param amount [String] Repay amount + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def repay_fixed_with_collateral(loan_id:, loan_currency:, collateral_coin:, amount:, **kwargs) params = kwargs.merge(loan_id: loan_id, loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) @@ -263,6 +281,7 @@ def repay_fixed_with_collateral(loan_id:, loan_currency:, collateral_coin:, amou # # @param order_id [String] Order ID # @option kwargs [Integer] :refunded_account Refunded account + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_fixed_supply_order(order_id:, **kwargs) params = kwargs.merge(order_id: order_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -277,6 +296,7 @@ def cancel_fixed_supply_order(order_id:, **kwargs) # @option kwargs [String] :loan_currency Loan currency # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_flexible_borrow_history(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -288,6 +308,7 @@ def get_flexible_borrow_history(**kwargs) # GET /v5/crypto-loan-flexible/ongoing-coin # # @option kwargs [String] :loan_currency Loan currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_flexible_ongoing_coin(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -302,6 +323,7 @@ def get_flexible_ongoing_coin(**kwargs) # @option kwargs [String] :loan_currency Loan currency # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [Integer] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_flexible_repayment_history(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -315,6 +337,7 @@ def get_flexible_repayment_history(**kwargs) # @param loan_currency [String] Loan currency # @param loan_amount [String] Loan amount # @param collateral_list [Array] Collateral coin list + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def borrow_flexible(loan_currency:, loan_amount:, collateral_list:, **kwargs) params = kwargs.merge(loan_currency: loan_currency, loan_amount: loan_amount, collateral_list: collateral_list) params = Bybit::Utils::WireKeys.camelize(params) @@ -327,6 +350,7 @@ def borrow_flexible(loan_currency:, loan_amount:, collateral_list:, **kwargs) # # @param loan_currency [String] Loan currency # @param amount [String] Repayment amount + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def repay_flexible(loan_currency:, amount:, **kwargs) params = kwargs.merge(loan_currency: loan_currency, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) @@ -340,6 +364,7 @@ def repay_flexible(loan_currency:, amount:, **kwargs) # @param loan_currency [String] Loan currency # @param collateral_coin [String] Collateral coin # @param amount [String] Repayment amount + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def repay_flexible_with_collateral(loan_currency:, collateral_coin:, amount:, **kwargs) params = kwargs.merge(loan_currency: loan_currency, collateral_coin: collateral_coin, amount: amount) params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/earn_service.rb b/lib/bybit/rest_api/earn_service.rb index 58726f7..280020d 100644 --- a/lib/bybit/rest_api/earn_service.rb +++ b/lib/bybit/rest_api/earn_service.rb @@ -14,6 +14,7 @@ class EarnService < BaseService # @option kwargs [String] :quote_amount Quote coin amount # @option kwargs [String] :base_amount Base coin amount # @option kwargs [String] :leverage Leverage + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def add_liquidity(product_id:, order_link_id:, **kwargs) params = kwargs.merge(product_id: product_id, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -29,6 +30,7 @@ def add_liquidity(product_id:, order_link_id:, **kwargs) # @param position_id [String] Position ID # @param amount [String] Margin amount # @param quote_account_type [String] Quote coin account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def add_margin(product_id:, order_link_id:, position_id:, amount:, quote_account_type:, **kwargs) params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id, amount: amount, quote_account_type: quote_account_type) params = Bybit::Utils::WireKeys.camelize(params) @@ -40,6 +42,7 @@ def add_margin(product_id:, order_link_id:, position_id:, amount:, quote_account # POST /v5/earn/liquidity-mining/claim-interest # # @param product_id [String] Product ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def claim_liquidity_interest(product_id:, **kwargs) params = kwargs.merge(product_id: product_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -58,6 +61,7 @@ def claim_liquidity_interest(product_id:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Page size limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_advance_earn_order(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -73,6 +77,7 @@ def get_advance_earn_order(category:, **kwargs) # @option kwargs [String] :coin Coin # @option kwargs [Integer] :limit Page size limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_advance_earn_position(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -86,6 +91,7 @@ def get_advance_earn_position(category:, **kwargs) # @param category [String] Product category # @option kwargs [String] :coin Coin # @option kwargs [String] :duration Product duration + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_advance_earn_product(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -98,6 +104,7 @@ def get_advance_earn_product(category:, **kwargs) # # @param category [String] Product category # @option kwargs [Integer] :product_id Product ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_advance_earn_product_extra_info(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -112,6 +119,7 @@ def get_advance_earn_product_extra_info(category:, **kwargs) # @param initial_price [String] Initial price # @param lower_price [String] Lower price bound # @param upper_price [String] Upper price bound + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_double_win_leverage(product_id:, initial_price:, lower_price:, upper_price:, **kwargs) params = kwargs.merge(product_id: product_id, initial_price: initial_price, lower_price: lower_price, upper_price: upper_price) params = Bybit::Utils::WireKeys.camelize(params) @@ -126,6 +134,7 @@ def get_double_win_leverage(product_id:, initial_price:, lower_price:, upper_pri # @param product_id [String] Product ID # @param start_time [Integer] Start timestamp in milliseconds # @param end_time [Integer] End timestamp in milliseconds + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_apr_history(category:, product_id:, start_time:, end_time:, **kwargs) params = kwargs.merge(category: category, product_id: product_id, start_time: start_time, end_time: end_time) params = Bybit::Utils::WireKeys.camelize(params) @@ -142,6 +151,7 @@ def get_apr_history(category:, product_id:, start_time:, end_time:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Page size limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_hourly_yield_history(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -160,6 +170,7 @@ def get_hourly_yield_history(category:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Page size limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_order_history(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -173,6 +184,7 @@ def get_order_history(category:, **kwargs) # @param category [String] Product category # @option kwargs [String] :product_id Product ID # @option kwargs [String] :coin Coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_position(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -185,6 +197,7 @@ def get_position(category:, **kwargs) # # @param category [String] Product category # @option kwargs [String] :coin Coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_product(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -201,6 +214,7 @@ def get_product(category:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Page size limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_yield_history(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -219,6 +233,7 @@ def get_yield_history(category:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [Integer] :limit Result limit per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_term_order(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -232,6 +247,7 @@ def get_fixed_term_order(**kwargs) # @option kwargs [String] :product_id Product ID # @option kwargs [String] :category Product category # @option kwargs [String] :coin Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_term_position(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -243,6 +259,7 @@ def get_fixed_term_position(**kwargs) # GET /v5/earn/fixed-term/product # # @option kwargs [String] :coin Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_fixed_term_product(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -264,6 +281,7 @@ def get_hold_to_earn_product # @option kwargs [Integer] :time_start Start time in ms # @option kwargs [Integer] :time_end End time in ms # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_hold_to_earn_yield_history(limit:, **kwargs) params = kwargs.merge(limit: limit) params = Bybit::Utils::WireKeys.camelize(params) @@ -280,6 +298,7 @@ def get_hold_to_earn_yield_history(limit:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Result page size # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_liquidity_mining_liquidation_records(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -299,6 +318,7 @@ def get_liquidity_mining_liquidation_records(**kwargs) # @option kwargs [Integer] :end_time End time in ms # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_liquidity_mining_orders(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -311,6 +331,7 @@ def get_liquidity_mining_orders(**kwargs) # # @option kwargs [String] :product_id Product ID # @option kwargs [String] :base_coin Base coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_liquidity_mining_positions(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -323,6 +344,7 @@ def get_liquidity_mining_positions(**kwargs) # # @option kwargs [String] :base_coin Base coin # @option kwargs [String] :quote_coin Quote coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_liquidity_mining_products(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -339,6 +361,7 @@ def get_liquidity_mining_products(**kwargs) # @option kwargs [Integer] :end_time End time in ms # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_liquidity_mining_yield_records(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -352,6 +375,7 @@ def get_liquidity_mining_yield_records(**kwargs) # @param product_id [Integer] Product ID # @option kwargs [Integer] :start_time Start time in ms # @option kwargs [Integer] :end_time End time in ms + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_rwa_nav_chart(product_id:, **kwargs) params = kwargs.merge(product_id: product_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -370,6 +394,7 @@ def get_rwa_nav_chart(product_id:, **kwargs) # @option kwargs [Integer] :end_time End time in ms # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_rwa_order_list(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -388,6 +413,7 @@ def get_rwa_position_list # GET /v5/earn/rwa/product # # @option kwargs [String] :coin Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_rwa_product_list(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -400,6 +426,7 @@ def get_rwa_product_list(**kwargs) # # @param category [String] Product category # @param position_ids [String] Position IDs + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_smart_leverage_redeem_est_amount_list(category:, position_ids:, **kwargs) params = kwargs.merge(category: category, position_ids: position_ids) params = Bybit::Utils::WireKeys.camelize(params) @@ -415,6 +442,7 @@ def get_smart_leverage_redeem_est_amount_list(category:, position_ids:, **kwargs # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [String] :cursor Cursor for pagination # @option kwargs [Integer] :limit Result limit per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_daily_yield(coin:, **kwargs) params = kwargs.merge(coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -427,6 +455,7 @@ def get_token_daily_yield(coin:, **kwargs) # # @param coin [String] Coin name # @param range [Integer] Range window for APR history + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_historical_apr(coin:, range:, **kwargs) params = kwargs.merge(coin: coin, range: range) params = Bybit::Utils::WireKeys.camelize(params) @@ -442,6 +471,7 @@ def get_token_historical_apr(coin:, range:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [String] :cursor Cursor for pagination # @option kwargs [Integer] :limit Result limit per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_hourly_yield(coin:, **kwargs) params = kwargs.merge(coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -460,6 +490,7 @@ def get_token_hourly_yield(coin:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [String] :cursor Cursor for pagination # @option kwargs [Integer] :limit Result limit per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_order_list(coin:, **kwargs) params = kwargs.merge(coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -471,6 +502,7 @@ def get_token_order_list(coin:, **kwargs) # GET /v5/earn/token/position # # @param coin [String] Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_position(coin:, **kwargs) params = kwargs.merge(coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -482,6 +514,7 @@ def get_token_position(coin:, **kwargs) # GET /v5/earn/token/product # # @param coin [String] Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_token_product(coin:, **kwargs) params = kwargs.merge(coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -493,6 +526,7 @@ def get_token_product(coin:, **kwargs) # GET /v5/earn/coupons # # @param category [String] Product category + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def list_coupons(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @@ -507,6 +541,7 @@ def list_coupons(category:, **kwargs) # @param product_id [Integer] Product ID # @param position_id [Integer] Position ID # @param auto_reinvest [Integer] Auto-reinvest flag + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def modify_earn_position(category:, product_id:, position_id:, auto_reinvest:, **kwargs) params = kwargs.merge(category: category, product_id: product_id, position_id: position_id, auto_reinvest: auto_reinvest) params = Bybit::Utils::WireKeys.camelize(params) @@ -531,6 +566,7 @@ def modify_earn_position(category:, product_id:, position_id:, auto_reinvest:, * # @option kwargs [Hash] :double_win_stake_extra Double win stake extra info # @option kwargs [Hash] :double_win_redeem_extra Double win redeem extra info # @option kwargs [Hash] :discount_buy_extra Discount buy extra info + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def place_advance_earn_order(category:, product_id:, order_type:, amount:, account_type:, coin:, order_link_id:, **kwargs) params = kwargs.merge(category: category, product_id: product_id, order_type: order_type, amount: amount, account_type: account_type, coin: coin, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -551,6 +587,7 @@ def place_advance_earn_order(category:, product_id:, order_type:, amount:, accou # @option kwargs [String] :redeem_position_id Redeem position ID # @option kwargs [String] :to_account_type Destination account type # @option kwargs [Hash] :interest_card Interest card info + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def place_order(category:, order_type:, account_type:, amount:, coin:, product_id:, order_link_id:, **kwargs) params = kwargs.merge(category: category, order_type: order_type, account_type: account_type, amount: amount, coin: coin, product_id: product_id, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -568,6 +605,7 @@ def place_order(category:, order_type:, account_type:, amount:, coin:, product_i # @param account_type [String] Account type # @param order_link_id [String] Client-supplied order link ID # @option kwargs [Boolean] :auto_invest Whether to enable auto-invest + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def place_fixed_term_order(product_id:, category:, coin:, amount:, account_type:, order_link_id:, **kwargs) params = kwargs.merge(product_id: product_id, category: category, coin: coin, amount: amount, account_type: account_type, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -585,6 +623,7 @@ def place_fixed_term_order(product_id:, category:, coin:, amount:, account_type: # @option kwargs [String] :stake_amount Stake amount, required when orderType is Stake # @option kwargs [String] :redeem_shares Redeem shares, required when orderType is Redeem # @option kwargs [String] :account_type Account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def place_rwa_order(product_id:, order_type:, coin:, order_link_id:, **kwargs) params = kwargs.merge(product_id: product_id, order_type: order_type, coin: coin, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -600,6 +639,7 @@ def place_rwa_order(product_id:, order_type:, coin:, order_link_id:, **kwargs) # @param order_type [String] Order type (mint or redeem) # @param amount [String] Order amount # @param account_type [String] Account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def place_token_order(coin:, order_link_id:, order_type:, amount:, account_type:, **kwargs) params = kwargs.merge(coin: coin, order_link_id: order_link_id, order_type: order_type, amount: amount, account_type: account_type) params = Bybit::Utils::WireKeys.camelize(params) @@ -613,6 +653,7 @@ def place_token_order(coin:, order_link_id:, order_type:, amount:, account_type: # @param plan_id [String] Investment plan ID # @option kwargs [Integer] :start_time Start timestamp (ms) # @option kwargs [Integer] :end_time End timestamp (ms) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_asset_trend(plan_id:, **kwargs) params = kwargs.merge(plan_id: plan_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -626,6 +667,7 @@ def pwm_asset_trend(plan_id:, **kwargs) # @param plan_id [String] Investment plan ID # @param order_link_id [String] Client-generated unique order link ID # @option kwargs [String] :to_account_type Destination account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_claim(plan_id:, order_link_id:, **kwargs) params = kwargs.merge(plan_id: plan_id, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -639,6 +681,7 @@ def pwm_claim(plan_id:, order_link_id:, **kwargs) # @param products [Array] List of products to include in the plan # @param order_link_id [String] Client-generated unique order link ID # @option kwargs [String] :account_type Source account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_create_custom_plan(products:, order_link_id:, **kwargs) params = kwargs.merge(products: products, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -652,6 +695,7 @@ def pwm_create_custom_plan(products:, order_link_id:, **kwargs) # @param fund_id [String] Fund ID # @option kwargs [Integer] :start_time Start timestamp (ms) # @option kwargs [Integer] :end_time End timestamp (ms) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_fund_nav(fund_id:, **kwargs) params = kwargs.merge(fund_id: fund_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -667,6 +711,7 @@ def pwm_fund_nav(fund_id:, **kwargs) # @param to_user_id [Integer] Destination sub-account UID # @param amount [String] Transfer amount # @param coin [String] Coin symbol + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_fund_transfer(transfer_id:, from_user_id:, to_user_id:, amount:, coin:, **kwargs) params = kwargs.merge(transfer_id: transfer_id, from_user_id: from_user_id, to_user_id: to_user_id, amount: amount, coin: coin) params = Bybit::Utils::WireKeys.camelize(params) @@ -678,6 +723,7 @@ def pwm_fund_transfer(transfer_id:, from_user_id:, to_user_id:, amount:, coin:, # GET /v5/earn/pwm/investment-plan/new-plan # # @param plan_id [String] Investment plan identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_get_new_plan_detail(plan_id:, **kwargs) params = kwargs.merge(plan_id: plan_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -689,6 +735,7 @@ def pwm_get_new_plan_detail(plan_id:, **kwargs) # GET /v5/earn/pwm/investment-plan/detail # # @param plan_id [String] Investment plan identifier + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_get_plan_detail(plan_id:, **kwargs) params = kwargs.merge(plan_id: plan_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -705,6 +752,7 @@ def pwm_get_plan_detail(plan_id:, **kwargs) # @param management_fee_rate [String] Management fee rate # @param req_link_id [String] Client-supplied idempotency key # @option kwargs [String] :fund_introduction Fund introduction text + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_create_fund(fund_name:, coin:, profit_share_rate:, management_fee_rate:, req_link_id:, **kwargs) params = kwargs.merge(fund_name: fund_name, coin: coin, profit_share_rate: profit_share_rate, management_fee_rate: management_fee_rate, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -720,6 +768,7 @@ def pwm_inst_create_fund(fund_name:, coin:, profit_share_rate:, management_fee_r # @param plan_type [String] Investment plan type # @param investment_distribution [Array] Fund allocation distribution # @param req_link_id [String] Client-supplied idempotency key + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_create_investment_plan(account_uid:, plan_name:, plan_type:, investment_distribution:, req_link_id:, **kwargs) params = kwargs.merge(account_uid: account_uid, plan_name: plan_name, plan_type: plan_type, investment_distribution: investment_distribution, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -732,6 +781,7 @@ def pwm_inst_create_investment_plan(account_uid:, plan_name:, plan_type:, invest # # @param fund_id [String] Fund identifier # @param req_link_id [String] Client-supplied idempotency key + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_create_sub_account(fund_id:, req_link_id:, **kwargs) params = kwargs.merge(fund_id: fund_id, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -747,6 +797,7 @@ def pwm_inst_create_sub_account(fund_id:, req_link_id:, **kwargs) # @option kwargs [String] :subscription_uid Subscription account UID # @option kwargs [Integer] :limit Result page size # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_get_investment_plans(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -762,6 +813,7 @@ def pwm_inst_get_investment_plans(**kwargs) # @option kwargs [String] :status Fund status filter # @option kwargs [Integer] :limit Result page size # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_list_funds(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -779,6 +831,7 @@ def pwm_inst_list_funds(**kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Result page size # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_list_orders(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -793,6 +846,7 @@ def pwm_inst_list_orders(**kwargs) # @param req_link_id [String] Client-supplied idempotency key # @option kwargs [String] :update_status New plan status # @option kwargs [Array] :update_funds Updated fund allocation + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_manage_investment_plan(plan_id:, req_link_id:, **kwargs) params = kwargs.merge(plan_id: plan_id, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -806,6 +860,7 @@ def pwm_inst_manage_investment_plan(plan_id:, req_link_id:, **kwargs) # @param order_id [String] Order identifier # @param action [String] Approve or reject action # @param req_link_id [String] Client-supplied idempotency key + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_manage_order(order_id:, action:, req_link_id:, **kwargs) params = kwargs.merge(order_id: order_id, action: action, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -818,6 +873,7 @@ def pwm_inst_manage_order(order_id:, action:, req_link_id:, **kwargs) # # @param fund_id [String] Fund identifier # @param req_link_id [String] Client-supplied idempotency key + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_inst_settle_profit(fund_id:, req_link_id:, **kwargs) params = kwargs.merge(fund_id: fund_id, req_link_id: req_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -834,6 +890,7 @@ def pwm_inst_settle_profit(fund_id:, req_link_id:, **kwargs) # @param amount [String] Investment amount # @param order_link_id [String] Client-generated unique order link ID # @option kwargs [String] :account_type Source account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_invest_more(plan_id:, category:, product_id:, amount:, order_link_id:, **kwargs) params = kwargs.merge(plan_id: plan_id, category: category, product_id: product_id, amount: amount, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -848,6 +905,7 @@ def pwm_invest_more(plan_id:, category:, product_id:, amount:, order_link_id:, * # @option kwargs [String] :status Plan status filter # @option kwargs [Integer] :limit Result page size # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_list_investment_plans(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -867,6 +925,7 @@ def pwm_list_investment_plans(**kwargs) # @option kwargs [Integer] :limit Result limit per page # @option kwargs [String] :cursor Pagination cursor # @option kwargs [String] :order_link_id Client order link ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_list_order(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -886,6 +945,7 @@ def pwm_list_product_cards # # @option kwargs [String] :transfer_id Client-supplied transfer identifier # @option kwargs [Integer] :from_user_id Source sub-account UID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_query_fund_transfer_result(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -903,6 +963,7 @@ def pwm_query_fund_transfer_result(**kwargs) # @option kwargs [String] :shares Number of shares to redeem # @option kwargs [String] :amount Amount to redeem # @option kwargs [Integer] :position_id Position ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_redeem(plan_id:, category:, product_id:, order_link_id:, **kwargs) params = kwargs.merge(plan_id: plan_id, category: category, product_id: product_id, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -916,6 +977,7 @@ def pwm_redeem(plan_id:, category:, product_id:, order_link_id:, **kwargs) # @param plan_id [String] Investment plan ID # @param order_link_id [String] Client-generated unique order link ID # @option kwargs [String] :account_type Source account type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def pwm_subscribe(plan_id:, order_link_id:, **kwargs) params = kwargs.merge(plan_id: plan_id, order_link_id: order_link_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -929,6 +991,7 @@ def pwm_subscribe(plan_id:, order_link_id:, **kwargs) # @param product_id [String] Product ID # @param category [String] Product category # @param position_id [String] Position ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def redeem_fixed_term(product_id:, category:, position_id:, **kwargs) params = kwargs.merge(product_id: product_id, category: category, position_id: position_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -942,6 +1005,7 @@ def redeem_fixed_term(product_id:, category:, position_id:, **kwargs) # @param product_id [String] Product ID # @param order_link_id [String] Client order id # @param position_id [String] Position ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def reinvest_liquidity(product_id:, order_link_id:, position_id:, **kwargs) params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -957,6 +1021,7 @@ def reinvest_liquidity(product_id:, order_link_id:, position_id:, **kwargs) # @param position_id [String] Position ID # @option kwargs [Integer] :remove_rate Remove rate percent # @option kwargs [String] :remove_type Remove type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def remove_liquidity(product_id:, order_link_id:, position_id:, **kwargs) params = kwargs.merge(product_id: product_id, order_link_id: order_link_id, position_id: position_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -971,6 +1036,7 @@ def remove_liquidity(product_id:, order_link_id:, position_id:, **kwargs) # @param category [String] Product category # @param position_id [String] Position ID # @param status [String] Auto-invest status + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def set_fixed_term_auto_invest(product_id:, category:, position_id:, status:, **kwargs) params = kwargs.merge(product_id: product_id, category: category, position_id: position_id, status: status) params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/market_service.rb b/lib/bybit/rest_api/market_service.rb index bd52abd..252ac75 100644 --- a/lib/bybit/rest_api/market_service.rb +++ b/lib/bybit/rest_api/market_service.rb @@ -8,6 +8,7 @@ class MarketService < BaseService # GET /v5/market/adlalert # # @option kwargs [String] :symbol Symbol name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/adl-alert def get_adl_alert(**kwargs) params = kwargs.dup @@ -25,6 +26,7 @@ def get_adl_alert(**kwargs) # @option kwargs [String] :settle_coin Settle coin # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/delivery-price def get_delivery_price(category:, **kwargs) params = kwargs.merge(category: category) @@ -38,6 +40,7 @@ def get_delivery_price(category:, **kwargs) # # @param product_type [String] Product type # @option kwargs [String] :group_id Fee group id + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/fee-group-info def get_fee_group_info(product_type:, **kwargs) params = kwargs.merge(product_type: product_type) @@ -54,6 +57,7 @@ def get_fee_group_info(product_type:, **kwargs) # @option kwargs [Integer] :start_time Start timestamp (ms) # @option kwargs [Integer] :end_time End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/history-fund-rate def get_funding_rate_history(category:, symbol:, **kwargs) params = kwargs.merge(category: category, symbol: symbol) @@ -71,6 +75,7 @@ def get_funding_rate_history(category:, symbol:, **kwargs) # @option kwargs [Integer] :period Period # @option kwargs [Integer] :start_time Start timestamp (ms) # @option kwargs [Integer] :end_time End timestamp (ms) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/iv def get_historical_volatility(category:, **kwargs) params = kwargs.merge(category: category) @@ -83,6 +88,7 @@ def get_historical_volatility(category:, **kwargs) # GET /v5/market/index-price-components # # @param index_name [String] Index name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_index_price_components(index_name:, **kwargs) params = kwargs.merge(index_name: index_name) params = Bybit::Utils::WireKeys.camelize(params) @@ -99,6 +105,7 @@ def get_index_price_components(index_name:, **kwargs) # @option kwargs [Integer] :start Start timestamp (ms) # @option kwargs [Integer] :end_ End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/index-kline def get_index_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) @@ -117,6 +124,7 @@ def get_index_price_kline(symbol:, interval:, **kwargs) # @option kwargs [String] :base_coin Base coin # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/instrument def get_instruments_info(category:, **kwargs) params = kwargs.merge(category: category) @@ -129,6 +137,7 @@ def get_instruments_info(category:, **kwargs) # GET /v5/market/insurance # # @option kwargs [String] :coin Coin name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/insurance def get_insurance_pool(**kwargs) params = kwargs.dup @@ -147,6 +156,7 @@ def get_insurance_pool(**kwargs) # @option kwargs [String] :end_time End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/long-short-ratio def get_long_short_ratio(category:, symbol:, period:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, period: period) @@ -164,6 +174,7 @@ def get_long_short_ratio(category:, symbol:, period:, **kwargs) # @option kwargs [Integer] :start Start timestamp (ms) # @option kwargs [Integer] :end_ End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/mark-kline def get_mark_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) @@ -182,6 +193,7 @@ def get_mark_price_kline(symbol:, interval:, **kwargs) # @option kwargs [Integer] :start Start timestamp (ms) # @option kwargs [Integer] :end_ End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/kline def get_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) @@ -197,6 +209,7 @@ def get_kline(symbol:, interval:, **kwargs) # @param category [String] Product type # @param base_coin [String] Base coin # @option kwargs [String] :settle_coin Settle coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/new-delivery-price def get_new_delivery_price(category:, base_coin:, **kwargs) params = kwargs.merge(category: category, base_coin: base_coin) @@ -215,6 +228,7 @@ def get_new_delivery_price(category:, base_coin:, **kwargs) # @option kwargs [Integer] :end_time End timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/open-interest def get_open_interest(category:, symbol:, interval_time:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, interval_time: interval_time) @@ -228,6 +242,7 @@ def get_open_interest(category:, symbol:, interval_time:, **kwargs) # # @param symbol [String] Symbol name # @option kwargs [String] :category Product type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_order_price_limit(symbol:, **kwargs) params = kwargs.merge(symbol: symbol) params = Bybit::Utils::WireKeys.camelize(params) @@ -241,6 +256,7 @@ def get_order_price_limit(symbol:, **kwargs) # @param category [String] Product type: spot, linear, inverse, option # @param symbol [String] Symbol name # @option kwargs [Integer] :limit Limit size for each bid/ask + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/orderbook def get_orderbook(category:, symbol:, **kwargs) params = kwargs.merge(category: category, symbol: symbol) @@ -258,6 +274,7 @@ def get_orderbook(category:, symbol:, **kwargs) # @option kwargs [Integer] :start The start timestamp (ms) # @option kwargs [Integer] :end_ The end timestamp (ms) # @option kwargs [Integer] :limit Limit for data size per page. [1, 1000]. Default: 200 + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/premium-index-kline def get_premium_index_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) @@ -275,6 +292,7 @@ def get_premium_index_price_kline(symbol:, interval:, **kwargs) # @option kwargs [String] :base_coin Base coin, for option only # @option kwargs [String] :option_type Option type: Call or Put, for option only # @option kwargs [Integer] :limit Limit for data size per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/recent-trade def get_recent_public_trades(category:, **kwargs) params = kwargs.merge(category: category) @@ -289,6 +307,7 @@ def get_recent_public_trades(category:, **kwargs) # @param category [String] Product type: linear, inverse # @option kwargs [String] :symbol Symbol name # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/risk-limit def get_risk_limit(category:, **kwargs) params = kwargs.merge(category: category) @@ -303,6 +322,7 @@ def get_risk_limit(category:, **kwargs) # @param symbol [String] Symbol name # @param limit [Integer] Limit size for each bid/ask # @option kwargs [String] :category Product type: spot + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/rpi-orderbook def get_rpi_orderbook(symbol:, limit:, **kwargs) params = kwargs.merge(symbol: symbol, limit: limit) @@ -327,6 +347,7 @@ def get_server_time # @option kwargs [String] :symbol Symbol name # @option kwargs [String] :base_coin Base coin, for option only # @option kwargs [String] :exp_date Expiry date, for option only + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/market/tickers def get_tickers(category:, **kwargs) params = kwargs.merge(category: category) diff --git a/lib/bybit/rest_api/p2p_service.rb b/lib/bybit/rest_api/p2p_service.rb index 1f4e3d2..7222ab9 100644 --- a/lib/bybit/rest_api/p2p_service.rb +++ b/lib/bybit/rest_api/p2p_service.rb @@ -19,6 +19,7 @@ def get_account_info # @param side [String] Side: 0 buy, 1 sell # @option kwargs [String] :page Page number # @option kwargs [String] :size Page size + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_ads(token_id:, currency_id:, side:, **kwargs) params = kwargs.merge(token_id: token_id, currency_id: currency_id, side: side) params = Bybit::Utils::WireKeys.camelize(params) @@ -36,6 +37,7 @@ def get_ads(token_id:, currency_id:, side:, **kwargs) # @option kwargs [String] :end_time End time in milliseconds # @option kwargs [String] :token_id Token ID # @option kwargs [Integer] :side Side: 0 buy, 1 sell + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_all_orders(page:, size:, **kwargs) params = kwargs.merge(page: page, size: size) params = Bybit::Utils::WireKeys.camelize(params) @@ -49,6 +51,7 @@ def get_all_orders(page:, size:, **kwargs) # @param order_id [String] Order ID # @param size [String] Page size # @option kwargs [String] :current_page Current page number + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_chat_messages(order_id:, size:, **kwargs) params = kwargs.merge(order_id: order_id, size: size) params = Bybit::Utils::WireKeys.camelize(params) @@ -61,6 +64,7 @@ def get_chat_messages(order_id:, size:, **kwargs) # # @option kwargs [String] :original_uid Original user ID of the counterparty. # @option kwargs [String] :order_id P2P order ID. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_counterparty_user_info(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -72,6 +76,7 @@ def get_counterparty_user_info(**kwargs) # POST /v5/p2p/item/info # # @param item_id [String] Advertisement ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_my_ad_details(item_id:, **kwargs) params = kwargs.merge(item_id: item_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -89,6 +94,7 @@ def get_my_ad_details(item_id:, **kwargs) # @option kwargs [String] :page Page number # @option kwargs [String] :size Page size # @option kwargs [String] :currency_id Currency ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_my_ads(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -100,6 +106,7 @@ def get_my_ads(**kwargs) # POST /v5/p2p/order/info # # @param order_id [String] Order ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_order_detail(order_id:, **kwargs) params = kwargs.merge(order_id: order_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -117,6 +124,7 @@ def get_order_detail(order_id:, **kwargs) # @option kwargs [String] :end_time End time in milliseconds # @option kwargs [String] :token_id Token ID # @option kwargs [Integer] :side Side: 0 buy, 1 sell + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_pending_orders(page:, size:, **kwargs) params = kwargs.merge(page: page, size: size) params = Bybit::Utils::WireKeys.camelize(params) @@ -137,6 +145,7 @@ def get_user_payment # @param order_id [String] Order ID # @param payment_type [String] Payment method type # @param payment_id [String] Payment method ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def mark_order_as_paid(order_id:, payment_type:, payment_id:, **kwargs) params = kwargs.merge(order_id: order_id, payment_type: payment_type, payment_id: payment_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -161,6 +170,7 @@ def mark_order_as_paid(order_id:, payment_type:, payment_id:, **kwargs) # @param quantity [String] Ad quantity # @param payment_period [String] Payment period in minutes # @param item_type [String] Item type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_ad(token_id:, currency_id:, side:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, quantity:, payment_period:, item_type:, **kwargs) params = kwargs.merge(token_id: token_id, currency_id: currency_id, side: side, price_type: price_type, premium: premium, price: price, min_amount: min_amount, max_amount: max_amount, remark: remark, trading_preference_set: trading_preference_set, payment_ids: payment_ids, quantity: quantity, payment_period: payment_period, item_type: item_type) params = Bybit::Utils::WireKeys.camelize(params) @@ -172,6 +182,7 @@ def create_ad(token_id:, currency_id:, side:, price_type:, premium:, price:, min # POST /v5/p2p/order/finish # # @param order_id [String] Order ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def release_assets(order_id:, **kwargs) params = kwargs.merge(order_id: order_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -183,6 +194,7 @@ def release_assets(order_id:, **kwargs) # POST /v5/p2p/item/cancel # # @param item_id [String] Advertisement ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def remove_ad(item_id:, **kwargs) params = kwargs.merge(item_id: item_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -198,6 +210,7 @@ def remove_ad(item_id:, **kwargs) # @param order_id [String] Order ID # @param msg_uuid [String] Message UUID # @option kwargs [String] :file_name File name when sending a file + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def send_chat_message(message:, content_type:, order_id:, msg_uuid:, **kwargs) params = kwargs.merge(message: message, content_type: content_type, order_id: order_id, msg_uuid: msg_uuid) params = Bybit::Utils::WireKeys.camelize(params) @@ -220,6 +233,7 @@ def send_chat_message(message:, content_type:, order_id:, msg_uuid:, **kwargs) # @param action_type [String] Action type: MODIFY or ACTIVE # @param quantity [String] Ad quantity # @param payment_period [String] Payment period in minutes + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def update_ad(id:, price_type:, premium:, price:, min_amount:, max_amount:, remark:, trading_preference_set:, payment_ids:, action_type:, quantity:, payment_period:, **kwargs) params = kwargs.merge(id: id, price_type: price_type, premium: premium, price: price, min_amount: min_amount, max_amount: max_amount, remark: remark, trading_preference_set: trading_preference_set, payment_ids: payment_ids, action_type: action_type, quantity: quantity, payment_period: payment_period) params = Bybit::Utils::WireKeys.camelize(params) @@ -231,6 +245,7 @@ def update_ad(id:, price_type:, premium:, price:, min_amount:, max_amount:, rema # POST /v5/p2p/oss/upload_file # # @param upload_file [String] File to upload + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def upload_chat_file(upload_file:, **kwargs) params = kwargs.merge(upload_file: upload_file) params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/position_service.rb b/lib/bybit/rest_api/position_service.rb index 068bca6..d9c7dc9 100644 --- a/lib/bybit/rest_api/position_service.rb +++ b/lib/bybit/rest_api/position_service.rb @@ -11,6 +11,7 @@ class PositionService < BaseService # @param symbol [String] Symbol name # @param margin [String] Add or reduce. To add, then 10; to reduce, then -10 # @option kwargs [Integer] :position_idx Used to identify positions in different position modes + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/manual-add-margin def add_reduce_margin(category:, symbol:, margin:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, margin: margin) @@ -24,6 +25,7 @@ def add_reduce_margin(category:, symbol:, margin:, **kwargs) # # @param category [String] Product type # @param symbol [String] Symbol name + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/confirm-mmr def confirm_new_risk_limit(category:, symbol:, **kwargs) params = kwargs.merge(category: category, symbol: symbol) @@ -41,8 +43,9 @@ def confirm_new_risk_limit(category:, symbol:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/close-position - def get_close_position(category:, **kwargs) + def get_closed_positions(category:, **kwargs) params = kwargs.merge(category: category) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/position/get-closed-positions', params: params) @@ -58,6 +61,7 @@ def get_close_position(category:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in ms # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/close-pnl def get_closed_pnl(category:, **kwargs) params = kwargs.merge(category: category) @@ -77,6 +81,7 @@ def get_closed_pnl(category:, **kwargs) # @option kwargs [String] :block_trade_id Block trade ID # @option kwargs [String] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/move-position-history def get_move_position_history(**kwargs) params = kwargs.dup @@ -94,6 +99,7 @@ def get_move_position_history(**kwargs) # @option kwargs [String] :settle_coin Settle coin # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position def get_positions(category:, **kwargs) params = kwargs.merge(category: category) @@ -108,6 +114,7 @@ def get_positions(category:, **kwargs) # @param from_uid [String] Original UID (from which the position is moved) # @param to_uid [String] Target UID (to which the position is moved) # @param list [Array] Positions to move (array of objects) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/move-position def move_position(from_uid:, to_uid:, list:, **kwargs) params = kwargs.merge(from_uid: from_uid, to_uid: to_uid, list: list) @@ -123,6 +130,7 @@ def move_position(from_uid:, to_uid:, list:, **kwargs) # @param symbol [String] Symbol name # @param auto_add_margin [Integer] 0: disable, 1: enable # @option kwargs [Integer] :position_idx Used to identify positions in different position modes + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/auto-add-margin def set_auto_add_margin(category:, symbol:, auto_add_margin:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, auto_add_margin: auto_add_margin) @@ -138,6 +146,7 @@ def set_auto_add_margin(category:, symbol:, auto_add_margin:, **kwargs) # @param symbol [String] Symbol name # @param buy_leverage [String] Buy leverage # @param sell_leverage [String] Sell leverage + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/leverage def set_leverage(category:, symbol:, buy_leverage:, sell_leverage:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, buy_leverage: buy_leverage, sell_leverage: sell_leverage) @@ -165,6 +174,7 @@ def set_leverage(category:, symbol:, buy_leverage:, sell_leverage:, **kwargs) # @option kwargs [String] :sl_limit_price The limit order price when stop loss price is triggered # @option kwargs [String] :tp_order_type Take profit order type: Market or Limit # @option kwargs [String] :sl_order_type Stop loss order type: Market or Limit + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/trading-stop def set_trading_stop(category:, symbol:, tpsl_mode:, position_idx:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, tpsl_mode: tpsl_mode, position_idx: position_idx) @@ -180,6 +190,7 @@ def set_trading_stop(category:, symbol:, tpsl_mode:, position_idx:, **kwargs) # @param mode [Integer] Position mode: 0 (Merged Single), 3 (Both Sides) # @option kwargs [String] :symbol Symbol name # @option kwargs [String] :coin Coin + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/position/position-mode def switch_position_mode(category:, mode:, **kwargs) params = kwargs.merge(category: category, mode: mode) diff --git a/lib/bybit/rest_api/rfq_service.rb b/lib/bybit/rest_api/rfq_service.rb index 666f9f1..1279f77 100644 --- a/lib/bybit/rest_api/rfq_service.rb +++ b/lib/bybit/rest_api/rfq_service.rb @@ -8,6 +8,7 @@ class RfqService < BaseService # POST /v5/rfq/accept-other-quote # # @param rfq_id [String] RFQ ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def accept_non_lp_quote(rfq_id:, **kwargs) params = kwargs.merge(rfq_id: rfq_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -17,6 +18,7 @@ def accept_non_lp_quote(rfq_id:, **kwargs) # Cancel all active quotes. # # POST /v5/rfq/cancel-all-quotes + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_all_quotes(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -26,6 +28,7 @@ def cancel_all_quotes(**kwargs) # Cancel all active RFQs. # # POST /v5/rfq/cancel-all-rfq + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_all_rfqs(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -39,6 +42,7 @@ def cancel_all_rfqs(**kwargs) # @option kwargs [String] :quote_id Quote ID # @option kwargs [String] :rfq_id RFQ ID # @option kwargs [String] :quote_link_id User-defined quote link ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_quote(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -51,6 +55,7 @@ def cancel_quote(**kwargs) # # @option kwargs [String] :rfq_id RFQ ID # @option kwargs [String] :rfq_link_id User-defined RFQ link ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def cancel_rfq(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -67,6 +72,7 @@ def cancel_rfq(**kwargs) # @option kwargs [Integer] :expire_in Expiration time in seconds # @option kwargs [Array] :quote_buy_list Buy side quote entries # @option kwargs [Array] :quote_sell_list Sell side quote entries + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_quote(rfq_id:, **kwargs) params = kwargs.merge(rfq_id: rfq_id) params = Bybit::Utils::WireKeys.camelize(params) @@ -82,6 +88,7 @@ def create_quote(rfq_id:, **kwargs) # @option kwargs [String] :rfq_link_id User-defined RFQ link ID # @option kwargs [Boolean] :anonymous Whether to submit the RFQ anonymously # @option kwargs [String] :strategy_type Strategy type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def create_rfq(counterparties:, list:, **kwargs) params = kwargs.merge(counterparties: counterparties, list: list) params = Bybit::Utils::WireKeys.camelize(params) @@ -95,6 +102,7 @@ def create_rfq(counterparties:, list:, **kwargs) # @param rfq_id [String] RFQ ID # @param quote_id [String] Quote ID # @param quote_side [String] Quote side (Buy or Sell) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def execute_quote(rfq_id:, quote_id:, quote_side:, **kwargs) params = kwargs.merge(rfq_id: rfq_id, quote_id: quote_id, quote_side: quote_side) params = Bybit::Utils::WireKeys.camelize(params) @@ -109,6 +117,7 @@ def execute_quote(rfq_id:, quote_id:, quote_side:, **kwargs) # @option kwargs [Integer] :end_time End timestamp in milliseconds # @option kwargs [Integer] :limit Maximum number of records to return # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_public_trades(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -126,6 +135,7 @@ def get_public_trades(**kwargs) # @option kwargs [String] :status Quote status filter # @option kwargs [Integer] :limit Maximum number of records to return # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_quotes(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -140,6 +150,7 @@ def get_quotes(**kwargs) # @option kwargs [String] :quote_id Quote ID # @option kwargs [String] :quote_link_id User-defined quote link ID # @option kwargs [String] :trader_type Trader type (Taker or Maker) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_quotes_realtime(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -149,6 +160,7 @@ def get_quotes_realtime(**kwargs) # Query RFQ configuration information. # # GET /v5/rfq/config + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_config(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -165,6 +177,7 @@ def get_config(**kwargs) # @option kwargs [String] :status RFQ status filter # @option kwargs [Integer] :limit Maximum number of records to return # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_rfqs(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -178,6 +191,7 @@ def get_rfqs(**kwargs) # @option kwargs [String] :rfq_id RFQ ID # @option kwargs [String] :rfq_link_id User-defined RFQ link ID # @option kwargs [String] :trader_type Trader type (Taker or Maker) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_rfqs_realtime(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -196,6 +210,7 @@ def get_rfqs_realtime(**kwargs) # @option kwargs [String] :status Trade status filter # @option kwargs [Integer] :limit Maximum number of records to return # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_trade_history(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) diff --git a/lib/bybit/rest_api/spot_margin_service.rb b/lib/bybit/rest_api/spot_margin_service.rb index dc045c3..ed74cf1 100644 --- a/lib/bybit/rest_api/spot_margin_service.rb +++ b/lib/bybit/rest_api/spot_margin_service.rb @@ -11,6 +11,7 @@ class SpotMarginService < BaseService # @option kwargs [String] :vip_level VIP level # @option kwargs [Integer] :start_time Start time in milliseconds # @option kwargs [Integer] :end_time End time in milliseconds + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/historical-interest def get_historical_interest_rate(currency:, **kwargs) params = kwargs.merge(currency: currency) @@ -23,6 +24,7 @@ def get_historical_interest_rate(currency:, **kwargs) # GET /v5/spot-margin-trade/position-tiers # # @option kwargs [String] :currency Currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/position-tiers def get_position_tiers(**kwargs) params = kwargs.dup @@ -35,6 +37,7 @@ def get_position_tiers(**kwargs) # GET /v5/spot-margin-trade/collateral # # @option kwargs [String] :currency Currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_tiered_collateral_ratio(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -47,6 +50,7 @@ def get_tiered_collateral_ratio(**kwargs) # # @option kwargs [String] :vip_level VIP level # @option kwargs [String] :currency Currency + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/vip-margin def get_vip_margin_data(**kwargs) params = kwargs.dup diff --git a/lib/bybit/rest_api/trade_service.rb b/lib/bybit/rest_api/trade_service.rb index 8ac06ca..a5625e8 100644 --- a/lib/bybit/rest_api/trade_service.rb +++ b/lib/bybit/rest_api/trade_service.rb @@ -18,6 +18,7 @@ class TradeService < BaseService # @option kwargs [String] :exec_type Execution type # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/execution def get_execution_history(category:, **kwargs) params = kwargs.merge(category: category) @@ -45,6 +46,7 @@ def get_execution_history(category:, **kwargs) # @option kwargs [String] :trigger_by Trigger price type # @option kwargs [String] :tp_limit_price Take profit limit price # @option kwargs [String] :sl_limit_price Stop loss limit price + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/amend-order def amend_order(category:, symbol:, **kwargs) params = kwargs.merge(category: category, symbol: symbol) @@ -58,6 +60,7 @@ def amend_order(category:, symbol:, **kwargs) # # @param category [String] Product type # @param request [Array] Array of order amend objects + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/batch-amend def batch_amend_orders(category:, request:, **kwargs) params = kwargs.merge(category: category, request: request) @@ -71,6 +74,7 @@ def batch_amend_orders(category:, request:, **kwargs) # # @param category [String] Product type # @param request [Array] Array of order cancel objects + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/batch-cancel def batch_cancel_orders(category:, request:, **kwargs) params = kwargs.merge(category: category, request: request) @@ -84,6 +88,7 @@ def batch_cancel_orders(category:, request:, **kwargs) # # @param category [String] Product type # @param request [Array] Array of order create objects + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/batch-place def batch_create_orders(category:, request:, **kwargs) params = kwargs.merge(category: category, request: request) @@ -101,6 +106,7 @@ def batch_create_orders(category:, request:, **kwargs) # @option kwargs [String] :settle_coin Settle coin # @option kwargs [String] :order_filter Order filter # @option kwargs [String] :stop_order_type Stop order type + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/cancel-all def cancel_all_orders(category:, **kwargs) params = kwargs.merge(category: category) @@ -117,6 +123,7 @@ def cancel_all_orders(category:, **kwargs) # @option kwargs [String] :order_id Order ID # @option kwargs [String] :order_link_id User customised order ID # @option kwargs [String] :order_filter Order filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/cancel-order def cancel_order(category:, symbol:, **kwargs) params = kwargs.merge(category: category, symbol: symbol) @@ -162,6 +169,7 @@ def cancel_order(category:, symbol:, **kwargs) # @option kwargs [String] :bbo_side_type BBO side type # @option kwargs [String] :bbo_level BBO level # @option kwargs [Boolean] :rpi_taker_access RPI taker access flag + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/create-order def create_order(category:, symbol:, side:, order_type:, qty:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, side: side, order_type: order_type, qty: qty) @@ -175,8 +183,9 @@ def create_order(category:, symbol:, side:, order_type:, qty:, **kwargs) # # @param time_window [Integer] DCP time window in seconds # @option kwargs [String] :product Product type for DCP + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/dcp - def dcp_set_timewindow(time_window:, **kwargs) + def set_dcp_time_window(time_window:, **kwargs) params = kwargs.merge(time_window: time_window) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/order/disconnected-cancel-all', body: params) @@ -196,6 +205,7 @@ def dcp_set_timewindow(time_window:, **kwargs) # @option kwargs [String] :order_filter Order filter # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/open-order def get_open_orders(category:, **kwargs) params = kwargs.merge(category: category) @@ -219,6 +229,7 @@ def get_open_orders(category:, **kwargs) # @option kwargs [Integer] :end_time End time (ms) # @option kwargs [Integer] :limit Limit for data size per page # @option kwargs [String] :cursor Cursor for pagination + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/order-list def get_order_history(category:, **kwargs) params = kwargs.merge(category: category) @@ -233,6 +244,7 @@ def get_order_history(category:, **kwargs) # @param category [String] Product type # @param symbol [String] Symbol name # @param side [String] Buy or Sell + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/spot-borrow-quota def get_spot_borrow_quota(category:, symbol:, side:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, side: side) @@ -265,6 +277,7 @@ def get_spot_borrow_quota(category:, symbol:, side:, **kwargs) # @option kwargs [String] :tp_order_type Take profit order type # @option kwargs [String] :sl_order_type Stop loss order type # @option kwargs [String] :order_iv Implied volatility (options) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/order/pre-check-order def pre_check_order(category:, symbol:, side:, order_type:, qty:, **kwargs) params = kwargs.merge(category: category, symbol: symbol, side: side, order_type: order_type, qty: qty) diff --git a/lib/bybit/rest_api/user_service.rb b/lib/bybit/rest_api/user_service.rb index cf9202e..254c6b9 100644 --- a/lib/bybit/rest_api/user_service.rb +++ b/lib/bybit/rest_api/user_service.rb @@ -9,13 +9,21 @@ class UserService < BaseService # # @param subuid [Integer] Sub UID # @param read_only [Integer] Read-only permission flag (0: read-write, 1: read-only) - # @param permissions [Hash] API key permission settings + # @param permissions [Hash] API key permission settings. Bybit V5 expects + # PascalCase group keys — e.g. `{ 'ContractTrade' => ['Order'], 'Spot' => ['Order'], 'Wallet' => ['AccountTransfer'] }`. + # The SDK passes this hash through VERBATIM (no snake→camel conversion) + # so callers must use the docs-spelling of each group name. # @option kwargs [String] :ips Bound IP addresses (comma-separated) # @option kwargs [String] :note API key note + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/create-subuid-apikey def create_sub_api_key(subuid:, read_only:, permissions:, **kwargs) - params = kwargs.merge(subuid: subuid, read_only: read_only, permissions: permissions) + params = kwargs.merge(subuid: subuid, read_only: read_only) params = Bybit::Utils::WireKeys.camelize(params) + # `permissions` is intentionally re-attached AFTER camelize — Bybit V5 + # wants PascalCase group keys (ContractTrade / Spot / Wallet / ...) + # which a naive snake→camel pass would corrupt. + params[:permissions] = permissions @session.sign_request(method: :post, path: '/v5/user/create-sub-api', body: params) end @@ -29,6 +37,7 @@ def create_sub_api_key(subuid:, read_only:, permissions:, **kwargs) # @option kwargs [Integer] :switch Quick login toggle # @option kwargs [Boolean] :is_uta Whether to create as UTA account # @option kwargs [String] :note Sub-account note + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/create-subuid def create_sub_member(username:, member_type:, **kwargs) params = kwargs.merge(username: username, member_type: member_type) @@ -41,6 +50,7 @@ def create_sub_member(username:, member_type:, **kwargs) # POST /v5/user/delete-api # # @option kwargs [String] :apikey API key to delete + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/rm-master-apikey def delete_api_key(**kwargs) params = kwargs.dup @@ -54,6 +64,7 @@ def delete_api_key(**kwargs) # # @param subuid [Integer] Sub UID # @option kwargs [String] :apikey Sub-account API key to delete + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/rm-sub-apikey def delete_sub_api_key(subuid:, **kwargs) params = kwargs.merge(subuid: subuid) @@ -66,6 +77,7 @@ def delete_sub_api_key(subuid:, **kwargs) # POST /v5/user/del-submember # # @param subuid [Integer] Sub UID to delete + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def delete_sub_member(subuid:, **kwargs) params = kwargs.merge(subuid: subuid) params = Bybit::Utils::WireKeys.camelize(params) @@ -78,6 +90,7 @@ def delete_sub_member(subuid:, **kwargs) # # @param subuid [Integer] Sub UID # @param frozen [Integer] Freeze flag (0: unfreeze, 1: freeze) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/froze-subuid def frozen_sub_member(subuid:, frozen:, **kwargs) params = kwargs.merge(subuid: subuid, frozen: frozen) @@ -92,6 +105,7 @@ def frozen_sub_member(subuid:, frozen:, **kwargs) # @param uid [String] Affiliate user UID # @option kwargs [String] :coin Coin filter # @option kwargs [String] :business Business type filter + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_affiliate_custom_open_info(uid:, **kwargs) params = kwargs.merge(uid: uid) params = Bybit::Utils::WireKeys.camelize(params) @@ -103,6 +117,7 @@ def get_affiliate_custom_open_info(uid:, **kwargs) # GET /v5/user/get-member-type # # @option kwargs [String] :member_ids Comma-separated member IDs + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def get_member_account_type(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -116,6 +131,7 @@ def get_member_account_type(**kwargs) # @param subuid [Integer] Sub UID # @option kwargs [Integer] :limit Page limit # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/list-sub-apikeys def list_sub_api_keys(subuid:, **kwargs) params = kwargs.merge(subuid: subuid) @@ -128,7 +144,7 @@ def list_sub_api_keys(subuid:, **kwargs) # GET /v5/user/query-api # # @see https://bybit-exchange.github.io/docs/v5/user/apikey-info - def query_api_key + def get_api_key_info @session.sign_request(method: :get, path: '/v5/user/query-api') end @@ -138,7 +154,8 @@ def query_api_key # # @option kwargs [Integer] :next_cursor Pagination cursor # @option kwargs [Integer] :page_size Page size - def query_escrow_sub_members(**kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def list_escrow_sub_members(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/escrow_sub_members', params: params) @@ -151,7 +168,8 @@ def query_escrow_sub_members(**kwargs) # @option kwargs [String] :cursor Pagination cursor # @option kwargs [Integer] :size Page size # @option kwargs [String] :status Referral status filter - def query_referrals(**kwargs) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def list_referrals(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/user/invitation/referrals', params: params) @@ -162,17 +180,18 @@ def query_referrals(**kwargs) # GET /v5/user/query-sub-members # # @see https://bybit-exchange.github.io/docs/v5/user/subuid-list - def query_sub_members + def list_sub_member_uids @session.sign_request(method: :get, path: '/v5/user/query-sub-members') end # List Sub-accounts (paginated variant — /v5/user/submembers). - # Distinct from #query_sub_members (/v5/user/query-sub-members). + # Distinct from #list_sub_member_uids (/v5/user/query-sub-members). # # GET /v5/user/submembers # # @option kwargs [Integer] :page_size Page size # @option kwargs [Integer] :next_cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def list_sub_members(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) @@ -185,6 +204,7 @@ def list_sub_members(**kwargs) # # @param category [Integer] Agreement category # @param agree [Boolean] Whether to agree + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def sign_agreement(category:, agree:, **kwargs) params = kwargs.merge(category: category, agree: agree) params = Bybit::Utils::WireKeys.camelize(params) @@ -198,6 +218,7 @@ def sign_agreement(category:, agree:, **kwargs) # @option kwargs [Integer] :read_only Read-only flag # @option kwargs [String] :ips Bound IP list # @option kwargs [Hash] :permissions Permissions object + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/modify-master-apikey def update_api_key(**kwargs) params = kwargs.dup @@ -213,12 +234,19 @@ def update_api_key(**kwargs) # @param read_only [Integer] Read-only flag # @option kwargs [String] :apikey Sub-account API key # @option kwargs [String] :ips Bound IP list - # @option kwargs [Hash] :permissions Permissions object + # @option kwargs [Hash] :permissions Permissions object. Bybit V5 expects + # PascalCase group keys (`ContractTrade` / `Spot` / `Wallet` / ...). This + # hash is passed through VERBATIM — no snake→camel conversion. # @option kwargs [String] :note Note + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). # @see https://bybit-exchange.github.io/docs/v5/user/modify-sub-apikey def update_sub_api_key(subuid:, read_only:, **kwargs) + permissions = kwargs.delete(:permissions) params = kwargs.merge(subuid: subuid, read_only: read_only) params = Bybit::Utils::WireKeys.camelize(params) + # See `create_sub_api_key` — permissions is re-attached AFTER camelize + # so PascalCase group keys survive the wire trip intact. + params[:permissions] = permissions unless permissions.nil? @session.sign_request(method: :post, path: '/v5/user/update-sub-api', body: params) end end diff --git a/spec/services/account_service_spec.rb b/spec/services/account_service_spec.rb new file mode 100644 index 0000000..d8e70a1 --- /dev/null +++ b/spec/services/account_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::AccountService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::AccountService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :batch_set_collateral, path: '/v5/account/set-collateral-switch-batch', method: :post, signed: true }, + { name: :get_borrow_history, path: '/v5/account/borrow-history', method: :get, signed: true }, + { name: :get_collateral_info, path: '/v5/account/collateral-info', method: :get, signed: true }, + { name: :get_dcp_info, path: '/v5/account/query-dcp-info', method: :get, signed: true }, + { name: :get_fee_rate, path: '/v5/account/fee-rate', method: :get, signed: true }, + { name: :get_info, path: '/v5/account/info', method: :get, signed: true }, + { name: :get_instruments, path: '/v5/account/instruments-info', method: :get, signed: true }, + { name: :get_mmp_state, path: '/v5/account/mmp-state', method: :get, signed: true }, + { name: :get_smp_group, path: '/v5/account/smp-group', method: :get, signed: true }, + { name: :get_transaction_log, path: '/v5/account/transaction-log', method: :get, signed: true }, + { name: :get_transferable_amount, path: '/v5/account/withdrawal', method: :get, signed: true }, + { name: :get_user_settings, path: '/v5/account/user-setting-config', method: :get, signed: true }, + { name: :manual_borrow, path: '/v5/account/borrow', method: :post, signed: true }, + { name: :manual_repay, path: '/v5/account/repay', method: :post, signed: true }, + { name: :no_convert_repay, path: '/v5/account/no-convert-repay', method: :post, signed: true }, + { name: :one_click_repay, path: '/v5/account/quick-repayment', method: :post, signed: true }, + { name: :reset_mmp, path: '/v5/account/mmp-reset', method: :post, signed: true }, + { name: :set_collateral_coin, path: '/v5/account/set-collateral-switch', method: :post, signed: true }, + { name: :set_margin_mode, path: '/v5/account/set-margin-mode', method: :post, signed: true }, + { name: :set_mmp, path: '/v5/account/mmp-modify', method: :post, signed: true }, + { name: :set_price_limit, path: '/v5/account/set-limit-px-action', method: :post, signed: true }, + { name: :set_spot_hedging, path: '/v5/account/set-hedging-mode', method: :post, signed: true }, + { name: :upgrade_to_uta_pro, path: '/v5/account/upgrade-to-uta', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/affiliate_service_spec.rb b/spec/services/affiliate_service_spec.rb new file mode 100644 index 0000000..c270f31 --- /dev/null +++ b/spec/services/affiliate_service_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::AffiliateService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::AffiliateService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :get_sub_list, path: '/v5/affiliate/affiliate-sub-list', method: :get, signed: true }, + { name: :get_user_list, path: '/v5/affiliate/aff-user-list', method: :get, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/asset_service_spec.rb b/spec/services/asset_service_spec.rb new file mode 100644 index 0000000..0ea70b9 --- /dev/null +++ b/spec/services/asset_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::AssetService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::AssetService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :account_coin_balance_query, path: '/v5/asset/transfer/query-account-coin-balance', method: :get, signed: true }, + { name: :asset_info_query, path: '/v5/asset/transfer/query-asset-info', method: :get, signed: true }, + { name: :cancel_withdraw, path: '/v5/asset/withdraw/cancel', method: :post, signed: true }, + { name: :coin_convert_limit_query, path: '/v5/asset/exchange/query-convert-limit', method: :get, signed: true }, + { name: :coin_list_query, path: '/v5/asset/exchange/query-coin-list', method: :get, signed: true }, + { name: :convert_execute, path: '/v5/asset/exchange/convert-execute', method: :post, signed: true }, + { name: :convert_history_query, path: '/v5/asset/exchange/query-convert-history', method: :get, signed: true }, + { name: :get_asset_overview, path: '/v5/asset/asset-overview', method: :get, signed: true }, + { name: :get_coin_balance, path: '/v5/asset/transfer/query-account-coins-balance', method: :get, signed: true }, + { name: :get_coin_greeks, path: '/v5/asset/coin-greeks', method: :get, signed: true }, + { name: :get_coin_info, path: '/v5/asset/coin/query-info', method: :get, signed: true }, + { name: :get_convert_result, path: '/v5/asset/exchange/convert-result-query', method: :get, signed: true }, + { name: :get_delivery_record, path: '/v5/asset/delivery-record', method: :get, signed: true }, + { name: :get_deposit_address, path: '/v5/asset/deposit/query-address', method: :get, signed: true }, + { name: :get_portfolio_margin, path: '/v5/asset/portfolio-margin', method: :get, signed: true }, + { name: :get_settlement_record, path: '/v5/asset/settlement-record', method: :get, signed: true }, + { name: :get_sub_member_deposit_address, path: '/v5/asset/deposit/query-sub-member-address', method: :get, signed: true }, + { name: :get_total_members_assets, path: '/v5/asset/total-members-assets', method: :get, signed: true }, + { name: :get_vasp_list, path: '/v5/asset/withdraw/vasp/list', method: :get, signed: true }, + { name: :get_withdrawable_amount_by_coin, path: '/v5/asset/withdraw/withdrawable-amount', method: :get, signed: true }, + { name: :inter_transfer, path: '/v5/asset/transfer/inter-transfer', method: :post, signed: true }, + { name: :inter_transfer_list_query, path: '/v5/asset/transfer/query-inter-transfer-list', method: :get, signed: true }, + { name: :list_convert_orders, path: '/v5/asset/exchange/query-order-list', method: :get, signed: true }, + { name: :list_convert_orders_by_page, path: '/v5/asset/exchange/order-record', method: :get, signed: true }, + { name: :list_deposit_records, path: '/v5/asset/deposit/query-record', method: :get, signed: true }, + { name: :list_funding_history, path: '/v5/asset/fundinghistory', method: :get, signed: true }, + { name: :list_internal_deposit_records, path: '/v5/asset/deposit/query-internal-record', method: :get, signed: true }, + { name: :list_small_balance_coins, path: '/v5/asset/covert/small-balance-list', method: :get, signed: true }, + { name: :list_small_balance_history, path: '/v5/asset/covert/small-balance-history', method: :get, signed: true }, + { name: :list_sub_member_deposit_records, path: '/v5/asset/deposit/query-sub-member-record', method: :get, signed: true }, + { name: :list_withdraw_addresses, path: '/v5/asset/withdraw/query-address', method: :get, signed: true }, + { name: :list_withdraw_records, path: '/v5/asset/withdraw/query-record', method: :get, signed: true }, + { name: :quote_apply, path: '/v5/asset/exchange/quote-apply', method: :post, signed: true }, + { name: :send_withdraw, path: '/v5/asset/withdraw/create', method: :post, signed: true }, + { name: :set_default_deposit_to_account, path: '/v5/asset/deposit/deposit-to-account', method: :post, signed: true }, + { name: :small_asset_convert, path: '/v5/asset/covert/small-balance-execute', method: :post, signed: true }, + { name: :small_asset_quote, path: '/v5/asset/covert/get-quote', method: :post, signed: true }, + { name: :sub_member_list_query, path: '/v5/asset/transfer/query-sub-member-list', method: :get, signed: true }, + { name: :transfer_coin_list_query, path: '/v5/asset/transfer/query-transfer-coin-list', method: :get, signed: true }, + { name: :transfer_sub_member_save, path: '/v5/asset/transfer/save-transfer-sub-member', method: :post, signed: true }, + { name: :universal_transfer, path: '/v5/asset/transfer/universal-transfer', method: :post, signed: true }, + { name: :universal_transfer_list_query, path: '/v5/asset/transfer/query-universal-transfer-list', method: :get, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/bot_service_spec.rb b/spec/services/bot_service_spec.rb new file mode 100644 index 0000000..b34f23f --- /dev/null +++ b/spec/services/bot_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::BotService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::BotService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :close_combo_bot, path: '/v5/fcombobot/close', method: :post, signed: true }, + { name: :close_dca_bot, path: '/v5/dca/close-bot', method: :post, signed: true }, + { name: :close_futures_grid_bot, path: '/v5/fgridbot/close', method: :post, signed: true }, + { name: :close_futures_martingale_bot, path: '/v5/fmartingalebot/close', method: :post, signed: true }, + { name: :close_grid_bot, path: '/v5/grid/close-grid', method: :post, signed: true }, + { name: :create_combo_bot, path: '/v5/fcombobot/create', method: :post, signed: true }, + { name: :create_dca_bot, path: '/v5/dca/create-bot', method: :post, signed: true }, + { name: :create_futures_grid_bot, path: '/v5/fgridbot/create', method: :post, signed: true }, + { name: :create_futures_martingale_bot, path: '/v5/fmartingalebot/create', method: :post, signed: true }, + { name: :create_grid_bot, path: '/v5/grid/create-grid', method: :post, signed: true }, + { name: :get_combo_detail, path: '/v5/fcombobot/detail', method: :post, signed: true }, + { name: :get_combo_limit, path: '/v5/fcombobot/getlimit', method: :post, signed: true }, + { name: :get_futures_grid_detail, path: '/v5/fgridbot/detail', method: :post, signed: true }, + { name: :get_futures_martingale_detail, path: '/v5/fmartingalebot/detail', method: :post, signed: true }, + { name: :get_futures_martingale_limit, path: '/v5/fmartingalebot/getlimit', method: :post, signed: true }, + { name: :get_grid_detail, path: '/v5/grid/query-grid-detail', method: :post, signed: true }, + { name: :validate_futures_grid_input, path: '/v5/fgridbot/validate', method: :post, signed: true }, + { name: :validate_grid_input, path: '/v5/grid/validate-input', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/broker_service_spec.rb b/spec/services/broker_service_spec.rb new file mode 100644 index 0000000..4310d46 --- /dev/null +++ b/spec/services/broker_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::BrokerService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::BrokerService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :distribute_award, path: '/v5/broker/award/distribute-award', method: :post, signed: true }, + { name: :get_award_info, path: '/v5/broker/award/info', method: :post, signed: true }, + { name: :get_broker_account_info, path: '/v5/broker/account-info', method: :get, signed: true }, + { name: :get_broker_rate_limit_cap, path: '/v5/broker/apilimit/query-cap', method: :get, signed: true }, + { name: :get_distribution_record, path: '/v5/broker/award/distribution-record', method: :post, signed: true }, + { name: :list_broker_earnings, path: '/v5/broker/earnings-info', method: :get, signed: true }, + { name: :list_broker_sub_uids, path: '/v5/broker/apilimit/query-all', method: :get, signed: true }, + { name: :set_api_limit, path: '/v5/broker/apilimit/set', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/crypto_loan_service_spec.rb b/spec/services/crypto_loan_service_spec.rb new file mode 100644 index 0000000..c0df672 --- /dev/null +++ b/spec/services/crypto_loan_service_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::CryptoLoanService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::CryptoLoanService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :adjust_ltv, path: '/v5/crypto-loan-common/adjust-ltv', method: :post, signed: true }, + { name: :borrow_fixed, path: '/v5/crypto-loan-fixed/borrow', method: :post, signed: true }, + { name: :borrow_flexible, path: '/v5/crypto-loan-flexible/borrow', method: :post, signed: true }, + { name: :cancel_fixed_borrow_order, path: '/v5/crypto-loan-fixed/borrow-order-cancel', method: :post, signed: true }, + { name: :cancel_fixed_supply_order, path: '/v5/crypto-loan-fixed/supply-order-cancel', method: :post, signed: true }, + { name: :get_adjustment_history, path: '/v5/crypto-loan-common/adjustment-history', method: :get, signed: true }, + { name: :get_collateral_data, path: '/v5/crypto-loan-common/collateral-data', method: :get, signed: false }, + { name: :get_fixed_borrow_contract_info, path: '/v5/crypto-loan-fixed/borrow-contract-info', method: :get, signed: true }, + { name: :get_fixed_borrow_order_info, path: '/v5/crypto-loan-fixed/borrow-order-info', method: :get, signed: true }, + { name: :get_fixed_borrow_order_quote, path: '/v5/crypto-loan-fixed/borrow-order-quote', method: :get, signed: false }, + { name: :get_fixed_renew_info, path: '/v5/crypto-loan-fixed/renew-info', method: :get, signed: true }, + { name: :get_fixed_supply_contract_info, path: '/v5/crypto-loan-fixed/supply-contract-info', method: :get, signed: true }, + { name: :get_fixed_supply_order_info, path: '/v5/crypto-loan-fixed/supply-order-info', method: :get, signed: true }, + { name: :get_fixed_supply_order_quote, path: '/v5/crypto-loan-fixed/supply-order-quote', method: :get, signed: false }, + { name: :get_flexible_borrow_history, path: '/v5/crypto-loan-flexible/borrow-history', method: :get, signed: true }, + { name: :get_flexible_ongoing_coin, path: '/v5/crypto-loan-flexible/ongoing-coin', method: :get, signed: true }, + { name: :get_flexible_repayment_history, path: '/v5/crypto-loan-flexible/repayment-history', method: :get, signed: true }, + { name: :get_loanable_data, path: '/v5/crypto-loan-common/loanable-data', method: :get, signed: true }, + { name: :get_max_collateral_amount, path: '/v5/crypto-loan-common/max-collateral-amount', method: :get, signed: true }, + { name: :get_max_loan, path: '/v5/crypto-loan-common/max-loan', method: :post, signed: true }, + { name: :get_position, path: '/v5/crypto-loan-common/position', method: :get, signed: true }, + { name: :renew_fixed, path: '/v5/crypto-loan-fixed/renew', method: :post, signed: true }, + { name: :repay_fixed_fully, path: '/v5/crypto-loan-fixed/fully-repay', method: :post, signed: true }, + { name: :repay_fixed_with_collateral, path: '/v5/crypto-loan-fixed/repay-collateral', method: :post, signed: true }, + { name: :repay_flexible, path: '/v5/crypto-loan-flexible/repay', method: :post, signed: true }, + { name: :repay_flexible_with_collateral, path: '/v5/crypto-loan-flexible/repay-collateral', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/earn_service_spec.rb b/spec/services/earn_service_spec.rb new file mode 100644 index 0000000..4a652bc --- /dev/null +++ b/spec/services/earn_service_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::EarnService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::EarnService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :add_liquidity, path: '/v5/earn/liquidity-mining/add-liquidity', method: :post, signed: true }, + { name: :add_margin, path: '/v5/earn/liquidity-mining/add-margin', method: :post, signed: true }, + { name: :claim_liquidity_interest, path: '/v5/earn/liquidity-mining/claim-interest', method: :post, signed: true }, + { name: :get_advance_earn_order, path: '/v5/earn/advance/order', method: :get, signed: true }, + { name: :get_advance_earn_position, path: '/v5/earn/advance/position', method: :get, signed: true }, + { name: :get_advance_earn_product, path: '/v5/earn/advance/product', method: :get, signed: false }, + { name: :get_advance_earn_product_extra_info, path: '/v5/earn/advance/product-extra-info', method: :get, signed: false }, + { name: :get_apr_history, path: '/v5/earn/apr-history', method: :get, signed: false }, + { name: :get_double_win_leverage, path: '/v5/earn/advance/double-win-leverage', method: :get, signed: true }, + { name: :get_fixed_term_order, path: '/v5/earn/fixed-term/order', method: :get, signed: true }, + { name: :get_fixed_term_position, path: '/v5/earn/fixed-term/position', method: :get, signed: true }, + { name: :get_fixed_term_product, path: '/v5/earn/fixed-term/product', method: :get, signed: false }, + { name: :get_hold_to_earn_product, path: '/v5/earn/hold-to-earn/product', method: :get, signed: false }, + { name: :get_hold_to_earn_yield_history, path: '/v5/earn/hold-to-earn/yield-history', method: :get, signed: true }, + { name: :get_hourly_yield_history, path: '/v5/earn/hourly-yield', method: :get, signed: true }, + { name: :get_liquidity_mining_liquidation_records, path: '/v5/earn/liquidity-mining/liquidation-records', method: :get, signed: true }, + { name: :get_liquidity_mining_orders, path: '/v5/earn/liquidity-mining/order', method: :get, signed: true }, + { name: :get_liquidity_mining_positions, path: '/v5/earn/liquidity-mining/position', method: :get, signed: true }, + { name: :get_liquidity_mining_products, path: '/v5/earn/liquidity-mining/product', method: :get, signed: false }, + { name: :get_liquidity_mining_yield_records, path: '/v5/earn/liquidity-mining/yield-records', method: :get, signed: true }, + { name: :get_order_history, path: '/v5/earn/order', method: :get, signed: true }, + { name: :get_position, path: '/v5/earn/position', method: :get, signed: true }, + { name: :get_product, path: '/v5/earn/product', method: :get, signed: false }, + { name: :get_rwa_nav_chart, path: '/v5/earn/rwa/nav-chart', method: :get, signed: false }, + { name: :get_rwa_order_list, path: '/v5/earn/rwa/order', method: :get, signed: true }, + { name: :get_rwa_position_list, path: '/v5/earn/rwa/position', method: :get, signed: true }, + { name: :get_rwa_product_list, path: '/v5/earn/rwa/product', method: :get, signed: false }, + { name: :get_smart_leverage_redeem_est_amount_list, path: '/v5/earn/advance/get-redeem-est-amount-list', method: :get, signed: true }, + { name: :get_token_daily_yield, path: '/v5/earn/token/yield', method: :get, signed: true }, + { name: :get_token_historical_apr, path: '/v5/earn/token/history-apr', method: :get, signed: false }, + { name: :get_token_hourly_yield, path: '/v5/earn/token/hourly-yield', method: :get, signed: true }, + { name: :get_token_order_list, path: '/v5/earn/token/order', method: :get, signed: true }, + { name: :get_token_position, path: '/v5/earn/token/position', method: :get, signed: true }, + { name: :get_token_product, path: '/v5/earn/token/product', method: :get, signed: false }, + { name: :get_yield_history, path: '/v5/earn/yield', method: :get, signed: true }, + { name: :list_coupons, path: '/v5/earn/coupons', method: :get, signed: true }, + { name: :modify_earn_position, path: '/v5/earn/position/modify', method: :post, signed: true }, + { name: :place_advance_earn_order, path: '/v5/earn/advance/place-order', method: :post, signed: true }, + { name: :place_fixed_term_order, path: '/v5/earn/fixed-term/place-order', method: :post, signed: true }, + { name: :place_order, path: '/v5/earn/place-order', method: :post, signed: true }, + { name: :place_rwa_order, path: '/v5/earn/rwa/place-order', method: :post, signed: true }, + { name: :place_token_order, path: '/v5/earn/token/place-order', method: :post, signed: true }, + { name: :pwm_asset_trend, path: '/v5/earn/pwm/investment-plan/asset-trend', method: :get, signed: true }, + { name: :pwm_claim, path: '/v5/earn/pwm/investment-plan/claim', method: :post, signed: true }, + { name: :pwm_create_custom_plan, path: '/v5/earn/pwm/customize-plan/create', method: :post, signed: true }, + { name: :pwm_fund_nav, path: '/v5/earn/pwm/investment-plan/fund-nav', method: :get, signed: true }, + { name: :pwm_fund_transfer, path: '/v5/earn/pwm/fund-transfer', method: :post, signed: true }, + { name: :pwm_get_new_plan_detail, path: '/v5/earn/pwm/investment-plan/new-plan', method: :get, signed: true }, + { name: :pwm_get_plan_detail, path: '/v5/earn/pwm/investment-plan/detail', method: :get, signed: true }, + { name: :pwm_inst_create_fund, path: '/v5/earn/pwm/asset-manager/create-fund', method: :post, signed: true }, + { name: :pwm_inst_create_investment_plan, path: '/v5/earn/pwm/asset-manager/create-investment-plan', method: :post, signed: true }, + { name: :pwm_inst_create_sub_account, path: '/v5/earn/pwm/asset-manager/create-sub-account', method: :post, signed: true }, + { name: :pwm_inst_get_investment_plans, path: '/v5/earn/pwm/asset-manager/get-investment-plan', method: :get, signed: true }, + { name: :pwm_inst_list_funds, path: '/v5/earn/pwm/asset-manager/all-funds', method: :get, signed: true }, + { name: :pwm_inst_list_orders, path: '/v5/earn/pwm/asset-manager/all-order', method: :get, signed: true }, + { name: :pwm_inst_manage_investment_plan, path: '/v5/earn/pwm/asset-manager/manage-investment-plan', method: :post, signed: true }, + { name: :pwm_inst_manage_order, path: '/v5/earn/pwm/asset-manager/manage-order', method: :post, signed: true }, + { name: :pwm_inst_settle_profit, path: '/v5/earn/pwm/asset-manager/settle-profit', method: :post, signed: true }, + { name: :pwm_invest_more, path: '/v5/earn/pwm/investment-plan/invest-more', method: :post, signed: true }, + { name: :pwm_list_investment_plans, path: '/v5/earn/pwm/investment-plan/all', method: :get, signed: true }, + { name: :pwm_list_order, path: '/v5/earn/pwm/investment-plan/order', method: :get, signed: true }, + { name: :pwm_list_product_cards, path: '/v5/earn/pwm/customize-plan/product', method: :get, signed: false }, + { name: :pwm_query_fund_transfer_result, path: '/v5/earn/pwm/query-fund-transfer-result', method: :get, signed: true }, + { name: :pwm_redeem, path: '/v5/earn/pwm/investment-plan/redeem', method: :post, signed: true }, + { name: :pwm_subscribe, path: '/v5/earn/pwm/investment-plan/subscribe', method: :post, signed: true }, + { name: :redeem_fixed_term, path: '/v5/earn/fixed-term/redeem', method: :post, signed: true }, + { name: :reinvest_liquidity, path: '/v5/earn/liquidity-mining/reinvest', method: :post, signed: true }, + { name: :remove_liquidity, path: '/v5/earn/liquidity-mining/remove-liquidity', method: :post, signed: true }, + { name: :set_fixed_term_auto_invest, path: '/v5/earn/fixed-term/position/auto-invest', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/market_service_spec.rb b/spec/services/market_service_spec.rb new file mode 100644 index 0000000..02c0824 --- /dev/null +++ b/spec/services/market_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::MarketService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::MarketService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :get_adl_alert, path: '/v5/market/adlalert', method: :get, signed: false }, + { name: :get_delivery_price, path: '/v5/market/delivery-price', method: :get, signed: false }, + { name: :get_fee_group_info, path: '/v5/market/fee-group-info', method: :get, signed: false }, + { name: :get_funding_rate_history, path: '/v5/market/funding/history', method: :get, signed: false }, + { name: :get_historical_volatility, path: '/v5/market/historical-volatility', method: :get, signed: false }, + { name: :get_index_price_components, path: '/v5/market/index-price-components', method: :get, signed: false }, + { name: :get_index_price_kline, path: '/v5/market/index-price-kline', method: :get, signed: false }, + { name: :get_instruments_info, path: '/v5/market/instruments-info', method: :get, signed: false }, + { name: :get_insurance_pool, path: '/v5/market/insurance', method: :get, signed: false }, + { name: :get_kline, path: '/v5/market/kline', method: :get, signed: false }, + { name: :get_long_short_ratio, path: '/v5/market/account-ratio', method: :get, signed: false }, + { name: :get_mark_price_kline, path: '/v5/market/mark-price-kline', method: :get, signed: false }, + { name: :get_new_delivery_price, path: '/v5/market/new-delivery-price', method: :get, signed: false }, + { name: :get_open_interest, path: '/v5/market/open-interest', method: :get, signed: false }, + { name: :get_order_price_limit, path: '/v5/market/price-limit', method: :get, signed: false }, + { name: :get_orderbook, path: '/v5/market/orderbook', method: :get, signed: false }, + { name: :get_premium_index_price_kline, path: '/v5/market/premium-index-price-kline', method: :get, signed: false }, + { name: :get_recent_public_trades, path: '/v5/market/recent-trade', method: :get, signed: false }, + { name: :get_risk_limit, path: '/v5/market/risk-limit', method: :get, signed: false }, + { name: :get_rpi_orderbook, path: '/v5/market/rpi_orderbook', method: :get, signed: false }, + { name: :get_server_time, path: '/v5/market/time', method: :get, signed: false }, + { name: :get_tickers, path: '/v5/market/tickers', method: :get, signed: false }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/p2p_service_spec.rb b/spec/services/p2p_service_spec.rb new file mode 100644 index 0000000..3017d96 --- /dev/null +++ b/spec/services/p2p_service_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::P2pService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::P2pService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :create_ad, path: '/v5/p2p/item/create', method: :post, signed: true }, + { name: :get_account_info, path: '/v5/p2p/user/personal/info', method: :post, signed: true }, + { name: :get_ads, path: '/v5/p2p/item/online', method: :post, signed: true }, + { name: :get_all_orders, path: '/v5/p2p/order/simplifylist', method: :post, signed: true }, + { name: :get_chat_messages, path: '/v5/p2p/order/message/listpage', method: :post, signed: true }, + { name: :get_counterparty_user_info, path: '/v5/p2p/user/order/personal/info', method: :post, signed: true }, + { name: :get_my_ad_details, path: '/v5/p2p/item/info', method: :post, signed: true }, + { name: :get_my_ads, path: '/v5/p2p/item/personal/list', method: :post, signed: true }, + { name: :get_order_detail, path: '/v5/p2p/order/info', method: :post, signed: true }, + { name: :get_pending_orders, path: '/v5/p2p/order/pending/simplifylist', method: :post, signed: true }, + { name: :get_user_payment, path: '/v5/p2p/user/payment/list', method: :post, signed: true }, + { name: :mark_order_as_paid, path: '/v5/p2p/order/pay', method: :post, signed: true }, + { name: :release_assets, path: '/v5/p2p/order/finish', method: :post, signed: true }, + { name: :remove_ad, path: '/v5/p2p/item/cancel', method: :post, signed: true }, + { name: :send_chat_message, path: '/v5/p2p/order/message/send', method: :post, signed: true }, + { name: :update_ad, path: '/v5/p2p/item/update', method: :post, signed: true }, + { name: :upload_chat_file, path: '/v5/p2p/oss/upload_file', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/position_service_spec.rb b/spec/services/position_service_spec.rb new file mode 100644 index 0000000..e1cc225 --- /dev/null +++ b/spec/services/position_service_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::PositionService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::PositionService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :add_reduce_margin, path: '/v5/position/add-margin', method: :post, signed: true }, + { name: :confirm_new_risk_limit, path: '/v5/position/confirm-pending-mmr', method: :post, signed: true }, + { name: :get_closed_pnl, path: '/v5/position/closed-pnl', method: :get, signed: true }, + { name: :get_closed_positions, path: '/v5/position/get-closed-positions', method: :get, signed: true }, + { name: :get_move_position_history, path: '/v5/position/move-history', method: :get, signed: true }, + { name: :get_positions, path: '/v5/position/list', method: :get, signed: true }, + { name: :move_position, path: '/v5/position/move-positions', method: :post, signed: true }, + { name: :set_auto_add_margin, path: '/v5/position/set-auto-add-margin', method: :post, signed: true }, + { name: :set_leverage, path: '/v5/position/set-leverage', method: :post, signed: true }, + { name: :set_trading_stop, path: '/v5/position/trading-stop', method: :post, signed: true }, + { name: :switch_position_mode, path: '/v5/position/switch-mode', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/rfq_service_spec.rb b/spec/services/rfq_service_spec.rb new file mode 100644 index 0000000..ea55f3a --- /dev/null +++ b/spec/services/rfq_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::RfqService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::RfqService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :accept_non_lp_quote, path: '/v5/rfq/accept-other-quote', method: :post, signed: true }, + { name: :cancel_all_quotes, path: '/v5/rfq/cancel-all-quotes', method: :post, signed: true }, + { name: :cancel_all_rfqs, path: '/v5/rfq/cancel-all-rfq', method: :post, signed: true }, + { name: :cancel_quote, path: '/v5/rfq/cancel-quote', method: :post, signed: true }, + { name: :cancel_rfq, path: '/v5/rfq/cancel-rfq', method: :post, signed: true }, + { name: :create_quote, path: '/v5/rfq/create-quote', method: :post, signed: true }, + { name: :create_rfq, path: '/v5/rfq/create-rfq', method: :post, signed: true }, + { name: :execute_quote, path: '/v5/rfq/execute-quote', method: :post, signed: true }, + { name: :get_config, path: '/v5/rfq/config', method: :get, signed: true }, + { name: :get_public_trades, path: '/v5/rfq/public-trades', method: :get, signed: true }, + { name: :get_quotes, path: '/v5/rfq/quote-list', method: :get, signed: true }, + { name: :get_quotes_realtime, path: '/v5/rfq/quote-realtime', method: :get, signed: true }, + { name: :get_rfqs, path: '/v5/rfq/rfq-list', method: :get, signed: true }, + { name: :get_rfqs_realtime, path: '/v5/rfq/rfq-realtime', method: :get, signed: true }, + { name: :get_trade_history, path: '/v5/rfq/trade-list', method: :get, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/spot_margin_service_spec.rb b/spec/services/spot_margin_service_spec.rb new file mode 100644 index 0000000..bab6d21 --- /dev/null +++ b/spec/services/spot_margin_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::SpotMarginService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::SpotMarginService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :get_historical_interest_rate, path: '/v5/spot-margin-trade/interest-rate-history', method: :get, signed: true }, + { name: :get_position_tiers, path: '/v5/spot-margin-trade/position-tiers', method: :get, signed: true }, + { name: :get_tiered_collateral_ratio, path: '/v5/spot-margin-trade/collateral', method: :get, signed: false }, + { name: :get_vip_margin_data, path: '/v5/spot-margin-trade/data', method: :get, signed: false }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/trade_service_spec.rb b/spec/services/trade_service_spec.rb new file mode 100644 index 0000000..dcb5438 --- /dev/null +++ b/spec/services/trade_service_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::TradeService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::TradeService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :amend_order, path: '/v5/order/amend', method: :post, signed: true }, + { name: :batch_amend_orders, path: '/v5/order/amend-batch', method: :post, signed: true }, + { name: :batch_cancel_orders, path: '/v5/order/cancel-batch', method: :post, signed: true }, + { name: :batch_create_orders, path: '/v5/order/create-batch', method: :post, signed: true }, + { name: :cancel_all_orders, path: '/v5/order/cancel-all', method: :post, signed: true }, + { name: :cancel_order, path: '/v5/order/cancel', method: :post, signed: true }, + { name: :create_order, path: '/v5/order/create', method: :post, signed: true }, + { name: :get_execution_history, path: '/v5/execution/list', method: :get, signed: true }, + { name: :get_open_orders, path: '/v5/order/realtime', method: :get, signed: true }, + { name: :get_order_history, path: '/v5/order/history', method: :get, signed: true }, + { name: :get_spot_borrow_quota, path: '/v5/order/spot-borrow-check', method: :get, signed: true }, + { name: :pre_check_order, path: '/v5/order/pre-check', method: :post, signed: true }, + { name: :set_dcp_time_window, path: '/v5/order/disconnected-cancel-all', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/user_service_spec.rb b/spec/services/user_service_spec.rb new file mode 100644 index 0000000..8a30ed8 --- /dev/null +++ b/spec/services/user_service_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::UserService — table-driven assertion that every +# public method dispatches to the correct (path, HTTP method, signed) tuple. +# Auto-generated by tools/gen_service_specs.py; hand-edits are OK but will be +# overwritten if the generator runs again. + +RSpec.describe Bybit::RestApi::UserService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + # Filled in by the loop below — one row per method the scanner picked up. + METHOD_TABLE = [ + { name: :create_sub_api_key, path: '/v5/user/create-sub-api', method: :post, signed: true }, + { name: :create_sub_member, path: '/v5/user/create-sub-member', method: :post, signed: true }, + { name: :delete_api_key, path: '/v5/user/delete-api', method: :post, signed: true }, + { name: :delete_sub_api_key, path: '/v5/user/delete-sub-api', method: :post, signed: true }, + { name: :delete_sub_member, path: '/v5/user/del-submember', method: :post, signed: true }, + { name: :frozen_sub_member, path: '/v5/user/frozen-sub-member', method: :post, signed: true }, + { name: :get_affiliate_custom_open_info, path: '/v5/user/aff-customer-info', method: :get, signed: true }, + { name: :get_api_key_info, path: '/v5/user/query-api', method: :get, signed: true }, + { name: :get_member_account_type, path: '/v5/user/get-member-type', method: :get, signed: true }, + { name: :list_escrow_sub_members, path: '/v5/user/escrow_sub_members', method: :get, signed: true }, + { name: :list_referrals, path: '/v5/user/invitation/referrals', method: :get, signed: true }, + { name: :list_sub_api_keys, path: '/v5/user/sub-apikeys', method: :get, signed: true }, + { name: :list_sub_member_uids, path: '/v5/user/query-sub-members', method: :get, signed: true }, + { name: :list_sub_members, path: '/v5/user/submembers', method: :get, signed: true }, + { name: :sign_agreement, path: '/v5/user/agreement', method: :post, signed: true }, + { name: :update_api_key, path: '/v5/user/update-api', method: :post, signed: true }, + { name: :update_sub_api_key, path: '/v5/user/update-sub-api', method: :post, signed: true }, + ].freeze + + METHOD_TABLE.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + # Supply blank strings for every required keyword arg — the value + # doesn't matter, we're asserting dispatch, not the wire payload. + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ""] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end From 256a0a3ec1372e04a26f4574a9acc2e0fe82603a Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 16:19:49 +0800 Subject: [PATCH 05/18] add ci yml --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314ed23..52306f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,9 @@ jobs: - name: Gemfile.lock resolves rubygems.org only (block private-registry drift) run: | if [ -f Gemfile.lock ]; then - bad=$(grep -E 'remote:' Gemfile.lock | grep -vE 'https?://rubygems\.org/?' || true) + # Only inspect HTTP(S) remotes; skip the PATH section's `remote: .` + # that bundler writes for the local gemspec source. + bad=$(grep -E '^\s*remote:\s*https?://' Gemfile.lock | grep -vE 'https?://rubygems\.org/?' || true) if [ -n "$bad" ]; then echo "::error::Gemfile.lock references a non-rubygems.org source. Re-generate with Gemfile pointing at 'https://rubygems.org' only." echo "$bad" | head -5 From 8662cb465390cb3694f6be16c78c3af01845a32c Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 16:59:43 +0800 Subject: [PATCH 06/18] optimization on ci --- .rubocop.yml | 11 ++++++++++- bybit-connector-ruby.gemspec | 14 ++++++++------ lib/bybit/client.rb | 21 ++++++--------------- lib/bybit/configuration.rb | 36 ++++++++++++++++++++++++------------ lib/bybit/error.rb | 6 ++++-- lib/bybit/session.rb | 19 +++++++++++-------- lib/bybit/utils/wire_keys.rb | 17 +++++++++++------ 7 files changed, 74 insertions(+), 50 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index c354d40..c87a07d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,14 +9,23 @@ AllCops: - 'bin/**/*' Style/Documentation: Enabled: false +# Codegen (lib/bybit/rest_api/*_service.rb) emits long YARD @option lines and +# wide method signatures verbatim from the OpenAPI spec — reformatting would +# just be reverted by the next codegen run. +Layout/LineLength: + Max: 400 Metrics/MethodLength: Max: 40 Metrics/ClassLength: Max: 2500 Metrics/AbcSize: Max: 40 -Metrics/ParameterLists: +Metrics/CyclomaticComplexity: + Max: 12 +Metrics/PerceivedComplexity: Max: 12 +Metrics/ParameterLists: + Max: 15 Metrics/BlockLength: Enabled: false Naming/AccessorMethodName: diff --git a/bybit-connector-ruby.gemspec b/bybit-connector-ruby.gemspec index fc80369..ed43a5e 100644 --- a/bybit-connector-ruby.gemspec +++ b/bybit-connector-ruby.gemspec @@ -6,7 +6,9 @@ Gem::Specification.new do |s| s.name = 'bybit-connector-ruby' s.version = Bybit::VERSION s.summary = 'Official Bybit V5 REST API connector for Ruby' - s.description = 'Ruby connector for the Bybit V5 REST API — keyword-arg method signatures, HMAC-SHA256 signing, typed error hierarchy, Faraday-based transport.' + s.description = 'Ruby connector for the Bybit V5 REST API — ' \ + 'keyword-arg method signatures, HMAC-SHA256 signing, ' \ + 'typed error hierarchy, Faraday-based transport.' s.authors = ['Bybit'] s.license = 'MIT' s.homepage = 'https://github.com/bybit-exchange/bybit.ruby.api' @@ -14,15 +16,15 @@ Gem::Specification.new do |s| s.files = Dir['lib/**/*.rb', 'LICENSE*', 'README.md', 'CHANGELOG.md', 'examples/**/*.rb'] s.require_paths = ['lib'] s.metadata = { - 'source_code_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api', - 'documentation_uri' => 'https://bybit-exchange.github.io/docs/v5/intro', - 'changelog_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api/blob/main/CHANGELOG.md', + 'source_code_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api', + 'documentation_uri' => 'https://bybit-exchange.github.io/docs/v5/intro', + 'changelog_uri' => 'https://github.com/bybit-exchange/bybit.ruby.api/blob/main/CHANGELOG.md', 'rubygems_mfa_required' => 'true' } s.add_dependency 'faraday', '~> 2.0' s.add_development_dependency 'rspec', '~> 3.12' s.add_development_dependency 'rubocop', '~> 1.60' - s.add_development_dependency 'yard', '~> 0.9' - s.add_development_dependency 'webmock', '~> 3.0' s.add_development_dependency 'simplecov', '~> 0.22' + s.add_development_dependency 'webmock', '~> 3.0' + s.add_development_dependency 'yard', '~> 0.9' end diff --git a/lib/bybit/client.rb b/lib/bybit/client.rb index f3c94c0..f34789c 100644 --- a/lib/bybit/client.rb +++ b/lib/bybit/client.rb @@ -21,9 +21,11 @@ module Bybit # Global configuration block — Bybit.configure { |c| c.api_key = ... } class << self attr_writer :configuration + def configuration @configuration ||= Configuration.new end + def configure yield configuration end @@ -31,20 +33,9 @@ def configure class Client # gen-sdk-ruby:client-readers:start - attr_reader :account - attr_reader :affiliate - attr_reader :asset - attr_reader :bot - attr_reader :broker - attr_reader :crypto_loan - attr_reader :earn - attr_reader :market - attr_reader :p2p - attr_reader :position - attr_reader :rfq - attr_reader :spot_margin - attr_reader :trade - attr_reader :user + attr_reader :account, :affiliate, :asset, :bot, :broker, :crypto_loan, :earn, + :market, :p2p, :position, :rfq, :spot_margin, :trade, :user + # gen-sdk-ruby:client-readers:end def initialize(config = nil, **overrides) @@ -75,6 +66,6 @@ def initialize(config = nil, **overrides) def inspect "#" end - alias_method :to_s, :inspect + alias to_s inspect end end diff --git a/lib/bybit/configuration.rb b/lib/bybit/configuration.rb index 3d2f527..edf2933 100644 --- a/lib/bybit/configuration.rb +++ b/lib/bybit/configuration.rb @@ -28,35 +28,47 @@ def resolved_base_url # Serialization-safe hash used by as_json / to_json / marshal_dump. def to_h_safe { - api_key: redact(@api_key), - api_secret: redact(@api_secret), - testnet: @testnet, - base_url: resolved_base_url, + api_key: redact(@api_key), + api_secret: redact(@api_secret), + testnet: @testnet, + base_url: resolved_base_url, recv_window: @recv_window, - timeout: @timeout, + timeout: @timeout } end - def to_json(*args); require 'json'; to_h_safe.to_json(*args); end - def as_json(*_); to_h_safe.transform_keys(&:to_s); end - def marshal_dump; to_h_safe; end + def to_json(*args) + require 'json' + to_h_safe.to_json(*args) + end + + def as_json(*_) + to_h_safe.transform_keys(&:to_s) + end + + def marshal_dump + to_h_safe + end + # Un-marshalling a redacted Configuration is intentionally lossy — callers # should never round-trip credentials through Marshal. - def marshal_load(h); h.each { |k, v| instance_variable_set("@#{k}", v) }; end + def marshal_load(hash) + hash.each { |k, v| instance_variable_set("@#{k}", v) } + end # Redact credentials from #inspect and #to_s so a stray puts/logger call # doesn't leak the secret into log aggregation. def inspect "#" end - alias_method :to_s, :inspect + alias to_s inspect private - def redact(v) + def redact(value) # Use .to_s.empty? so an Integer / other non-String credential (rare but # possible via misconfig) doesn't crash with NoMethodError. - v.nil? || v.to_s.empty? ? '(unset)' : '[REDACTED]' + value.nil? || value.to_s.empty? ? '(unset)' : '[REDACTED]' end end end diff --git a/lib/bybit/error.rb b/lib/bybit/error.rb index 285654f..468288d 100644 --- a/lib/bybit/error.rb +++ b/lib/bybit/error.rb @@ -45,6 +45,7 @@ class NetworkError < TransportError; end # so consumers can log CDN / maintenance-page HTML for post-mortem. class ParseError < TransportError attr_reader :body, :http_status + def initialize(message, body: nil, http_status: nil) @body = body @http_status = http_status @@ -52,14 +53,15 @@ def initialize(message, body: nil, http_status: nil) end end - AUTH_RET_CODES = [10002, 10003, 10004, 10005, 10007, 10009, 10010, 10029].freeze - RATE_LIMIT_RET_CODES = [10006, 10018].freeze + AUTH_RET_CODES = [10_002, 10_003, 10_004, 10_005, 10_007, 10_009, 10_010, 10_029].freeze + RATE_LIMIT_RET_CODES = [10_006, 10_018].freeze # Route a V5 response with retCode != 0 to the most specific error subclass. def self.api_error_from(response, http_status: nil) code = response['retCode'] return AuthError.new(response, http_status: http_status) if AUTH_RET_CODES.include?(code) return RateLimitError.new(response, http_status: http_status) if RATE_LIMIT_RET_CODES.include?(code) + ApiError.new(response, http_status: http_status) end end diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index aa75ea3..be203ec 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -25,7 +25,7 @@ def initialize(config) # Public unsigned endpoint — no X-BAPI-* headers attached. # session.public_request(path: '/v5/market/kline', params: {...}) - def public_request(method: :get, path:, params: nil, body: nil) + def public_request(path:, method: :get, params: nil, body: nil) dispatch(method: method, path: path, signed: false, params: params, body: body) end @@ -80,18 +80,18 @@ def dispatch(method:, path:, signed:, params:, body:) # Arrays become repeated keys (`symbol=BTCUSDT&symbol=ETHUSDT`) — this # matches Bybit V5's flat-list expectation. def encode_query(params) - params.sort_by { |k, _| k.to_s }.flat_map { |k, v| - Array(v).map { |single| "#{URI.encode_www_form_component(k.to_s)}=#{URI.encode_www_form_component(single.to_s)}" } - }.join('&') + params.sort_by { |k, _| k.to_s }.flat_map do |k, v| + key_enc = URI.encode_www_form_component(k.to_s) + Array(v).map { |single| "#{key_enc}=#{URI.encode_www_form_component(single.to_s)}" } + end.join('&') end def build_headers(signed:, method:, query_str:, body_str:) h = {} h['Content-Type'] = 'application/json' unless body_str.empty? return h unless signed - if @config.api_key.nil? || @config.api_secret.nil? - raise Bybit::ConfigurationError, 'signed endpoint requires api_key + api_secret' - end + raise Bybit::ConfigurationError, 'signed endpoint requires api_key + api_secret' if @config.api_key.nil? || @config.api_secret.nil? + ts = (Time.now.to_f * 1000).to_i.to_s # payload for GET/DELETE is query string; for POST/PUT/PATCH it's the # JSON body. Both branches use the SAME predicate as dispatch above. @@ -102,7 +102,7 @@ def build_headers(signed:, method:, query_str:, body_str:) h['X-BAPI-SIGN'] = Authentication.sign_v5( @config.api_secret, ts, @config.api_key, @config.recv_window.to_s, payload ) - h['X-BAPI-SIGN-TYPE'] = '2' + h['X-BAPI-SIGN-TYPE'] = '2' h end @@ -128,6 +128,7 @@ def parse_response(response) end end return body if body['retCode'].zero? + raise Bybit.api_error_from(body, http_status: status) end @@ -139,6 +140,7 @@ def safe_parse_json(str) def truncate_for_error(raw) return '(nil body)' if raw.nil? + s = raw.is_a?(String) ? raw : raw.inspect s.length > 2048 ? "#{s[0, 2048]}…(truncated)" : s end @@ -147,6 +149,7 @@ def truncate_for_error(raw) # Recursion happens on the wire-key side via WireKeys.camelize. def compact(hash) return nil if hash.nil? + hash.reject { |_, v| v.nil? } end diff --git a/lib/bybit/utils/wire_keys.rb b/lib/bybit/utils/wire_keys.rb index 8e943a7..3a45c3f 100644 --- a/lib/bybit/utils/wire_keys.rb +++ b/lib/bybit/utils/wire_keys.rb @@ -18,7 +18,7 @@ module WireKeys # `params[:end] = params.delete(:end_)` shims. RESERVED_ALIASES = { end_: :end, begin_: :begin, class_: :class, next_: :next, - return_: :return, do_: :do, if_: :if, else_: :else, + return_: :return, do_: :do, if_: :if, else_: :else }.freeze module_function @@ -29,30 +29,35 @@ module WireKeys # rewritten to their bare form. def camelize(hash) return hash if hash.nil? + hash.each_with_object({}) do |(k, v), out| out[to_camel(unalias(k))] = camelize_value(v) end end - def camelize_value(v) - case v - when Hash then camelize(v) - when Array then v.map { |el| el.is_a?(Hash) ? camelize(el) : el } - else v + def camelize_value(value) + case value + when Hash then camelize(value) + when Array then value.map { |el| el.is_a?(Hash) ? camelize(el) : el } + else value end end def unalias(key) return key unless key.is_a?(Symbol) || key.is_a?(String) + aliased = RESERVED_ALIASES[key.to_s.to_sym] return key unless aliased + key.is_a?(Symbol) ? aliased : aliased.to_s end def to_camel(key) return key unless key.is_a?(Symbol) || key.is_a?(String) + parts = key.to_s.split('_') return key if parts.size < 2 + camel = parts[0] + parts[1..].reject(&:empty?).map(&:capitalize).join key.is_a?(Symbol) ? camel.to_sym : camel end From 46b3f6393f28fa5f66d2413b334547eb703e389c Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 17:07:24 +0800 Subject: [PATCH 07/18] optimization on ci --- spec/spec_helper.rb | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 412fab6..4c09c5c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,9 +4,29 @@ SimpleCov.start do add_filter '/spec/' add_filter '/examples/' - # Signing + transport layer must stay tight — fail CI if either drops. - minimum_coverage_by_file 'lib/bybit/authentication.rb' => 100 - minimum_coverage_by_file 'lib/bybit/session.rb' => 90 +end + +# Signing + transport layer must stay tight — fail CI if either drops. +# SimpleCov's `minimum_coverage_by_file` only accepts a single global percentage +# (or a criterion hash), not per-file thresholds — enforce them by hand. +PER_FILE_COVERAGE_GATES = { + 'lib/bybit/authentication.rb' => 100.0, + 'lib/bybit/session.rb' => 90.0 +}.freeze + +SimpleCov.at_exit do + SimpleCov.result.format! + failed = PER_FILE_COVERAGE_GATES.filter_map do |path, threshold| + file = SimpleCov.result.files.find { |f| f.filename.end_with?(path) } + next "#{path}: not tracked by SimpleCov" if file.nil? + next if file.covered_percent >= threshold + + format('%s: %.2f%% < %.2f%%', path: path, pct: file.covered_percent, req: threshold) + end + unless failed.empty? + warn "SimpleCov per-file coverage gate failed:\n #{failed.join("\n ")}" + exit 1 + end end require 'bybit' From c63e8adf6f4541943d9104339149379df233e5ae Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 17:21:08 +0800 Subject: [PATCH 08/18] optimization on CI --- lib/bybit/session.rb | 15 ++++++++++++++- spec/services/account_service_spec.rb | 4 ++-- spec/services/affiliate_service_spec.rb | 4 ++-- spec/services/asset_service_spec.rb | 4 ++-- spec/services/bot_service_spec.rb | 4 ++-- spec/services/broker_service_spec.rb | 4 ++-- spec/services/crypto_loan_service_spec.rb | 4 ++-- spec/services/earn_service_spec.rb | 4 ++-- spec/services/market_service_spec.rb | 4 ++-- spec/services/p2p_service_spec.rb | 4 ++-- spec/services/position_service_spec.rb | 4 ++-- spec/services/rfq_service_spec.rb | 4 ++-- spec/services/spot_margin_service_spec.rb | 4 ++-- spec/services/trade_service_spec.rb | 4 ++-- spec/services/user_service_spec.rb | 4 ++-- spec/session_spec.rb | 9 +++++---- 16 files changed, 47 insertions(+), 33 deletions(-) diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index be203ec..80b94ac 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -2,6 +2,7 @@ require 'faraday' require 'json' +require 'timeout' require 'uri' module Bybit @@ -68,7 +69,11 @@ def dispatch(method:, path:, signed:, params:, body:) parse_response(resp) rescue Faraday::TimeoutError => e raise Bybit::TimeoutError, e.message - rescue Faraday::ConnectionFailed, Faraday::SSLError => e + rescue Faraday::ConnectionFailed => e + raise Bybit::TimeoutError, e.message if connect_timeout?(e) + + raise Bybit::NetworkError, e.message + rescue Faraday::SSLError => e raise Bybit::NetworkError, e.message rescue Faraday::Error => e # Catch-all for Faraday::ParsingError / ClientError / ServerError etc. @@ -76,6 +81,14 @@ def dispatch(method:, path:, signed:, params:, body:) raise Bybit::TransportError, e.message end + # Faraday 2's net_http adapter wraps Net::OpenTimeout / Timeout::Error / + # Errno::ETIMEDOUT in ConnectionFailed (only Net::ReadTimeout becomes + # Faraday::TimeoutError). Callers still want these surfaced as TimeoutError. + def connect_timeout?(err) + cause = err.respond_to?(:wrapped_exception) ? err.wrapped_exception : nil + cause.is_a?(Timeout::Error) || cause.is_a?(Errno::ETIMEDOUT) + end + # Deterministic `&`-joined encoding, keys sorted, values URI-escaped. # Arrays become repeated keys (`symbol=BTCUSDT&symbol=ETHUSDT`) — this # matches Bybit V5's flat-list expectation. diff --git a/spec/services/account_service_spec.rb b/spec/services/account_service_spec.rb index d8e70a1..cf71aa6 100644 --- a/spec/services/account_service_spec.rb +++ b/spec/services/account_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :batch_set_collateral, path: '/v5/account/set-collateral-switch-batch', method: :post, signed: true }, { name: :get_borrow_history, path: '/v5/account/borrow-history', method: :get, signed: true }, { name: :get_collateral_info, path: '/v5/account/collateral-info', method: :get, signed: true }, @@ -36,7 +36,7 @@ { name: :upgrade_to_uta_pro, path: '/v5/account/upgrade-to-uta', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/affiliate_service_spec.rb b/spec/services/affiliate_service_spec.rb index c270f31..09bb1d4 100644 --- a/spec/services/affiliate_service_spec.rb +++ b/spec/services/affiliate_service_spec.rb @@ -10,12 +10,12 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :get_sub_list, path: '/v5/affiliate/affiliate-sub-list', method: :get, signed: true }, { name: :get_user_list, path: '/v5/affiliate/aff-user-list', method: :get, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/asset_service_spec.rb b/spec/services/asset_service_spec.rb index 0ea70b9..7e5ccc4 100644 --- a/spec/services/asset_service_spec.rb +++ b/spec/services/asset_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :account_coin_balance_query, path: '/v5/asset/transfer/query-account-coin-balance', method: :get, signed: true }, { name: :asset_info_query, path: '/v5/asset/transfer/query-asset-info', method: :get, signed: true }, { name: :cancel_withdraw, path: '/v5/asset/withdraw/cancel', method: :post, signed: true }, @@ -55,7 +55,7 @@ { name: :universal_transfer_list_query, path: '/v5/asset/transfer/query-universal-transfer-list', method: :get, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/bot_service_spec.rb b/spec/services/bot_service_spec.rb index b34f23f..8a91920 100644 --- a/spec/services/bot_service_spec.rb +++ b/spec/services/bot_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :close_combo_bot, path: '/v5/fcombobot/close', method: :post, signed: true }, { name: :close_dca_bot, path: '/v5/dca/close-bot', method: :post, signed: true }, { name: :close_futures_grid_bot, path: '/v5/fgridbot/close', method: :post, signed: true }, @@ -31,7 +31,7 @@ { name: :validate_grid_input, path: '/v5/grid/validate-input', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/broker_service_spec.rb b/spec/services/broker_service_spec.rb index 4310d46..1087e7e 100644 --- a/spec/services/broker_service_spec.rb +++ b/spec/services/broker_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :distribute_award, path: '/v5/broker/award/distribute-award', method: :post, signed: true }, { name: :get_award_info, path: '/v5/broker/award/info', method: :post, signed: true }, { name: :get_broker_account_info, path: '/v5/broker/account-info', method: :get, signed: true }, @@ -21,7 +21,7 @@ { name: :set_api_limit, path: '/v5/broker/apilimit/set', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/crypto_loan_service_spec.rb b/spec/services/crypto_loan_service_spec.rb index c0df672..7a08f80 100644 --- a/spec/services/crypto_loan_service_spec.rb +++ b/spec/services/crypto_loan_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :adjust_ltv, path: '/v5/crypto-loan-common/adjust-ltv', method: :post, signed: true }, { name: :borrow_fixed, path: '/v5/crypto-loan-fixed/borrow', method: :post, signed: true }, { name: :borrow_flexible, path: '/v5/crypto-loan-flexible/borrow', method: :post, signed: true }, @@ -39,7 +39,7 @@ { name: :repay_flexible_with_collateral, path: '/v5/crypto-loan-flexible/repay-collateral', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/earn_service_spec.rb b/spec/services/earn_service_spec.rb index 4a652bc..b5a37ea 100644 --- a/spec/services/earn_service_spec.rb +++ b/spec/services/earn_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :add_liquidity, path: '/v5/earn/liquidity-mining/add-liquidity', method: :post, signed: true }, { name: :add_margin, path: '/v5/earn/liquidity-mining/add-margin', method: :post, signed: true }, { name: :claim_liquidity_interest, path: '/v5/earn/liquidity-mining/claim-interest', method: :post, signed: true }, @@ -82,7 +82,7 @@ { name: :set_fixed_term_auto_invest, path: '/v5/earn/fixed-term/position/auto-invest', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/market_service_spec.rb b/spec/services/market_service_spec.rb index 02c0824..20d8911 100644 --- a/spec/services/market_service_spec.rb +++ b/spec/services/market_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :get_adl_alert, path: '/v5/market/adlalert', method: :get, signed: false }, { name: :get_delivery_price, path: '/v5/market/delivery-price', method: :get, signed: false }, { name: :get_fee_group_info, path: '/v5/market/fee-group-info', method: :get, signed: false }, @@ -35,7 +35,7 @@ { name: :get_tickers, path: '/v5/market/tickers', method: :get, signed: false }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/p2p_service_spec.rb b/spec/services/p2p_service_spec.rb index 3017d96..1b79c1d 100644 --- a/spec/services/p2p_service_spec.rb +++ b/spec/services/p2p_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :create_ad, path: '/v5/p2p/item/create', method: :post, signed: true }, { name: :get_account_info, path: '/v5/p2p/user/personal/info', method: :post, signed: true }, { name: :get_ads, path: '/v5/p2p/item/online', method: :post, signed: true }, @@ -30,7 +30,7 @@ { name: :upload_chat_file, path: '/v5/p2p/oss/upload_file', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/position_service_spec.rb b/spec/services/position_service_spec.rb index e1cc225..93db33c 100644 --- a/spec/services/position_service_spec.rb +++ b/spec/services/position_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :add_reduce_margin, path: '/v5/position/add-margin', method: :post, signed: true }, { name: :confirm_new_risk_limit, path: '/v5/position/confirm-pending-mmr', method: :post, signed: true }, { name: :get_closed_pnl, path: '/v5/position/closed-pnl', method: :get, signed: true }, @@ -24,7 +24,7 @@ { name: :switch_position_mode, path: '/v5/position/switch-mode', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/rfq_service_spec.rb b/spec/services/rfq_service_spec.rb index ea55f3a..d082fdb 100644 --- a/spec/services/rfq_service_spec.rb +++ b/spec/services/rfq_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :accept_non_lp_quote, path: '/v5/rfq/accept-other-quote', method: :post, signed: true }, { name: :cancel_all_quotes, path: '/v5/rfq/cancel-all-quotes', method: :post, signed: true }, { name: :cancel_all_rfqs, path: '/v5/rfq/cancel-all-rfq', method: :post, signed: true }, @@ -28,7 +28,7 @@ { name: :get_trade_history, path: '/v5/rfq/trade-list', method: :get, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/spot_margin_service_spec.rb b/spec/services/spot_margin_service_spec.rb index bab6d21..5707703 100644 --- a/spec/services/spot_margin_service_spec.rb +++ b/spec/services/spot_margin_service_spec.rb @@ -10,14 +10,14 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :get_historical_interest_rate, path: '/v5/spot-margin-trade/interest-rate-history', method: :get, signed: true }, { name: :get_position_tiers, path: '/v5/spot-margin-trade/position-tiers', method: :get, signed: true }, { name: :get_tiered_collateral_ratio, path: '/v5/spot-margin-trade/collateral', method: :get, signed: false }, { name: :get_vip_margin_data, path: '/v5/spot-margin-trade/data', method: :get, signed: false }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/trade_service_spec.rb b/spec/services/trade_service_spec.rb index dcb5438..89e3621 100644 --- a/spec/services/trade_service_spec.rb +++ b/spec/services/trade_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :amend_order, path: '/v5/order/amend', method: :post, signed: true }, { name: :batch_amend_orders, path: '/v5/order/amend-batch', method: :post, signed: true }, { name: :batch_cancel_orders, path: '/v5/order/cancel-batch', method: :post, signed: true }, @@ -26,7 +26,7 @@ { name: :set_dcp_time_window, path: '/v5/order/disconnected-cancel-all', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/services/user_service_spec.rb b/spec/services/user_service_spec.rb index 8a30ed8..9cf0662 100644 --- a/spec/services/user_service_spec.rb +++ b/spec/services/user_service_spec.rb @@ -10,7 +10,7 @@ let(:svc) { described_class.new(session) } # Filled in by the loop below — one row per method the scanner picked up. - METHOD_TABLE = [ + method_table = [ { name: :create_sub_api_key, path: '/v5/user/create-sub-api', method: :post, signed: true }, { name: :create_sub_member, path: '/v5/user/create-sub-member', method: :post, signed: true }, { name: :delete_api_key, path: '/v5/user/delete-api', method: :post, signed: true }, @@ -30,7 +30,7 @@ { name: :update_sub_api_key, path: '/v5/user/update-sub-api', method: :post, signed: true }, ].freeze - METHOD_TABLE.each do |row| + method_table.each do |row| describe "##{row[:name]}" do it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do target = row[:signed] ? :sign_request : :public_request diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 5a9e731..467d140 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -15,15 +15,16 @@ describe 'signed dispatch' do it 'signs GET query-string exactly as it appears on the wire' do - stub = stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance}).to_return( + url = %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance} + stub_request(:get, url).to_return( status: 200, body: '{"retCode":0,"retMsg":"OK","result":{},"retExtInfo":{},"time":0}', - headers: { 'Content-Type' => 'application/json' }, + headers: { 'Content-Type' => 'application/json' } ) session.sign_request(method: :get, path: '/v5/account/wallet-balance', params: { accountType: 'UNIFIED' }) - expect(stub).to have_been_requested.with { |req| + expect(WebMock).to(have_requested(:get, url).with do |req| %w[X-BAPI-API-KEY X-BAPI-TIMESTAMP X-BAPI-RECV-WINDOW X-BAPI-SIGN X-BAPI-SIGN-TYPE] .all? { |h| req.headers.key?(h) } - } + end) end it 'signs POST body-string, does NOT send params on query string' do From 3e5dd07b69952837b12c0537a82ea5774b55b02b Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 17:27:04 +0800 Subject: [PATCH 09/18] optimization on CI --- spec/session_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 467d140..d474552 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -22,8 +22,10 @@ ) session.sign_request(method: :get, path: '/v5/account/wallet-balance', params: { accountType: 'UNIFIED' }) expect(WebMock).to(have_requested(:get, url).with do |req| + # WebMock title-cases header keys (`X-Bapi-Api-Key`), so match case-insensitively. + seen = req.headers.keys.map(&:downcase) %w[X-BAPI-API-KEY X-BAPI-TIMESTAMP X-BAPI-RECV-WINDOW X-BAPI-SIGN X-BAPI-SIGN-TYPE] - .all? { |h| req.headers.key?(h) } + .all? { |h| seen.include?(h.downcase) } end) end From fc9702d2cfd8a1df9d5cebbb2c1590a2a9b5fc73 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 17:30:47 +0800 Subject: [PATCH 10/18] optimization on CI --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52306f2..1076b33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,11 @@ jobs: # consumer sees them. Analogous to JS `dist smoke` step. - name: Load smoke (require + Client.new) run: | - bundle exec ruby -Ilib -rbybit -e ' + # `-rbundler/setup` MUST come before `-rbybit` — Ruby processes -r + # options in order, and `bybit` transitively requires `faraday` + # (a bundled gem) at load time. On Ruby 3.0, relying on + # `bundle exec`'s injected RUBYOPT alone left faraday unactivated. + bundle exec ruby -rbundler/setup -Ilib -rbybit -e ' raise "Bybit::VERSION missing" unless defined?(Bybit::VERSION) client = Bybit::Client.new(testnet: true) raise "Client init failed" unless client.is_a?(Bybit::Client) From 857799fa33d9ce7aaadfa317df7c04a097a9612e Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 18:24:55 +0800 Subject: [PATCH 11/18] optimization on testcase --- lib/bybit/rest_api/market_service.rb | 2 +- lib/bybit/rest_api/p2p_service.rb | 4 ++-- lib/bybit/session.rb | 26 ++++++++++++++++++++++++++ spec/session_spec.rb | 14 ++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/bybit/rest_api/market_service.rb b/lib/bybit/rest_api/market_service.rb index 252ac75..f130bc4 100644 --- a/lib/bybit/rest_api/market_service.rb +++ b/lib/bybit/rest_api/market_service.rb @@ -13,7 +13,7 @@ class MarketService < BaseService def get_adl_alert(**kwargs) params = kwargs.dup params = Bybit::Utils::WireKeys.camelize(params) - @session.public_request(path: '/v5/market/adlalert', params: params) + @session.public_request(path: '/v5/market/adlAlert', params: params) end # Get the delivery price for delivery contracts. diff --git a/lib/bybit/rest_api/p2p_service.rb b/lib/bybit/rest_api/p2p_service.rb index 7222ab9..95aa740 100644 --- a/lib/bybit/rest_api/p2p_service.rb +++ b/lib/bybit/rest_api/p2p_service.rb @@ -41,7 +41,7 @@ def get_ads(token_id:, currency_id:, side:, **kwargs) def get_all_orders(page:, size:, **kwargs) params = kwargs.merge(page: page, size: size) params = Bybit::Utils::WireKeys.camelize(params) - @session.sign_request(method: :post, path: '/v5/p2p/order/simplifylist', body: params) + @session.sign_request(method: :post, path: '/v5/p2p/order/simplifyList', body: params) end # Get the paginated chat message list for a P2P order. @@ -128,7 +128,7 @@ def get_order_detail(order_id:, **kwargs) def get_pending_orders(page:, size:, **kwargs) params = kwargs.merge(page: page, size: size) params = Bybit::Utils::WireKeys.camelize(params) - @session.sign_request(method: :post, path: '/v5/p2p/order/pending/simplifylist', body: params) + @session.sign_request(method: :post, path: '/v5/p2p/order/pending/simplifyList', body: params) end # Get the current user's P2P payment method list. diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index 80b94ac..5fd651e 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -123,6 +123,7 @@ def parse_response(response) status = response.status raw = response.body body = raw.is_a?(String) ? safe_parse_json(raw) : raw + body = normalize_legacy_body(body) if body.is_a?(Hash) # HTTP status wins over retCode when the body isn't a valid ApiResponse. # 5xx / non-auth 4xx get their own class so retries and pager logic can @@ -145,6 +146,31 @@ def parse_response(response) raise Bybit.api_error_from(body, http_status: status) end + # P2P endpoints (and a few legacy V3-derived paths) return the pre-V5 + # envelope: { ret_code, ret_msg, ext_code, ext_info, time_now, result }. + # Alias the legacy keys into the V5 shape so the rest of the parser and + # error mapper works uniformly. If the response is already V5, no-op. + LEGACY_KEY_MAP = { + 'ret_code' => 'retCode', + 'ret_msg' => 'retMsg', + 'ext_info' => 'retExtInfo', + 'time_now' => 'time' + }.freeze + + def normalize_legacy_body(body) + return body if body.key?('retCode') + return body unless body.key?('ret_code') + + LEGACY_KEY_MAP.each do |legacy, v5| + body[v5] = body.delete(legacy) if body.key?(legacy) + end + # `time_now` is a Bybit-legacy float-string; V5 exposes `time` as an + # integer millisecond epoch. Best-effort coerce so downstream code that + # compares against V5 `time` doesn't hit a type mismatch. + body['time'] = (body['time'].to_f * 1000).to_i if body['time'].is_a?(String) + body + end + def safe_parse_json(str) JSON.parse(str) rescue JSON::ParserError diff --git a/spec/session_spec.rb b/spec/session_spec.rb index d474552..0e0aa7f 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -91,6 +91,20 @@ stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_timeout expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::TimeoutError) end + + # P2P endpoints return the pre-V5 envelope {ret_code, ret_msg, ext_info, + # time_now}. Without aliasing, a valid P2P error response is misclassified + # as ParseError — swallows the retCode and prevents AuthError / RateLimit + # mapping. Regression guard for that. + it 'aliases legacy ret_code/ret_msg envelope (P2P) into V5 shape' do + stub_request(:post, 'https://api-testnet.bybit.com/v5/p2p/user/personal/info').to_return( + status: 200, + body: '{"ret_code":10003,"ret_msg":"API key is invalid.","result":{},"ext_code":"","ext_info":null,"time_now":"1700000000.123456"}', + headers: { 'Content-Type' => 'application/json' } + ) + expect { session.sign_request(method: :post, path: '/v5/p2p/user/personal/info', body: {}) } + .to raise_error(Bybit::AuthError, /10003/) + end end describe 'POST + params guard' do From ea5a7f0564b3ae985f7e457be3850e09cf4f185a Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 18:39:53 +0800 Subject: [PATCH 12/18] optimazition on testcase --- spec/services/market_service_spec.rb | 2 +- spec/services/p2p_service_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/services/market_service_spec.rb b/spec/services/market_service_spec.rb index 20d8911..bd3a26c 100644 --- a/spec/services/market_service_spec.rb +++ b/spec/services/market_service_spec.rb @@ -11,7 +11,7 @@ # Filled in by the loop below — one row per method the scanner picked up. method_table = [ - { name: :get_adl_alert, path: '/v5/market/adlalert', method: :get, signed: false }, + { name: :get_adl_alert, path: '/v5/market/adlAlert', method: :get, signed: false }, { name: :get_delivery_price, path: '/v5/market/delivery-price', method: :get, signed: false }, { name: :get_fee_group_info, path: '/v5/market/fee-group-info', method: :get, signed: false }, { name: :get_funding_rate_history, path: '/v5/market/funding/history', method: :get, signed: false }, diff --git a/spec/services/p2p_service_spec.rb b/spec/services/p2p_service_spec.rb index 1b79c1d..3a45549 100644 --- a/spec/services/p2p_service_spec.rb +++ b/spec/services/p2p_service_spec.rb @@ -14,13 +14,13 @@ { name: :create_ad, path: '/v5/p2p/item/create', method: :post, signed: true }, { name: :get_account_info, path: '/v5/p2p/user/personal/info', method: :post, signed: true }, { name: :get_ads, path: '/v5/p2p/item/online', method: :post, signed: true }, - { name: :get_all_orders, path: '/v5/p2p/order/simplifylist', method: :post, signed: true }, + { name: :get_all_orders, path: '/v5/p2p/order/simplifyList', method: :post, signed: true }, { name: :get_chat_messages, path: '/v5/p2p/order/message/listpage', method: :post, signed: true }, { name: :get_counterparty_user_info, path: '/v5/p2p/user/order/personal/info', method: :post, signed: true }, { name: :get_my_ad_details, path: '/v5/p2p/item/info', method: :post, signed: true }, { name: :get_my_ads, path: '/v5/p2p/item/personal/list', method: :post, signed: true }, { name: :get_order_detail, path: '/v5/p2p/order/info', method: :post, signed: true }, - { name: :get_pending_orders, path: '/v5/p2p/order/pending/simplifylist', method: :post, signed: true }, + { name: :get_pending_orders, path: '/v5/p2p/order/pending/simplifyList', method: :post, signed: true }, { name: :get_user_payment, path: '/v5/p2p/user/payment/list', method: :post, signed: true }, { name: :mark_order_as_paid, path: '/v5/p2p/order/pay', method: :post, signed: true }, { name: :release_assets, path: '/v5/p2p/order/finish', method: :post, signed: true }, From 25ac339fdd2d251b8af83fed6fbc295a8c5c1a46 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Mon, 20 Jul 2026 20:40:47 +0800 Subject: [PATCH 13/18] optimization --- CHANGELOG.md | 47 ++++++++++++++------------- lib/bybit/rest_api/account_service.rb | 14 ++++++++ spec/services/account_service_spec.rb | 1 + 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 513aded..f649d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `AccountService#batch_set_collateral(...)` - `AccountService#get_info(...)` +- `AccountService#get_wallet_balance(...)` - `AccountService#get_instruments(...)` - `AccountService#get_borrow_history(...)` - `AccountService#get_collateral_info(...)` @@ -35,14 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AffiliateService#get_user_list(...)` - `AssetService#get_coin_balance(...)` - `AssetService#get_coin_greeks(...)` -- `AssetService#query_funding_detail(...)` +- `AssetService#list_funding_history(...)` - `BrokerService#distribute_award(...)` - `BrokerService#get_award_info(...)` - `BrokerService#get_distribution_record(...)` -- `BrokerService#query_account_info(...)` -- `BrokerService#query_all_uid_details(...)` -- `BrokerService#query_cap(...)` -- `BrokerService#query_earning(...)` +- `BrokerService#get_broker_account_info(...)` +- `BrokerService#list_broker_sub_uids(...)` +- `BrokerService#get_broker_rate_limit_cap(...)` +- `BrokerService#list_broker_earnings(...)` - `BrokerService#set_api_limit(...)` - `CryptoLoanService#adjust_ltv(...)` - `CryptoLoanService#get_adjustment_history(...)` @@ -62,14 +63,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `CryptoLoanService#cancel_fixed_borrow_order(...)` - `CryptoLoanService#repay_fixed_fully(...)` - `CryptoLoanService#renew_fixed(...)` -- `CryptoLoanService#post_crypto_loan_fixed_repay_collateral(...)` +- `CryptoLoanService#repay_fixed_with_collateral(...)` - `CryptoLoanService#cancel_fixed_supply_order(...)` - `CryptoLoanService#get_flexible_borrow_history(...)` - `CryptoLoanService#get_flexible_ongoing_coin(...)` - `CryptoLoanService#get_flexible_repayment_history(...)` - `CryptoLoanService#borrow_flexible(...)` - `CryptoLoanService#repay_flexible(...)` -- `CryptoLoanService#post_crypto_loan_flexible_repay_collateral(...)` +- `CryptoLoanService#repay_flexible_with_collateral(...)` - `BotService#close_dca_bot(...)` - `BotService#create_dca_bot(...)` - `EarnService#add_liquidity(...)` @@ -83,7 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `EarnService#get_apr_history(...)` - `EarnService#get_hourly_yield_history(...)` - `EarnService#get_order_history(...)` -- `EarnService#get_position_get(...)` +- `EarnService#get_position(...)` - `EarnService#get_product(...)` - `EarnService#get_yield_history(...)` - `EarnService#get_fixed_term_order(...)` @@ -141,7 +142,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `EarnService#reinvest_liquidity(...)` - `EarnService#remove_liquidity(...)` - `EarnService#set_fixed_term_auto_invest(...)` -- `TradeService#get_history(...)` +- `TradeService#get_execution_history(...)` - `BotService#close_combo_bot(...)` - `BotService#create_combo_bot(...)` - `BotService#get_combo_detail(...)` @@ -156,7 +157,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `BotService#get_futures_martingale_limit(...)` - `BotService#close_grid_bot(...)` - `BotService#create_grid_bot(...)` -- `BotService#query_grid_detail(...)` +- `BotService#get_grid_detail(...)` - `BotService#validate_grid_input(...)` - `MarketService#get_adl_alert(...)` - `MarketService#get_delivery_price(...)` @@ -187,9 +188,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `TradeService#cancel_all_orders(...)` - `TradeService#cancel_order(...)` - `TradeService#create_order(...)` -- `TradeService#dcp_set_timewindow(...)` +- `TradeService#set_dcp_time_window(...)` - `TradeService#get_open_orders(...)` -- `TradeService#get_order_history_get(...)` +- `TradeService#get_order_history(...)` - `TradeService#get_spot_borrow_quota(...)` - `TradeService#pre_check_order(...)` - `P2pService#get_account_info(...)` @@ -203,7 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `P2pService#get_pending_orders(...)` - `P2pService#get_user_payment(...)` - `P2pService#mark_order_as_paid(...)` -- `P2pService#post_ad(...)` +- `P2pService#create_ad(...)` - `P2pService#release_assets(...)` - `P2pService#remove_ad(...)` - `P2pService#send_chat_message(...)` @@ -211,10 +212,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `P2pService#upload_chat_file(...)` - `PositionService#add_reduce_margin(...)` - `PositionService#confirm_new_risk_limit(...)` -- `PositionService#get_close_position(...)` +- `PositionService#get_closed_positions(...)` - `PositionService#get_closed_pnl(...)` - `PositionService#get_move_position_history(...)` -- `PositionService#get_info_get(...)` +- `PositionService#get_positions(...)` - `PositionService#move_position(...)` - `PositionService#set_auto_add_margin(...)` - `PositionService#set_leverage(...)` @@ -243,16 +244,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `UserService#create_sub_member(...)` - `UserService#delete_api_key(...)` - `UserService#delete_sub_api_key(...)` -- `UserService#delete_sub_member_v5(...)` +- `UserService#delete_sub_member(...)` - `UserService#frozen_sub_member(...)` -- `UserService#get_affiliate_custom_open_info_v5(...)` +- `UserService#get_affiliate_custom_open_info(...)` - `UserService#get_member_account_type(...)` -- `UserService#list_sub_api_keys_v5(...)` -- `UserService#query_api_key(...)` -- `UserService#query_escrow_sub_members_v5(...)` -- `UserService#query_referrals(...)` -- `UserService#query_sub_members(...)` -- `UserService#query_sub_members_v5(...)` +- `UserService#list_sub_api_keys(...)` +- `UserService#get_api_key_info(...)` +- `UserService#list_escrow_sub_members(...)` +- `UserService#list_referrals(...)` +- `UserService#list_sub_member_uids(...)` +- `UserService#list_sub_members(...)` - `UserService#sign_agreement(...)` - `UserService#update_api_key(...)` - `UserService#update_sub_api_key(...)` diff --git a/lib/bybit/rest_api/account_service.rb b/lib/bybit/rest_api/account_service.rb index c96452f..0aafc91 100644 --- a/lib/bybit/rest_api/account_service.rb +++ b/lib/bybit/rest_api/account_service.rb @@ -28,6 +28,20 @@ def get_info(**kwargs) @session.sign_request(method: :get, path: '/v5/account/info', params: params) end + # Get Wallet Balance + # + # GET /v5/account/wallet-balance + # + # @param account_type [String] Account type: UNIFIED / CONTRACT / SPOT / FUND / OPTION. + # @option kwargs [String] :coin Coin filter — comma-separated, e.g. "USDT,BTC". + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/account/wallet-balance + def get_wallet_balance(account_type:, **kwargs) + params = kwargs.merge(account_type: account_type) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/account/wallet-balance', params: params) + end + # Get Account Instruments # # GET /v5/account/instruments-info diff --git a/spec/services/account_service_spec.rb b/spec/services/account_service_spec.rb index cf71aa6..312d0ef 100644 --- a/spec/services/account_service_spec.rb +++ b/spec/services/account_service_spec.rb @@ -17,6 +17,7 @@ { name: :get_dcp_info, path: '/v5/account/query-dcp-info', method: :get, signed: true }, { name: :get_fee_rate, path: '/v5/account/fee-rate', method: :get, signed: true }, { name: :get_info, path: '/v5/account/info', method: :get, signed: true }, + { name: :get_wallet_balance, path: '/v5/account/wallet-balance', method: :get, signed: true }, { name: :get_instruments, path: '/v5/account/instruments-info', method: :get, signed: true }, { name: :get_mmp_state, path: '/v5/account/mmp-state', method: :get, signed: true }, { name: :get_smp_group, path: '/v5/account/smp-group', method: :get, signed: true }, From 953f1e54d55c44f4882de6b4c11cd78f05556968 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Tue, 21 Jul 2026 10:12:54 +0800 Subject: [PATCH 14/18] optimization --- CHANGELOG.md | 41 +++++++++++++++++++++++++++- lib/bybit/error.rb | 18 ++++++++---- lib/bybit/rest_api/broker_service.rb | 13 ++++++--- lib/bybit/session.rb | 16 +++++++++-- spec/session_spec.rb | 28 +++++++++++++++++++ 5 files changed, 103 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f649d04..97fc458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 0.1.0 — 2026-07-20 +## [0.1.0] - 2026-07-20 ### Added - `AccountService#batch_set_collateral(...)` @@ -34,9 +34,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `AccountService#upgrade_to_uta_pro(...)` - `AffiliateService#get_sub_list(...)` - `AffiliateService#get_user_list(...)` +- `AssetService#account_coin_balance_query(...)` +- `AssetService#asset_info_query(...)` +- `AssetService#cancel_withdraw(...)` +- `AssetService#coin_convert_limit_query(...)` +- `AssetService#coin_list_query(...)` +- `AssetService#convert_execute(...)` +- `AssetService#convert_history_query(...)` +- `AssetService#get_asset_overview(...)` - `AssetService#get_coin_balance(...)` - `AssetService#get_coin_greeks(...)` +- `AssetService#get_coin_info(...)` +- `AssetService#get_convert_result(...)` +- `AssetService#get_delivery_record(...)` +- `AssetService#get_deposit_address(...)` +- `AssetService#get_portfolio_margin(...)` +- `AssetService#get_settlement_record(...)` +- `AssetService#get_sub_member_deposit_address(...)` +- `AssetService#get_total_members_assets(...)` +- `AssetService#get_vasp_list(...)` +- `AssetService#get_withdrawable_amount_by_coin(...)` +- `AssetService#inter_transfer(...)` +- `AssetService#inter_transfer_list_query(...)` +- `AssetService#list_convert_orders(...)` +- `AssetService#list_convert_orders_by_page(...)` +- `AssetService#list_deposit_records(...)` - `AssetService#list_funding_history(...)` +- `AssetService#list_internal_deposit_records(...)` +- `AssetService#list_small_balance_coins(...)` +- `AssetService#list_small_balance_history(...)` +- `AssetService#list_sub_member_deposit_records(...)` +- `AssetService#list_withdraw_addresses(...)` +- `AssetService#list_withdraw_records(...)` +- `AssetService#quote_apply(...)` +- `AssetService#send_withdraw(...)` +- `AssetService#set_default_deposit_to_account(...)` +- `AssetService#small_asset_convert(...)` +- `AssetService#small_asset_quote(...)` +- `AssetService#sub_member_list_query(...)` +- `AssetService#transfer_coin_list_query(...)` +- `AssetService#transfer_sub_member_save(...)` +- `AssetService#universal_transfer(...)` +- `AssetService#universal_transfer_list_query(...)` - `BrokerService#distribute_award(...)` - `BrokerService#get_award_info(...)` - `BrokerService#get_distribution_record(...)` diff --git a/lib/bybit/error.rb b/lib/bybit/error.rb index 468288d..55bf16c 100644 --- a/lib/bybit/error.rb +++ b/lib/bybit/error.rb @@ -22,16 +22,24 @@ class ClientError < TransportError; end # Raised when the server returns HTTP 200 + retCode != 0 (the V5 norm), # or when a transport-level error is enriched with a Bybit body payload. + # Accepts either a V5 response Hash (extracts retCode/retMsg/result/time) + # or a plain String message (used for HTTP-status mapping when the CDN/WAF + # returns a non-JSON body — e.g. 401/403/429 → AuthError/RateLimitError + # without a decodable retCode). class ApiError < Error attr_reader :ret_code, :ret_msg, :result, :time, :http_status def initialize(response, http_status: nil) - @ret_code = response['retCode'] - @ret_msg = response['retMsg'] - @result = response['result'] - @time = response['time'] @http_status = http_status - super("[#{@ret_code}] #{@ret_msg}") + if response.is_a?(Hash) + @ret_code = response['retCode'] + @ret_msg = response['retMsg'] + @result = response['result'] + @time = response['time'] + super("[#{@ret_code}] #{@ret_msg}") + else + super(response.to_s) + end end end diff --git a/lib/bybit/rest_api/broker_service.rb b/lib/bybit/rest_api/broker_service.rb index 5025c2b..93cfa18 100644 --- a/lib/bybit/rest_api/broker_service.rb +++ b/lib/bybit/rest_api/broker_service.rb @@ -78,17 +78,22 @@ def get_broker_rate_limit_cap # # GET /v5/broker/earnings-info # + # Bybit's spec names the date-range params `begin` / `end`, which are + # Ruby keywords — callers use `:start_date` / `:end_date` and we rename + # them here before camelize. `:begin_` / `:end_` are also accepted (via + # WireKeys::RESERVED_ALIASES) for callers who prefer the underscore form. + # # @option kwargs [String] :biz_type Business type - # @option kwargs [String] :begin_ Begin date - # @option kwargs [String] :end_ End date + # @option kwargs [String] :start_date Begin date (wire: `begin`) + # @option kwargs [String] :end_date End date (wire: `end`) # @option kwargs [String] :uid Sub UID # @option kwargs [Integer] :limit Result limit # @option kwargs [String] :cursor Pagination cursor # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). def list_broker_earnings(**kwargs) params = kwargs.dup - params[:begin] = params.delete(:begin_) if params.key?(:begin_) - params[:end] = params.delete(:end_) if params.key?(:end_) + params[:begin_] = params.delete(:start_date) if params.key?(:start_date) + params[:end_] = params.delete(:end_date) if params.key?(:end_date) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :get, path: '/v5/broker/earnings-info', params: params) end diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index 5fd651e..9e26642 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -126,11 +126,21 @@ def parse_response(response) body = normalize_legacy_body(body) if body.is_a?(Hash) # HTTP status wins over retCode when the body isn't a valid ApiResponse. - # 5xx / non-auth 4xx get their own class so retries and pager logic can - # tell them apart from client / auth errors. + # 401/403/429 get their promised AuthError / RateLimitError classes even + # when a CDN/WAF returns HTML (no retCode); the README documents this + # contract. 5xx / other non-auth 4xx keep their generic Server/Client + # buckets so retry and pager logic can tell them apart. if !body.is_a?(Hash) || !body['retCode'].is_a?(Integer) preview = truncate_for_error(raw) - if status >= 500 + if [401, 403].include?(status) + raise Bybit::AuthError.new( + "Bybit auth error (status=#{status}): #{preview}", http_status: status + ) + elsif status == 429 + raise Bybit::RateLimitError.new( + "Bybit rate limit (status=#{status}): #{preview}", http_status: status + ) + elsif status >= 500 raise Bybit::ServerError, "Bybit server error (status=#{status}): #{preview}" elsif status >= 400 raise Bybit::ClientError, "Bybit client error (status=#{status}): #{preview}" diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 0e0aa7f..c4ebed9 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -87,6 +87,34 @@ expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::ClientError) end + # README promises 401/403 → AuthError, 429 → RateLimitError. These are + # transport-layer statuses returned by CDN/WAF with no Bybit body — the + # generic ClientError bucket would silently miss the rescue clauses + # documented in README.md#Error-Handling. + it 'raises AuthError on HTTP 401 with non-JSON body' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_return( + status: 401, body: 'Unauthorized', + ) + expect { session.public_request(path: '/v5/market/time') } + .to raise_error(Bybit::AuthError) { |e| expect(e.http_status).to eq(401) } + end + + it 'raises AuthError on HTTP 403 with non-JSON body' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_return( + status: 403, body: 'Forbidden', + ) + expect { session.public_request(path: '/v5/market/time') } + .to raise_error(Bybit::AuthError) { |e| expect(e.http_status).to eq(403) } + end + + it 'raises RateLimitError on HTTP 429 with non-JSON body' do + stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_return( + status: 429, body: 'Too Many Requests', + ) + expect { session.public_request(path: '/v5/market/time') } + .to raise_error(Bybit::RateLimitError) { |e| expect(e.http_status).to eq(429) } + end + it 'maps Faraday::TimeoutError to Bybit::TimeoutError' do stub_request(:get, %r{https://api-testnet\.bybit\.com/v5/market/time}).to_timeout expect { session.public_request(path: '/v5/market/time') }.to raise_error(Bybit::TimeoutError) From 33cd74d3756f2f00771d1a66d37ed94c27f99f99 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Tue, 21 Jul 2026 11:20:53 +0800 Subject: [PATCH 15/18] add websocket --- CHANGELOG.md | 34 ++ README.md | 60 +++- bybit-connector-ruby.gemspec | 6 + lib/bybit/client.rb | 9 +- .../rest_api/institutional_loan_service.rb | 106 +++++++ lib/bybit/rest_api/misc_service.rb | 36 +++ lib/bybit/rest_api/pre_upgrade_service.rb | 130 ++++++++ lib/bybit/websocket.rb | 9 + lib/bybit/websocket/client.rb | 294 ++++++++++++++++++ .../account_get_wallet_balance.yml | 22 ++ .../vcr_cassettes/market_get_server_time.yml | 22 ++ .../vcr_cassettes/trade_create_order.yml | 22 ++ spec/integration/rest_end_to_end_spec.rb | 92 ++++++ .../institutional_loan_service_spec.rb | 38 +++ spec/services/misc_service_spec.rb | 31 ++ spec/services/pre_upgrade_service_spec.rb | 35 +++ spec/spec_helper.rb | 17 + spec/support/vcr_setup.rb | 22 ++ spec/websocket/client_spec.rb | 220 +++++++++++++ 19 files changed, 1202 insertions(+), 3 deletions(-) create mode 100644 lib/bybit/rest_api/institutional_loan_service.rb create mode 100644 lib/bybit/rest_api/misc_service.rb create mode 100644 lib/bybit/rest_api/pre_upgrade_service.rb create mode 100644 lib/bybit/websocket.rb create mode 100644 lib/bybit/websocket/client.rb create mode 100644 spec/fixtures/vcr_cassettes/account_get_wallet_balance.yml create mode 100644 spec/fixtures/vcr_cassettes/market_get_server_time.yml create mode 100644 spec/fixtures/vcr_cassettes/trade_create_order.yml create mode 100644 spec/integration/rest_end_to_end_spec.rb create mode 100644 spec/services/institutional_loan_service_spec.rb create mode 100644 spec/services/misc_service_spec.rb create mode 100644 spec/services/pre_upgrade_service_spec.rb create mode 100644 spec/support/vcr_setup.rb create mode 100644 spec/websocket/client_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fc458..fcfabc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -296,3 +296,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `UserService#sign_agreement(...)` - `UserService#update_api_key(...)` - `UserService#update_sub_api_key(...)` +- `InstitutionalLoanService#get_product_info(...)` +- `InstitutionalLoanService#get_margin_coin_info(...)` +- `InstitutionalLoanService#get_loan_orders(...)` +- `InstitutionalLoanService#get_repayment_orders(...)` +- `InstitutionalLoanService#get_ltv(...)` +- `InstitutionalLoanService#bind_or_unbind_uid(...)` +- `InstitutionalLoanService#repay_loan(...)` +- `MiscService#get_announcements(...)` +- `MiscService#get_system_status(...)` +- `PreUpgradeService#get_order_history(...)` +- `PreUpgradeService#get_trade_history(...)` +- `PreUpgradeService#get_closed_pnl(...)` +- `PreUpgradeService#get_transaction_log(...)` +- `PreUpgradeService#get_option_delivery_record(...)` +- `PreUpgradeService#get_usdc_session_settlement(...)` +- `Bybit::WebSocket::Client` — public (spot/linear/inverse/option), private, + and trade streams; HMAC-SHA256 auth, ping/pong, deferred subscribe (topics + buffered until socket opens; private streams wait for auth OK), idempotent + `#connect` (second call replays buffered subscriptions), testnet host + support. Load via `require 'bybit/websocket'` (opt-in dep on + `websocket-client-simple`). +- VCR-driven end-to-end specs asserting the exact wire bytes and X-BAPI-SIGN + header for signed GET (query) and signed POST (body) routing. +- SimpleCov floor coverage gate for `lib/bybit/rest_api/*_service.rb` — new + services without matching specs now drop CI. +- README: WebSocket usage section. + +### Fixed +- `Session#parse_response` now maps HTTP 401/403 → `Bybit::AuthError` and 429 + → `Bybit::RateLimitError` even when the response body is non-JSON + (CDN/WAF path), matching the contract in README.md#Error-Handling. +- `BrokerService#list_broker_earnings` — caller-facing kwargs renamed from + `:begin_` / `:end_` to `:start_date` / `:end_date`; internal rewrite now + goes through `WireKeys::RESERVED_ALIASES` instead of hand-rolled shim. diff --git a/README.md b/README.md index 63761a4..d60be20 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,14 @@ Each API group is a property on `Bybit::Client`: - `client.affiliate` — sub-affiliate lists - `client.broker` — broker earnings, distributions - `client.crypto_loan` — flexible / fixed crypto loans +- `client.institutional_loan` — /v5/ins-loan/* (OTC institutional loans) - `client.rfq` — request-for-quote (block trades) - `client.spot_margin` — UTA spot margin - `client.earn` — earn, liquidity mining, RWA, PWM, hold-to-earn - `client.p2p` — P2P advertise / order / chat - `client.bot` — DCA / grid / futures-combo / futures-grid / martingale +- `client.pre_upgrade` — /v5/pre-upgrade/* historical queries for Classic → UTA upgrades +- `client.misc` — /v5/announcements/index, /v5/system/status ## Error Handling @@ -153,12 +156,65 @@ response['time'] # => 1234567890000 ## Testnet -Toggle `testnet: true` for [https://testnet.bybit.com](https://testnet.bybit.com): +Flip `testnet: true` for [https://testnet.bybit.com](https://testnet.bybit.com): ```ruby -client = Bybit::Client.new(api_key: ENV['BYBIT_TESTNET_KEY'], api_secret: ENV['BYBIT_TESTNET_SECRET'], testnet: true) +client = Bybit::Client.new( + api_key: ENV['BYBIT_TESTNET_KEY'], + api_secret: ENV['BYBIT_TESTNET_SECRET'], + testnet: true, +) ``` +REST base URLs (exported constants): + +- `Bybit::BASE_URL_MAINNET` — `https://api.bybit.com` +- `Bybit::BASE_URL_TESTNET` — `https://api-testnet.bybit.com` + +WebSocket hosts follow the same split: + +- Mainnet — `wss://stream.bybit.com/v5/{public/|private|trade}` +- Testnet — `wss://stream-testnet.bybit.com/v5/{public/|private|trade}` + +## WebSockets + +WebSocket support ships behind an opt-in require so REST-only consumers don't +load the underlying `websocket-client-simple` dependency at boot: + +```ruby +require 'bybit/websocket' + +# Public stream — no auth needed. +public_ws = Bybit::WebSocket::Client.new( + channel: :linear, # :spot / :linear / :inverse / :option + testnet: false, + on_message: ->(msg) { puts msg.inspect }, +).connect + +public_ws.subscribe('tickers.BTCUSDT', 'orderbook.1.BTCUSDT') + +# Private stream — HMAC-SHA256 auth over "GET/realtime" + expiry. +private_ws = Bybit::WebSocket::Client.new( + channel: :private, + api_key: ENV['BYBIT_KEY'], + api_secret: ENV['BYBIT_SECRET'], + on_message: ->(msg) { handle_private_event(msg) }, +).connect + +private_ws.subscribe('position', 'order', 'wallet') + +# Later: +private_ws.unsubscribe('wallet') +private_ws.disconnect +``` + +Supported channels: `:spot`, `:linear`, `:inverse`, `:option`, `:private`, +`:trade`. `testnet: true` swaps in the `stream-testnet.bybit.com` host. +Pings ship every 20 seconds — Bybit closes idle sockets after ~20s of +silence. There is no automatic reconnect on network drop: callers listening +on `on_close` can invoke `#connect` again to reopen the socket and replay +the buffered `#subscriptions`. + ## Development ``` diff --git a/bybit-connector-ruby.gemspec b/bybit-connector-ruby.gemspec index ed43a5e..434a1b2 100644 --- a/bybit-connector-ruby.gemspec +++ b/bybit-connector-ruby.gemspec @@ -22,9 +22,15 @@ Gem::Specification.new do |s| 'rubygems_mfa_required' => 'true' } s.add_dependency 'faraday', '~> 2.0' + # WebSocket support is opt-in via `require 'bybit/websocket'`. The REST + # core (`require 'bybit'`) does not touch this gem, so REST-only users + # aren't slowed down by it, but declaring it as a runtime dep gives one + # `bundle install` for the common case where callers want both. + s.add_dependency 'websocket-client-simple', '~> 0.6' s.add_development_dependency 'rspec', '~> 3.12' s.add_development_dependency 'rubocop', '~> 1.60' s.add_development_dependency 'simplecov', '~> 0.22' + s.add_development_dependency 'vcr', '~> 6.2' s.add_development_dependency 'webmock', '~> 3.0' s.add_development_dependency 'yard', '~> 0.9' end diff --git a/lib/bybit/client.rb b/lib/bybit/client.rb index f34789c..c18bf98 100644 --- a/lib/bybit/client.rb +++ b/lib/bybit/client.rb @@ -8,9 +8,12 @@ require 'bybit/rest_api/broker_service' require 'bybit/rest_api/crypto_loan_service' require 'bybit/rest_api/earn_service' +require 'bybit/rest_api/institutional_loan_service' require 'bybit/rest_api/market_service' +require 'bybit/rest_api/misc_service' require 'bybit/rest_api/p2p_service' require 'bybit/rest_api/position_service' +require 'bybit/rest_api/pre_upgrade_service' require 'bybit/rest_api/rfq_service' require 'bybit/rest_api/spot_margin_service' require 'bybit/rest_api/trade_service' @@ -34,7 +37,8 @@ def configure class Client # gen-sdk-ruby:client-readers:start attr_reader :account, :affiliate, :asset, :bot, :broker, :crypto_loan, :earn, - :market, :p2p, :position, :rfq, :spot_margin, :trade, :user + :institutional_loan, :market, :misc, :p2p, :position, :pre_upgrade, + :rfq, :spot_margin, :trade, :user # gen-sdk-ruby:client-readers:end @@ -52,9 +56,12 @@ def initialize(config = nil, **overrides) @broker = RestApi::BrokerService.new(session) @crypto_loan = RestApi::CryptoLoanService.new(session) @earn = RestApi::EarnService.new(session) + @institutional_loan = RestApi::InstitutionalLoanService.new(session) @market = RestApi::MarketService.new(session) + @misc = RestApi::MiscService.new(session) @p2p = RestApi::P2pService.new(session) @position = RestApi::PositionService.new(session) + @pre_upgrade = RestApi::PreUpgradeService.new(session) @rfq = RestApi::RfqService.new(session) @spot_margin = RestApi::SpotMarginService.new(session) @trade = RestApi::TradeService.new(session) diff --git a/lib/bybit/rest_api/institutional_loan_service.rb b/lib/bybit/rest_api/institutional_loan_service.rb new file mode 100644 index 0000000..048b771 --- /dev/null +++ b/lib/bybit/rest_api/institutional_loan_service.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + # /v5/ins-loan/* — institutional (OTC) loan namespace. Distinct from + # `crypto_loan` (retail flexible / fixed loans): these endpoints require + # a Bybit-provisioned institutional risk-unit UID and are read-only for + # non-institutional keys. + class InstitutionalLoanService < BaseService + # Get Product Info + # + # GET /v5/ins-loan/product-infos + # + # @option kwargs [String] :product_id Product ID; omit to return all products. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/margin-product-info + def get_product_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/ins-loan/product-infos', params: params) + end + + # Get Margin Coin Info (ensure-tokens-convert) + # + # GET /v5/ins-loan/ensure-tokens-convert + # + # @option kwargs [String] :product_id Product ID + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/margin-coin-convert-info + def get_margin_coin_info(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/ins-loan/ensure-tokens-convert', params: params) + end + + # Get Loan Orders + # + # GET /v5/ins-loan/loan-order + # + # @option kwargs [Integer] :order_id Loan order ID + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [Integer] :limit Records per page (default/max 100) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/loan-info + def get_loan_orders(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/ins-loan/loan-order', params: params) + end + + # Get Repayment Orders + # + # GET /v5/ins-loan/repaid-history + # + # @option kwargs [Integer] :start_time Start timestamp in ms + # @option kwargs [Integer] :end_time End timestamp in ms + # @option kwargs [Integer] :limit Records per page + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/repay-info + def get_repayment_orders(**kwargs) + params = kwargs.dup + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/ins-loan/repaid-history', params: params) + end + + # Get Loan-to-Value + # + # GET /v5/ins-loan/ltv-convert + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/ltv-convert + def get_ltv + @session.sign_request(method: :get, path: '/v5/ins-loan/ltv-convert') + end + + # Bind Or Unbind UID (institutional risk unit) + # + # POST /v5/ins-loan/association-uid + # + # @param uid [String] Sub UID to bind or unbind + # @param operate [String] `0` bind, `1` unbind + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/bind-uid + def bind_or_unbind_uid(uid:, operate:, **kwargs) + params = kwargs.merge(uid: uid, operate: operate) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/ins-loan/association-uid', body: params) + end + + # Repay Loan + # + # POST /v5/ins-loan/repay-loan + # + # @param order_id [String] Loan order ID to repay + # @param amount [String] Repayment amount + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/otc/repay-loan + def repay_loan(order_id:, amount:, **kwargs) + params = kwargs.merge(order_id: order_id, amount: amount) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :post, path: '/v5/ins-loan/repay-loan', body: params) + end + end + end +end diff --git a/lib/bybit/rest_api/misc_service.rb b/lib/bybit/rest_api/misc_service.rb new file mode 100644 index 0000000..73b6bd7 --- /dev/null +++ b/lib/bybit/rest_api/misc_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + # Endpoints that don't cleanly belong under a per-domain service: + # announcement feed and system status. + class MiscService < BaseService + # Get Announcements + # + # GET /v5/announcements/index + # + # @param locale [String] Language code, e.g. `en-US`, `zh-TW`. + # @option kwargs [String] :type Announcement type filter + # @option kwargs [String] :tag Tag filter + # @option kwargs [Integer] :page Page number + # @option kwargs [Integer] :limit Records per page (max 100) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/announcement + def get_announcements(locale:, **kwargs) + params = kwargs.merge(locale: locale) + params = Bybit::Utils::WireKeys.camelize(params) + @session.public_request(path: '/v5/announcements/index', params: params) + end + + # Get System Status + # + # GET /v5/system/status + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/system-status + def get_system_status + @session.public_request(path: '/v5/system/status') + end + end + end +end diff --git a/lib/bybit/rest_api/pre_upgrade_service.rb b/lib/bybit/rest_api/pre_upgrade_service.rb new file mode 100644 index 0000000..ecc99e9 --- /dev/null +++ b/lib/bybit/rest_api/pre_upgrade_service.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +module Bybit + module RestApi + # /v5/pre-upgrade/* — historical queries scoped to activity that occurred + # BEFORE a Classic → UTA account upgrade. These records live in the legacy + # partition and never merge into the UTA tables, so callers upgrading from + # older SDKs need a dedicated namespace to fetch them. + class PreUpgradeService < BaseService + # Get Pre-upgrade Order History + # + # GET /v5/pre-upgrade/order/history + # + # @param category [String] Product type: `linear`, `inverse`, `option` + # @option kwargs [String] :symbol Symbol filter + # @option kwargs [String] :base_coin Base coin filter + # @option kwargs [String] :order_id Order ID filter + # @option kwargs [String] :order_link_id User-side order link ID + # @option kwargs [String] :order_filter Order-type filter + # @option kwargs [String] :order_status Order status filter + # @option kwargs [Integer] :start_time Start ms timestamp + # @option kwargs [Integer] :end_time End ms timestamp + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/order-list + def get_order_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/order/history', params: params) + end + + # Get Pre-upgrade Trade (Execution) History + # + # GET /v5/pre-upgrade/execution/list + # + # @param category [String] Product type: `linear`, `inverse`, `option` + # @option kwargs [String] :symbol Symbol filter + # @option kwargs [String] :order_id Order ID filter + # @option kwargs [String] :order_link_id User-side order link ID + # @option kwargs [String] :base_coin Base coin filter + # @option kwargs [Integer] :start_time Start ms timestamp + # @option kwargs [Integer] :end_time End ms timestamp + # @option kwargs [String] :exec_type Execution type filter + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/execution + def get_trade_history(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/execution/list', params: params) + end + + # Get Pre-upgrade Closed PnL + # + # GET /v5/pre-upgrade/position/closed-pnl + # + # Bybit V5 only requires `symbol` when `category` is `inverse`; for + # `linear` it's optional. Keep it as an optional kwarg here — callers + # querying inverse pass it, callers on linear can omit it. + # + # @param category [String] Product type: `linear`, `inverse` + # @option kwargs [String] :symbol Symbol name (required for `inverse`) + # @option kwargs [Integer] :start_time Start ms timestamp + # @option kwargs [Integer] :end_time End ms timestamp + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/close-pnl + def get_closed_pnl(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/position/closed-pnl', params: params) + end + + # Get Pre-upgrade Transaction Log (USDC Derivatives wallet) + # + # GET /v5/pre-upgrade/account/transaction-log + # + # @param category [String] Product type: `linear`, `option` + # @option kwargs [String] :base_coin Base coin filter + # @option kwargs [String] :type Transaction type filter + # @option kwargs [Integer] :start_time Start ms timestamp + # @option kwargs [Integer] :end_time End ms timestamp + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/transaction-log + def get_transaction_log(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/account/transaction-log', params: params) + end + + # Get Pre-upgrade Option Delivery Record + # + # GET /v5/pre-upgrade/asset/delivery-record + # + # @param category [String] Product type: `option` + # @option kwargs [String] :symbol Symbol filter + # @option kwargs [Integer] :expiry_date Delivery expiry date (YYYYMMDD) + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/delivery + def get_option_delivery_record(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/asset/delivery-record', params: params) + end + + # Get Pre-upgrade USDC Session Settlement Record + # + # GET /v5/pre-upgrade/asset/settlement-record + # + # @param category [String] Product type: `linear` + # @option kwargs [String] :symbol Symbol filter + # @option kwargs [Integer] :limit Records per page + # @option kwargs [String] :cursor Pagination cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/pre-upgrade/settlement + def get_usdc_session_settlement(category:, **kwargs) + params = kwargs.merge(category: category) + params = Bybit::Utils::WireKeys.camelize(params) + @session.sign_request(method: :get, path: '/v5/pre-upgrade/asset/settlement-record', params: params) + end + end + end +end diff --git a/lib/bybit/websocket.rb b/lib/bybit/websocket.rb new file mode 100644 index 0000000..f505ab3 --- /dev/null +++ b/lib/bybit/websocket.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Optional entry point. `require 'bybit'` loads the REST core only; the +# WebSocket client lives behind a separate `require 'bybit/websocket'` so +# REST-only consumers don't take the `websocket-client-simple` dependency +# at load time. + +require 'bybit' +require 'bybit/websocket/client' diff --git a/lib/bybit/websocket/client.rb b/lib/bybit/websocket/client.rb new file mode 100644 index 0000000..9f28078 --- /dev/null +++ b/lib/bybit/websocket/client.rb @@ -0,0 +1,294 @@ +# frozen_string_literal: true + +require 'json' +require 'openssl' + +module Bybit + module WebSocket + # Bybit V5 WebSocket client — public streams (spot / linear / inverse / + # option) and the private user-data stream. + # + # Design goals mirror the REST session: + # - one class, `channel:` selects endpoint (avoids Public/Private class + # duplication for ping / subscribe / dispatch logic) + # - explicit callbacks (`on_message`, `on_open`, `on_close`, `on_error`) + # passed as procs; no global registry / singleton + # - HMAC-SHA256 auth for `:private` is identical to REST signing except + # the payload is `"GET/realtime" + expires` — the docs are explicit + # - ping every `ping_interval` seconds via a lightweight background + # thread; server terminates connections after 20s silence + # - automatic resubscribe on reconnect (topic list is retained across + # `#connect` calls) so callers don't lose subscriptions on transient + # network blips + # + # This class depends on `websocket-client-simple` (a runtime dep of the + # gem). If missing, requiring it raises a clear LoadError below. + class Client + begin + require 'websocket-client-simple' + rescue LoadError => e + raise LoadError, + "Bybit::WebSocket::Client requires the 'websocket-client-simple' " \ + "gem. Add `gem 'websocket-client-simple'` to your Gemfile. " \ + "(#{e.message})" + end + + # Channel → WS path (public streams are category-scoped; private has one). + PUBLIC_CHANNELS = %i[spot linear inverse option].freeze + PRIVATE_CHANNEL = :private + TRADE_CHANNEL = :trade + + PATHS = { + spot: '/v5/public/spot', + linear: '/v5/public/linear', + inverse: '/v5/public/inverse', + option: '/v5/public/option', + private: '/v5/private', + trade: '/v5/trade' + }.freeze + + HOST_MAINNET = 'stream.bybit.com' + HOST_TESTNET = 'stream-testnet.bybit.com' + + DEFAULT_PING_INTERVAL = 20 + # Bybit rejects auth expiry timestamps in the past AND >30s in the future. + DEFAULT_AUTH_EXPIRY_MS = 10_000 + + attr_reader :channel, :testnet, :url, :subscriptions + + # @param channel [Symbol] one of `:spot`, `:linear`, `:inverse`, + # `:option`, `:private`, `:trade`. + # @param testnet [Boolean] hit stream-testnet.bybit.com instead of mainnet + # @param api_key [String] required for `:private` / `:trade` + # @param api_secret [String] required for `:private` / `:trade` + # @param ping_interval [Integer] seconds between ping frames (default 20) + # @param url [String] override the entire WS URL (skips host/path build) + # @param on_open, on_message, on_close, on_error [Proc] + def initialize(channel:, testnet: false, + api_key: nil, api_secret: nil, + ping_interval: DEFAULT_PING_INTERVAL, + url: nil, + on_open: nil, on_message: nil, on_close: nil, on_error: nil) + validate_channel!(channel) + @channel = channel + @testnet = testnet + @api_key = api_key + @api_secret = api_secret + @ping_interval = ping_interval + @url = url || build_url + @on_open = on_open + @on_message = on_message + @on_close = on_close + @on_error = on_error + @subscriptions = [] + @ws = nil + @ping_thread = nil + @connected = false + @auth_ok = false + @closing = false + end + + # Open the WebSocket, authenticate (if private/trade), and start pinging. + # Calling `connect` a second time closes the prior socket first, then + # opens a fresh one and replays any topics buffered in `@subscriptions` + # (call `disconnect` explicitly if you want to drop them). No automatic + # reconnect on network drop — the caller decides when to re-open. + def connect + require_auth! if requires_auth? + # Idempotent: tear down any prior socket / ping thread so a second + # `connect` call doesn't leak the previous ws or leave two pingers. + disconnect if @ws + @closing = false + client = self + @ws = ::WebSocket::Client::Simple.connect(@url) + + @ws.on(:open) { client.send(:handle_open) } + @ws.on(:message) { |msg| client.send(:handle_message, msg) } + @ws.on(:close) { |ev| client.send(:handle_close, ev) } + @ws.on(:error) { |err| client.send(:handle_error, err) } + self + end + + # Subscribe to one or more topics. Bybit expects an array under `args`: + # {"op":"subscribe","args":["orderbook.1.BTCUSDT","tickers.BTCUSDT"]} + # Callers can call `subscribe` before `connect`; topics are buffered and + # sent once the socket opens (private streams also wait for auth OK). + def subscribe(*topics) + topics = topics.flatten.compact.uniq + return if topics.empty? + + @subscriptions |= topics + flush_subscriptions(topics) if ready? + self + end + + def unsubscribe(*topics) + topics = topics.flatten.compact.uniq + return if topics.empty? + + @subscriptions -= topics + send_frame(op: 'unsubscribe', args: topics) if ready? + self + end + + # Send a WS message directly (escape hatch for placing orders over the + # `/v5/trade` stream; the docs specify per-op payloads). + def send_raw(payload) + return unless @ws + + @ws.send(payload.is_a?(String) ? payload : JSON.generate(payload)) + end + + def disconnect + @closing = true + stop_pinger + @connected = false + @auth_ok = false + begin + @ws&.close + rescue StandardError + # Socket may already be half-closed by the peer or in a bad state — + # we only care about detaching, not about clean protocol shutdown. + end + @ws = nil + end + + def connected? + @connected + end + + def ready? + return false unless @connected + + requires_auth? ? @auth_ok : true + end + + private + + def validate_channel!(channel) + allowed = PUBLIC_CHANNELS + [PRIVATE_CHANNEL, TRADE_CHANNEL] + return if allowed.include?(channel) + + raise Bybit::ConfigurationError, + "invalid channel: #{channel.inspect} (must be one of #{allowed.inspect})" + end + + def requires_auth? + channel == PRIVATE_CHANNEL || channel == TRADE_CHANNEL + end + + def require_auth! + return unless @api_key.nil? || @api_secret.nil? + + raise Bybit::ConfigurationError, + ":#{channel} WebSocket requires api_key + api_secret" + end + + def build_url + host = @testnet ? HOST_TESTNET : HOST_MAINNET + path = PATHS.fetch(channel) + "wss://#{host}#{path}" + end + + def handle_open + @connected = true + if requires_auth? + send_auth_frame + else + @auth_ok = true + flush_subscriptions(@subscriptions) + end + start_pinger + @on_open&.call + end + + def handle_message(raw) + data = raw.respond_to?(:data) ? raw.data : raw.to_s + parsed = safe_parse(data) + if parsed.is_a?(Hash) && parsed['op'] == 'auth' + # V5 WS auth reply: {"op":"auth","success":true|false,"ret_msg":...}. + # `success` is authoritative — do NOT treat missing retCode as OK, + # else a rejection with only ret_msg would silently pass. + @auth_ok = parsed['success'] == true + flush_subscriptions(@subscriptions) if @auth_ok + end + @on_message&.call(parsed.nil? ? data : parsed) + end + + def handle_close(event) + stop_pinger + @connected = false + @auth_ok = false + @on_close&.call(event) + end + + def handle_error(err) + # Suppress the noisy IOError the reader thread emits when we close + # the socket deliberately (see `disconnect`). Real transport errors + # still fire — @closing only gates the shutdown window. + return if @closing + + @on_error&.call(err) + end + + # Ping every `ping_interval` seconds; Bybit closes idle sockets around + # 20s of silence, so keep this ≤ 20s. + def start_pinger + stop_pinger + ws = @ws + interval = @ping_interval + @ping_thread = Thread.new do + Thread.current.name = "bybit-ws-ping-#{channel}" if Thread.current.respond_to?(:name=) + loop do + sleep interval + break if ws.nil? || ws != @ws + + begin + ws.send(JSON.generate(op: 'ping')) + rescue StandardError + break + end + end + end + end + + def stop_pinger + return unless @ping_thread + + begin + @ping_thread.kill + rescue StandardError + nil + end + @ping_thread = nil + end + + def flush_subscriptions(topics) + return if topics.nil? || topics.empty? + + send_frame(op: 'subscribe', args: topics) + end + + def send_frame(payload) + return unless @ws + + @ws.send(JSON.generate(payload)) + end + + # HMAC-SHA256 over "GET/realtime" + expires — the private-stream auth + # scheme documented at + # https://bybit-exchange.github.io/docs/v5/ws/connect#authentication + def send_auth_frame + expires = (Time.now.to_f * 1000).to_i + DEFAULT_AUTH_EXPIRY_MS + signature = OpenSSL::HMAC.hexdigest('SHA256', @api_secret, "GET/realtime#{expires}") + send_frame(op: 'auth', args: [@api_key, expires, signature]) + end + + def safe_parse(str) + JSON.parse(str) + rescue JSON::ParserError + nil + end + end + end +end diff --git a/spec/fixtures/vcr_cassettes/account_get_wallet_balance.yml b/spec/fixtures/vcr_cassettes/account_get_wallet_balance.yml new file mode 100644 index 0000000..50fcd55 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/account_get_wallet_balance.yml @@ -0,0 +1,22 @@ +--- +http_interactions: +- request: + method: get + uri: https://api-testnet.bybit.com/v5/account/wallet-balance?accountType=UNIFIED + body: + encoding: US-ASCII + string: '' + headers: {} + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + body: + encoding: UTF-8 + string: '{"retCode":0,"retMsg":"OK","result":{"list":[{"accountType":"UNIFIED","totalEquity":"1000.00","coin":[]}]},"retExtInfo":{},"time":1700000000000}' + http_version: + recorded_at: Wed, 20 Jul 2026 00:00:00 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/market_get_server_time.yml b/spec/fixtures/vcr_cassettes/market_get_server_time.yml new file mode 100644 index 0000000..ad6a0c9 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/market_get_server_time.yml @@ -0,0 +1,22 @@ +--- +http_interactions: +- request: + method: get + uri: https://api-testnet.bybit.com/v5/market/time + body: + encoding: US-ASCII + string: '' + headers: {} + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + body: + encoding: UTF-8 + string: '{"retCode":0,"retMsg":"OK","result":{"timeSecond":"1700000000","timeNano":"1700000000000000000"},"retExtInfo":{},"time":1700000000000}' + http_version: + recorded_at: Wed, 20 Jul 2026 00:00:00 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/trade_create_order.yml b/spec/fixtures/vcr_cassettes/trade_create_order.yml new file mode 100644 index 0000000..35df155 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/trade_create_order.yml @@ -0,0 +1,22 @@ +--- +http_interactions: +- request: + method: post + uri: https://api-testnet.bybit.com/v5/order/create + body: + encoding: UTF-8 + string: '{"price":"30000","timeInForce":"GTC","category":"linear","symbol":"BTCUSDT","side":"Buy","orderType":"Limit","qty":"0.01"}' + headers: {} + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + body: + encoding: UTF-8 + string: '{"retCode":0,"retMsg":"OK","result":{"orderId":"1234567890","orderLinkId":""},"retExtInfo":{},"time":1700000000000}' + http_version: + recorded_at: Wed, 20 Jul 2026 00:00:00 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/integration/rest_end_to_end_spec.rb b/spec/integration/rest_end_to_end_spec.rb new file mode 100644 index 0000000..08d637b --- /dev/null +++ b/spec/integration/rest_end_to_end_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'support/vcr_setup' + +# End-to-end integration coverage using VCR cassettes. Two things this file +# guarantees that the WebMock-based service specs don't: +# +# 1. The full request that leaves the SDK — verb, URL, query string, JSON +# body — matches the recorded cassette byte-for-byte. Any drift in +# WireKeys camelization, query encoding, or POST body serialization +# causes VCR to raise UnhandledHTTPRequestError. +# 2. The HMAC signature that ships in the X-BAPI-SIGN header is exactly the +# value derived from the signed payload. Signatures are checked in-spec +# against a pre-computed golden value (Time is frozen so signing is +# deterministic). +RSpec.describe 'REST end-to-end via VCR', :vcr do + let(:config) do + c = Bybit::Configuration.new + c.api_key = 'test-key' + c.api_secret = 'test-secret' + c.testnet = true + c.recv_window = '5000' + c + end + let(:client) { Bybit::Client.new(config) } + # Freeze the signing timestamp so the recorded signature stays stable. + let(:fixed_ms) { 1_700_000_000_000 } + + before do + allow(Time).to receive(:now).and_return(Time.at(fixed_ms / 1000.0)) + end + + it 'issues an unsigned public GET (market/time) and parses the V5 envelope' do + VCR.use_cassette('market_get_server_time') do + resp = client.market.get_server_time + expect(resp['retCode']).to eq(0) + expect(resp['result']['timeSecond']).to eq('1700000000') + end + end + + it 'signs a GET params-in-query (account/wallet-balance) with the expected HMAC header' do + VCR.use_cassette('account_get_wallet_balance') do + captured = nil + WebMock.after_request do |req, _| + captured = req if req.uri.path == '/v5/account/wallet-balance' + end + + resp = client.account.get_wallet_balance(account_type: 'UNIFIED') + expect(resp['retCode']).to eq(0) + + # HMAC over: timestamp + api_key + recv_window + query_string. + # Golden value is precomputed in tools/gen_signatures — DO NOT + # regenerate without also updating the cassette request URL. + expected_sig = '3f10586267639c9f3f4f5e32e491a6ef80d157db06f51eb79e4988e24f97adba' + expect(captured.headers['X-Bapi-Sign']).to eq(expected_sig) + expect(captured.headers['X-Bapi-Timestamp']).to eq(fixed_ms.to_s) + expect(captured.headers['X-Bapi-Api-Key']).to eq('test-key') + # params: routes to the query string, NEVER the body, on GET. + expect(captured.uri.query).to eq('accountType=UNIFIED') + expect(captured.body.to_s).to eq('') + end + end + + it 'signs a POST body-only (order/create) — body: routes to JSON, no query string' do + VCR.use_cassette('trade_create_order') do + captured = nil + WebMock.after_request do |req, _| + captured = req if req.uri.path == '/v5/order/create' + end + + resp = client.trade.create_order( + category: 'linear', symbol: 'BTCUSDT', side: 'Buy', + order_type: 'Limit', qty: '0.01', price: '30000', time_in_force: 'GTC' + ) + expect(resp['result']['orderId']).to eq('1234567890') + + # HMAC over the JSON body string (POST branch of the signer). Golden + # value pinned to the exact insertion order of `kwargs.merge(...)` in + # TradeService#create_order — if that method reorders its merges, this + # signature drifts and the test fails, protecting the wire contract. + expected_sig = 'aae80c0b052347759ce532cce2f73dac47a96738d646e3df3969bcd733a9af8e' + expect(captured.headers['X-Bapi-Sign']).to eq(expected_sig) + # Body-carrying POST does NOT populate the query string. + expect(captured.uri.query).to be_nil + # Wire body follows kwargs-first insertion order (see comment above). + expect(captured.body).to eq( + '{"price":"30000","timeInForce":"GTC","category":"linear",' \ + '"symbol":"BTCUSDT","side":"Buy","orderType":"Limit","qty":"0.01"}' + ) + end + end +end diff --git a/spec/services/institutional_loan_service_spec.rb b/spec/services/institutional_loan_service_spec.rb new file mode 100644 index 0000000..552569f --- /dev/null +++ b/spec/services/institutional_loan_service_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::InstitutionalLoanService — table-driven +# assertion that every public method dispatches to the correct +# (path, HTTP method, signed) tuple. + +RSpec.describe Bybit::RestApi::InstitutionalLoanService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + method_table = [ + { name: :get_product_info, path: '/v5/ins-loan/product-infos', method: :get, signed: false }, + { name: :get_margin_coin_info, path: '/v5/ins-loan/ensure-tokens-convert', method: :get, signed: false }, + { name: :get_loan_orders, path: '/v5/ins-loan/loan-order', method: :get, signed: true }, + { name: :get_repayment_orders, path: '/v5/ins-loan/repaid-history', method: :get, signed: true }, + { name: :get_ltv, path: '/v5/ins-loan/ltv-convert', method: :get, signed: true }, + { name: :bind_or_unbind_uid, path: '/v5/ins-loan/association-uid', method: :post, signed: true }, + { name: :repay_loan, path: '/v5/ins-loan/repay-loan', method: :post, signed: true }, + ].freeze + + method_table.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ''] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/misc_service_spec.rb b/spec/services/misc_service_spec.rb new file mode 100644 index 0000000..82f58e3 --- /dev/null +++ b/spec/services/misc_service_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::MiscService. + +RSpec.describe Bybit::RestApi::MiscService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + method_table = [ + { name: :get_announcements, path: '/v5/announcements/index', method: :get, signed: false }, + { name: :get_system_status, path: '/v5/system/status', method: :get, signed: false }, + ].freeze + + method_table.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ''] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/services/pre_upgrade_service_spec.rb b/spec/services/pre_upgrade_service_spec.rb new file mode 100644 index 0000000..042b465 --- /dev/null +++ b/spec/services/pre_upgrade_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Smoke tests for Bybit::RestApi::PreUpgradeService. + +RSpec.describe Bybit::RestApi::PreUpgradeService do + let(:session) { instance_double(Bybit::Session, public_request: {}, sign_request: {}) } + let(:svc) { described_class.new(session) } + + method_table = [ + { name: :get_order_history, path: '/v5/pre-upgrade/order/history', method: :get, signed: true }, + { name: :get_trade_history, path: '/v5/pre-upgrade/execution/list', method: :get, signed: true }, + { name: :get_closed_pnl, path: '/v5/pre-upgrade/position/closed-pnl', method: :get, signed: true }, + { name: :get_transaction_log, path: '/v5/pre-upgrade/account/transaction-log', method: :get, signed: true }, + { name: :get_option_delivery_record, path: '/v5/pre-upgrade/asset/delivery-record', method: :get, signed: true }, + { name: :get_usdc_session_settlement, path: '/v5/pre-upgrade/asset/settlement-record', method: :get, signed: true }, + ].freeze + + method_table.each do |row| + describe "##{row[:name]}" do + it "dispatches #{row[:method].upcase} #{row[:path]} (signed=#{row[:signed]})" do + target = row[:signed] ? :sign_request : :public_request + expect(session).to receive(target) do |**kw| + expect(kw[:path]).to eq(row[:path]) + if row[:signed] || row[:method] != :get + expect(kw[:method]).to eq(row[:method]) if kw.key?(:method) + end + {} + end + method = svc.method(row[:name]) + required_kw = method.parameters.select { |t, _| t == :keyreq }.map { |_, n| [n, ''] }.to_h + svc.public_send(row[:name], **required_kw) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4c09c5c..3a22e28 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,10 +9,14 @@ # Signing + transport layer must stay tight — fail CI if either drops. # SimpleCov's `minimum_coverage_by_file` only accepts a single global percentage # (or a criterion hash), not per-file thresholds — enforce them by hand. +# Explicit files pin the signing + transport bar. Every rest_api/*_service.rb +# gets a floor bar via the directory-wide check below — landing a new service +# without a matching spec drops CI immediately. PER_FILE_COVERAGE_GATES = { 'lib/bybit/authentication.rb' => 100.0, 'lib/bybit/session.rb' => 90.0 }.freeze +SERVICE_FILE_COVERAGE_FLOOR = 80.0 SimpleCov.at_exit do SimpleCov.result.format! @@ -23,6 +27,19 @@ format('%s: %.2f%% < %.2f%%', path: path, pct: file.covered_percent, req: threshold) end + + # Service files: every rest_api/*_service.rb must meet the floor. base_service + # is skipped (a two-line initializer with nothing meaningful to cover). + SimpleCov.result.files.each do |file| + next unless file.filename.include?('/lib/bybit/rest_api/') + next if file.filename.end_with?('/base_service.rb') + next if file.covered_percent >= SERVICE_FILE_COVERAGE_FLOOR + + rel = file.filename.sub(%r{^.*/lib/}, 'lib/') + failed << format('%s: %.2f%% < %.2f%%', + path: rel, pct: file.covered_percent, req: SERVICE_FILE_COVERAGE_FLOOR) + end + unless failed.empty? warn "SimpleCov per-file coverage gate failed:\n #{failed.join("\n ")}" exit 1 diff --git a/spec/support/vcr_setup.rb b/spec/support/vcr_setup.rb new file mode 100644 index 0000000..80c66e8 --- /dev/null +++ b/spec/support/vcr_setup.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'vcr' + +VCR.configure do |c| + c.cassette_library_dir = File.expand_path('../fixtures/vcr_cassettes', __dir__) + c.hook_into :webmock + c.default_cassette_options = { + # Strict replay: fail the spec if the outbound request doesn't match a + # recorded interaction. Prevents accidentally hitting real Bybit endpoints + # from CI when a cassette is missing or drifts. + record: :none, + # Match on method + URI + body so signature-payload assertions ride on the + # cassette: if either the query string OR the JSON body drifts from what + # the SDK signed on record day, the interaction won't match and the spec + # fails. Headers are matched separately in the spec (see integration/*). + match_requests_on: %i[method uri body] + } + # Redact any accidental live-secret capture in future cassette recordings. + c.filter_sensitive_data('') { 'test-key' } + c.filter_sensitive_data('') { 'test-secret' } +end diff --git a/spec/websocket/client_spec.rb b/spec/websocket/client_spec.rb new file mode 100644 index 0000000..5e45472 --- /dev/null +++ b/spec/websocket/client_spec.rb @@ -0,0 +1,220 @@ +# frozen_string_literal: true + +require 'bybit/websocket/client' + +# Unit tests exercise the WebSocket client with the underlying +# `WebSocket::Client::Simple` stubbed out — WebMock cannot help here (it only +# intercepts HTTP), so we swap the `connect` factory with a fake socket that +# captures frames and lets us fire synthetic on_open / on_message callbacks. +RSpec.describe Bybit::WebSocket::Client do + # Minimal ws double: records outbound frames, exposes handler map so tests + # can invoke on_open / on_message / on_close. + class FakeWs + attr_reader :sent, :handlers + + def initialize + @sent = [] + @handlers = {} + @closed = false + end + + def on(event, &block) + @handlers[event] = block + end + + def send(msg) + @sent << msg + end + + def close + @closed = true + @handlers[:close]&.call(nil) + end + + def fire(event, arg = nil) + @handlers[event]&.call(arg) + end + end + + let(:fake_ws) { FakeWs.new } + + before do + # The real `WebSocket::Client::Simple.connect` opens a socket + spins up + # a reader thread — swap it for a FakeWs that records outbound frames + # and lets the test fire synthetic on_open / on_message callbacks. + allow(::WebSocket::Client::Simple).to receive(:connect).and_return(fake_ws) + end + + describe 'channel validation' do + it 'accepts each documented public channel' do + %i[spot linear inverse option].each do |ch| + expect { described_class.new(channel: ch) }.not_to raise_error + end + end + + it 'accepts :private and :trade' do + expect { described_class.new(channel: :private, api_key: 'k', api_secret: 's') }.not_to raise_error + expect { described_class.new(channel: :trade, api_key: 'k', api_secret: 's') }.not_to raise_error + end + + it 'rejects unknown channels' do + expect { described_class.new(channel: :spread) }.to raise_error(Bybit::ConfigurationError, /invalid channel/) + end + end + + describe 'URL selection' do + it 'points at mainnet by default' do + expect(described_class.new(channel: :linear).url).to eq('wss://stream.bybit.com/v5/public/linear') + end + + it 'points at stream-testnet when testnet: true' do + expect(described_class.new(channel: :spot, testnet: true).url) + .to eq('wss://stream-testnet.bybit.com/v5/public/spot') + end + + it 'respects a caller-supplied url override' do + cli = described_class.new(channel: :linear, url: 'wss://custom.example.com/x') + expect(cli.url).to eq('wss://custom.example.com/x') + end + end + + describe 'public subscribe flow' do + it 'buffers topics before connect and flushes on open' do + cli = described_class.new(channel: :linear).subscribe('tickers.BTCUSDT', 'orderbook.1.BTCUSDT') + expect(fake_ws.sent).to be_empty # not sent — socket isn't connected + cli.connect + fake_ws.fire(:open) + expect(fake_ws.sent.size).to eq(1) + frame = JSON.parse(fake_ws.sent.first) + expect(frame['op']).to eq('subscribe') + expect(frame['args']).to contain_exactly('tickers.BTCUSDT', 'orderbook.1.BTCUSDT') + end + + it 'de-duplicates topics across multiple subscribe calls' do + cli = described_class.new(channel: :linear) + cli.subscribe('tickers.BTCUSDT') + cli.subscribe('tickers.BTCUSDT', 'tickers.ETHUSDT') + expect(cli.subscriptions).to contain_exactly('tickers.BTCUSDT', 'tickers.ETHUSDT') + end + + it 'delivers messages to on_message as parsed hashes' do + seen = [] + cli = described_class.new(channel: :linear, on_message: ->(m) { seen << m }) + cli.connect + fake_ws.fire(:open) + fake_ws.fire(:message, double(data: '{"topic":"tickers.BTCUSDT","data":{"lastPrice":"30000"}}')) + expect(seen.first).to eq({ 'topic' => 'tickers.BTCUSDT', 'data' => { 'lastPrice' => '30000' } }) + end + + it 'passes raw string to on_message when body is not JSON' do + seen = [] + cli = described_class.new(channel: :linear, on_message: ->(m) { seen << m }) + cli.connect + fake_ws.fire(:open) + fake_ws.fire(:message, double(data: 'not-json')) + expect(seen.first).to eq('not-json') + end + end + + describe 'private-stream auth' do + it 'requires api_key + api_secret before connect' do + cli = described_class.new(channel: :private) + expect { cli.connect }.to raise_error(Bybit::ConfigurationError, /api_key/) + end + + it 'sends an auth frame on open and defers subscribe until auth OK' do + cli = described_class.new(channel: :private, api_key: 'key', api_secret: 'secret') + cli.subscribe('position') + cli.connect + fake_ws.fire(:open) + # First frame is auth, subscribe waits for the ack. + expect(fake_ws.sent.size).to eq(1) + auth = JSON.parse(fake_ws.sent.first) + expect(auth['op']).to eq('auth') + expect(auth['args'].first).to eq('key') + expect(auth['args'][2]).to match(/\A[a-f0-9]{64}\z/) + + # Auth success — subscribe should now flush. + fake_ws.fire(:message, double(data: '{"op":"auth","success":true}')) + sub = JSON.parse(fake_ws.sent.last) + expect(sub).to eq({ 'op' => 'subscribe', 'args' => ['position'] }) + end + + it 'does not flush subs if auth is rejected' do + cli = described_class.new(channel: :private, api_key: 'k', api_secret: 's') + cli.subscribe('position') + cli.connect + fake_ws.fire(:open) + fake_ws.fire(:message, double(data: '{"op":"auth","success":false,"retMsg":"bad sig"}')) + expect(fake_ws.sent.size).to eq(1) # only the auth frame + end + end + + describe 'unsubscribe / disconnect' do + it 'sends an unsubscribe frame and drops the topic' do + cli = described_class.new(channel: :linear).subscribe('tickers.BTCUSDT') + cli.connect + fake_ws.fire(:open) + fake_ws.sent.clear + cli.unsubscribe('tickers.BTCUSDT') + expect(JSON.parse(fake_ws.sent.first)).to eq({ 'op' => 'unsubscribe', 'args' => ['tickers.BTCUSDT'] }) + expect(cli.subscriptions).to be_empty + end + + it 'closes the socket on disconnect' do + cli = described_class.new(channel: :linear).connect + fake_ws.fire(:open) + cli.disconnect + expect(cli.connected?).to eq(false) + end + + it 'suppresses on_error during a deliberate disconnect' do + # The underlying ws-client-simple reader thread emits IOError when we + # close from the main thread; without the @closing gate that would leak + # into on_error and look like a real transport failure. + errors = [] + cli = described_class.new(channel: :linear, on_error: ->(e) { errors << e }).connect + fake_ws.fire(:open) + cli.disconnect + fake_ws.fire(:error, IOError.new('stream closed in another thread')) + expect(errors).to be_empty + end + end + + describe 'idempotent connect' do + it 'tears down the prior socket + ping thread before opening a new one' do + cli = described_class.new(channel: :linear).connect + fake_ws.fire(:open) + first_ws = fake_ws + + # Second `connect`: the client must call close on the previous ws and + # request a fresh one. Swap the factory to return a distinct fake and + # assert the old one was closed and replaced. + second_ws = FakeWs.new + allow(::WebSocket::Client::Simple).to receive(:connect).and_return(second_ws) + + cli.connect + expect(first_ws.instance_variable_get(:@closed)).to eq(true) + # Fresh socket wired up so a fired :open reaches the client. + second_ws.fire(:open) + expect(cli.connected?).to eq(true) + end + + it 'replays buffered subscriptions after a reconnect' do + cli = described_class.new(channel: :linear).subscribe('tickers.BTCUSDT') + cli.connect + fake_ws.fire(:open) + expect(cli.subscriptions).to include('tickers.BTCUSDT') + + second_ws = FakeWs.new + allow(::WebSocket::Client::Simple).to receive(:connect).and_return(second_ws) + cli.connect + second_ws.fire(:open) + + # First frame on the new socket is a subscribe replaying the topic. + first_frame = JSON.parse(second_ws.sent.first) + expect(first_frame['op']).to eq('subscribe') + expect(first_frame['args']).to include('tickers.BTCUSDT') + end + end +end From a4ec8c0eafca9e76cb669242c5103550b6974909 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Tue, 21 Jul 2026 11:31:07 +0800 Subject: [PATCH 16/18] optimization on CI --- lib/bybit/websocket/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bybit/websocket/client.rb b/lib/bybit/websocket/client.rb index 9f28078..c8d3d2b 100644 --- a/lib/bybit/websocket/client.rb +++ b/lib/bybit/websocket/client.rb @@ -174,7 +174,7 @@ def validate_channel!(channel) end def requires_auth? - channel == PRIVATE_CHANNEL || channel == TRADE_CHANNEL + [PRIVATE_CHANNEL, TRADE_CHANNEL].include?(channel) end def require_auth! From 13e397827af4f664ef282fe512e106827fcf115f Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Tue, 21 Jul 2026 12:20:40 +0800 Subject: [PATCH 17/18] optimization on reademe --- README.md | 139 +++++++++++++++++++++------ examples/quickstart.rb | 43 +++++++-- lib/bybit/rest_api/market_service.rb | 4 - 3 files changed, 145 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index d60be20..f524408 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,23 @@ Official lightweight Ruby connector for the [Bybit V5 REST API](https://bybit-exchange.github.io/docs/v5/intro). -`bybit-connector-ruby` wraps the Bybit V5 HTTP endpoints as a set of typed Ruby methods with keyword-arg signatures. Its goal is the same as [`pybit`](https://github.com/bybit-exchange/pybit) on the Python side: an easy-to-use, high-performance connector with a small dependency footprint. +`bybit-connector-ruby` wraps the Bybit V5 HTTP endpoints as a set of typed Ruby methods with keyword-arg signatures. +## Prerequisites + +Before you write any code you need a Bybit API key. Two accounts to know +about: + +- **Testnet** — [testnet.bybit.com](https://testnet.bybit.com) → sign up → + API Management → *Create New Key*. This is where you should point every + new integration until you're confident about behavior. Testnet balances + are virtual; nothing you do here touches real funds. +- **Mainnet** — [bybit.com](https://www.bybit.com) → API Management. Real + money. Enable only the permissions you actually need (spot / derivatives / + withdrawals) and prefer IP-restricted keys. + +Testnet and mainnet keys are **separate** — an API key issued on one won't +work against the other. The SDK selects the environment via +`testnet: true|false` (or an explicit `base_url:` override). ## Installation @@ -23,30 +39,43 @@ gem 'bybit-connector-ruby' ```ruby require 'bybit' +# All calls below run against testnet — swap testnet: false for mainnet +# once you're happy with the behavior. Bybit.configure do |c| - c.api_key = ENV['BYBIT_KEY'] - c.api_secret = ENV['BYBIT_SECRET'] - c.testnet = true # omit / false for mainnet + c.api_key = ENV['BYBIT_TESTNET_KEY'] + c.api_secret = ENV['BYBIT_TESTNET_SECRET'] + c.testnet = true end client = Bybit::Client.new -# Public endpoint — no auth needed +# 1. Public endpoint — no auth needed. puts client.market.get_server_time -# Signed endpoint — apiKey + apiSecret required +# 2. Signed endpoint — api_key + api_secret required. wallet = client.account.get_wallet_balance(account_type: 'UNIFIED') puts wallet['result']['list'] -# Place an order +# 3. Place a LIMIT order well below market so it sits on the book and does +# NOT fill (safe to run repeatedly). Adjust `price:` if BTC ever trades +# at $10k again — otherwise this stays a resting order you can cancel. order = client.trade.create_order( category: 'linear', symbol: 'BTCUSDT', side: 'Buy', order_type: 'Limit', qty: '0.01', - price: '30000', time_in_force: 'GTC', + price: '10000', time_in_force: 'GTC' ) -puts "orderId: #{order['result']['orderId']}" +order_id = order['result']['orderId'] +puts "orderId: #{order_id}" + +# 4. Cancel it before moving on. +client.trade.cancel_order(category: 'linear', symbol: 'BTCUSDT', order_id: order_id) ``` +> ⚠️ **Before switching `testnet: false`**: verify the price / qty in +> `create_order` won't cross the top of the book — a Limit Buy at $10k on +> mainnet becomes a market fill instantly (if BTC ever drops that low), +> and a Limit Sell at $1M does the reverse. + See `examples/quickstart.rb` for a runnable script. ## Configuration @@ -58,12 +87,17 @@ Bybit.configure do |c| c.api_key = ENV['BYBIT_KEY'] c.api_secret = ENV['BYBIT_SECRET'] c.testnet = false # default false - c.recv_window = '5000' # ms, X-BAPI-RECV-WINDOW header + c.recv_window = '5000' # milliseconds — Bybit rejects requests + # whose signed timestamp is older than + # this window. Bump to 10000+ if your + # clock drifts or the network is noisy. c.timeout = 10 # Faraday timeout, seconds end ``` -Or pass overrides to `Client.new`: +Or pass overrides to `Client.new` — **per-client overrides win over +`Bybit.configure`**, so you can share defaults globally and still spin up a +dedicated client with a different key or environment: ```ruby client = Bybit::Client.new(api_key: '...', api_secret: '...', testnet: true) @@ -142,6 +176,12 @@ Full hierarchy: Every `ApiError` exposes `#ret_code`, `#ret_msg`, `#result`, `#time`, `#http_status`. See the [Bybit V5 error-code list](https://bybit-exchange.github.io/docs/v5/error) for meanings. +> The `sleep 1 && retry` shown above is fine for exploration, **not** for +> production — Bybit will escalate throttling on tight retry loops. For +> real workloads, wire a `Faraday::Retry` middleware with exponential +> backoff via the "bring your own Faraday connection" hook in +> [Configuration](#configuration) above. + ## Return Value Every service method returns the raw parsed JSON as a `Hash`: @@ -154,32 +194,64 @@ response['result'] # => { 'category' => 'spot', 'symbol' => 'BTCUSDT', 'list response['time'] # => 1234567890000 ``` -## Testnet +> Keys are **strings, not symbols** — `response[:result]` is `nil`. This is +> a common first-hour gotcha; pass through `JSON.parse` semantics rather +> than converting. + +## Method Reference + +Method names follow the Bybit V5 endpoint slug in snake_case, grouped by +domain. A few illustrative mappings: + +| Bybit V5 path | SDK method | +| ------------------------------------- | ----------------------------------------- | +| `GET /v5/market/kline` | `client.market.get_kline` | +| `GET /v5/market/tickers` | `client.market.get_tickers` | +| `POST /v5/order/create` | `client.trade.create_order` | +| `POST /v5/order/cancel` | `client.trade.cancel_order` | +| `GET /v5/position/list` | `client.position.get_positions` | +| `GET /v5/account/wallet-balance` | `client.account.get_wallet_balance` | + +For the full list, generate YARD docs locally: + +``` +bundle exec yard doc +open doc/index.html +``` + +or browse the published copy on +[rubydoc.info/gems/bybit-connector-ruby](https://rubydoc.info/gems/bybit-connector-ruby). + +## Pagination -Flip `testnet: true` for [https://testnet.bybit.com](https://testnet.bybit.com): +Bybit V5 uses **opaque cursor pagination** — the response's +`result['nextPageCursor']` (empty string when the page is the last one) +feeds back in as the `cursor:` argument on the next call: ```ruby -client = Bybit::Client.new( - api_key: ENV['BYBIT_TESTNET_KEY'], - api_secret: ENV['BYBIT_TESTNET_SECRET'], - testnet: true, -) +cursor = nil +loop do + resp = client.trade.get_order_history(category: 'linear', limit: 50, cursor: cursor) + resp['result']['list'].each { |order| process(order) } + cursor = resp['result']['nextPageCursor'] + break if cursor.nil? || cursor.empty? +end ``` -REST base URLs (exported constants): +The same pattern works for `get_positions`, `get_execution_history`, +`get_transaction_log`, and every other paginated endpoint. -- `Bybit::BASE_URL_MAINNET` — `https://api.bybit.com` -- `Bybit::BASE_URL_TESTNET` — `https://api-testnet.bybit.com` +## WebSockets -WebSocket hosts follow the same split: +Hosts (mirror the REST split): - Mainnet — `wss://stream.bybit.com/v5/{public/|private|trade}` - Testnet — `wss://stream-testnet.bybit.com/v5/{public/|private|trade}` -## WebSockets - WebSocket support ships behind an opt-in require so REST-only consumers don't -load the underlying `websocket-client-simple` dependency at boot: +load the underlying `websocket-client-simple` dependency at boot. Four +callbacks are available; all are optional but you'll almost always want at +least `on_message`. ```ruby require 'bybit/websocket' @@ -187,8 +259,11 @@ require 'bybit/websocket' # Public stream — no auth needed. public_ws = Bybit::WebSocket::Client.new( channel: :linear, # :spot / :linear / :inverse / :option - testnet: false, - on_message: ->(msg) { puts msg.inspect }, + testnet: true, + on_open: -> { puts 'connected' }, + on_message: ->(msg) { puts msg.inspect }, # your handler + on_close: ->(_ev) { puts 'closed' }, + on_error: ->(err) { warn "ws error: #{err}" } ).connect public_ws.subscribe('tickers.BTCUSDT', 'orderbook.1.BTCUSDT') @@ -196,11 +271,15 @@ public_ws.subscribe('tickers.BTCUSDT', 'orderbook.1.BTCUSDT') # Private stream — HMAC-SHA256 auth over "GET/realtime" + expiry. private_ws = Bybit::WebSocket::Client.new( channel: :private, - api_key: ENV['BYBIT_KEY'], - api_secret: ENV['BYBIT_SECRET'], - on_message: ->(msg) { handle_private_event(msg) }, + testnet: true, + api_key: ENV['BYBIT_TESTNET_KEY'], + api_secret: ENV['BYBIT_TESTNET_SECRET'], + on_message: ->(msg) { puts msg.inspect } # your handler ).connect +# Subscribe returns immediately; the actual `op:subscribe` frame is deferred +# until the private-stream auth reply lands, so you can call subscribe +# anytime — before or after `#connect`. private_ws.subscribe('position', 'order', 'wallet') # Later: diff --git a/examples/quickstart.rb b/examples/quickstart.rb index 90ba83a..fd44fec 100644 --- a/examples/quickstart.rb +++ b/examples/quickstart.rb @@ -1,29 +1,35 @@ # frozen_string_literal: true -# End-to-end quickstart — public + signed calls + rescue matrix. -# Run with: BYBIT_KEY=... BYBIT_SECRET=... ruby examples/quickstart.rb +# End-to-end quickstart — mirrors README's Quick Start: +# 1. public unsigned call +# 2. signed GET (wallet balance) +# 3. safe LIMIT order well below market (does not fill) +# 4. cancel that order +# Run with: +# BYBIT_TESTNET_KEY=... BYBIT_TESTNET_SECRET=... ruby examples/quickstart.rb require 'bybit' Bybit.configure do |c| - c.api_key = ENV['BYBIT_KEY'] - c.api_secret = ENV['BYBIT_SECRET'] - c.testnet = true # flip to false for mainnet + c.api_key = ENV.fetch('BYBIT_TESTNET_KEY') + c.api_secret = ENV.fetch('BYBIT_TESTNET_SECRET') + c.testnet = true # flip to false for mainnet — see README warning end client = Bybit::Client.new -# 1. Public endpoint — server time (no auth needed) +# 1. Public endpoint — server time (no auth needed). puts '--- server time ---' puts client.market.get_server_time.inspect -# 2. Signed endpoint — wallet balance +# 2. Signed endpoint — wallet balance. puts '--- wallet balance ---' begin wallet = client.account.get_wallet_balance(account_type: 'UNIFIED') puts wallet['result']['list'].inspect rescue Bybit::AuthError => e warn "auth failed: [#{e.ret_code}] #{e.ret_msg}" + exit 1 rescue Bybit::RateLimitError => e warn "rate-limited: #{e.ret_msg}" rescue Bybit::TimeoutError => e @@ -33,3 +39,26 @@ rescue Bybit::ApiError => e warn "api error [#{e.ret_code}]: #{e.ret_msg}" end + +# 3. Place a LIMIT order well below market so it sits on the book and does +# NOT fill (safe to run repeatedly). Adjust price if BTC ever trades that +# low; otherwise this stays a resting order you can cancel below. +puts '--- place limit order ---' +begin + order = client.trade.create_order( + category: 'linear', symbol: 'BTCUSDT', + side: 'Buy', order_type: 'Limit', qty: '0.01', + price: '10000', time_in_force: 'GTC' + ) + order_id = order['result']['orderId'] + puts "orderId: #{order_id}" + + # 4. Cancel it before exiting so we don't leave test orders on the book. + puts '--- cancel order ---' + cancel = client.trade.cancel_order( + category: 'linear', symbol: 'BTCUSDT', order_id: order_id + ) + puts cancel['result'].inspect +rescue Bybit::ApiError => e + warn "api error [#{e.ret_code}]: #{e.ret_msg}" +end diff --git a/lib/bybit/rest_api/market_service.rb b/lib/bybit/rest_api/market_service.rb index f130bc4..32e3a22 100644 --- a/lib/bybit/rest_api/market_service.rb +++ b/lib/bybit/rest_api/market_service.rb @@ -109,7 +109,6 @@ def get_index_price_components(index_name:, **kwargs) # @see https://bybit-exchange.github.io/docs/v5/market/index-kline def get_index_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) - params[:end] = params.delete(:end_) if params.key?(:end_) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/market/index-price-kline', params: params) end @@ -178,7 +177,6 @@ def get_long_short_ratio(category:, symbol:, period:, **kwargs) # @see https://bybit-exchange.github.io/docs/v5/market/mark-kline def get_mark_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) - params[:end] = params.delete(:end_) if params.key?(:end_) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/market/mark-price-kline', params: params) end @@ -197,7 +195,6 @@ def get_mark_price_kline(symbol:, interval:, **kwargs) # @see https://bybit-exchange.github.io/docs/v5/market/kline def get_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) - params[:end] = params.delete(:end_) if params.key?(:end_) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/market/kline', params: params) end @@ -278,7 +275,6 @@ def get_orderbook(category:, symbol:, **kwargs) # @see https://bybit-exchange.github.io/docs/v5/market/premium-index-kline def get_premium_index_price_kline(symbol:, interval:, **kwargs) params = kwargs.merge(symbol: symbol, interval: interval) - params[:end] = params.delete(:end_) if params.key?(:end_) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/market/premium-index-price-kline', params: params) end From 06facfdc0e14186054568f24ed098aa039e43000 Mon Sep 17 00:00:00 2001 From: doris-xiao-bybit Date: Tue, 21 Jul 2026 18:20:43 +0800 Subject: [PATCH 18/18] update version --- CHANGELOG.md | 3 ++- lib/bybit/version.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcfabc4..94649e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.0] - 2026-07-20 + +## [0.1.1] - 2026-07-22 ### Added - `AccountService#batch_set_collateral(...)` diff --git a/lib/bybit/version.rb b/lib/bybit/version.rb index e10b1aa..90323fa 100644 --- a/lib/bybit/version.rb +++ b/lib/bybit/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Bybit - VERSION = '0.1.0' + VERSION = '0.1.1' end