mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
feat(binance): add full account sync and transaction processing (#1822)
* feat(binance): add full account sync and transaction processing - Fixed a bug that hindered Account setup - Wire up Binance accounts, sync statistics, and unlinked account tracking in the accounts dashboard. - Support setting a sync_start_date during Binance account setup. - Set Binance accounts' opening balance to zero to ensure the ledger builds cleanly from the actual trade history. - Expand the Binance importer and processor to handle Spot, Margin, Earn, P2P, and Futures trades and assets. - Implement TransactionBuilder to parse raw Binance trades, accurately calculating fees, base/quote asset amounts, and market values for proper ledger integration. - Update Binance API timeout (`recvWindow`) to 60,000ms to prevent connection drops. These changes provide comprehensive support for tracking Binance portfolios, ensuring accurate historical ledgers and proper visibility of sync statuses in the frontend dashboard. * refactor(binance): enforce strong params, double-entry safety, and native fiat currency support - Implement strong parameters in BinanceItemsController#complete_account_setup to satisfy Rails security guidelines. - Add robust date parsing with a grace fallback to prevent controller crashes on malformed sync start dates. - Wrap P2P transaction creations inside a database transaction block to guarantee ledger integrity and prevent orphan records. - Optimize P2P deduplication queries by batching checks for both transaction and funding external IDs. - Shift P2P entry persistence from forced USD tracking to native fiat values extracted directly from the Binance API payload. - Update BinanceAccount::ProcessorTest assertions and fixtures to validate native fiat and fee calculation logic. * fix(binance): process sync trades before caching transaction payload - Reorder Binance processor execution to insert trade records into the database prior to updating the `raw_transactions_payload` cache. This guarantees that if a database insertion fails, the cache won't prematurely mark the sync as successful, ensuring the data is retried on the next run. - Move `set_opening_anchor_balance(balance: 0)` out of the generic crypto exchange account builder and apply it specifically during Binance account creation. - Refactor date parsing in BinanceItemsController to explicitly catch `ArgumentError` via a block instead of using a blanket inline `rescue`. - Clean up the `setup_accounts` view template by removing hardcoded default translation strings. * fix(binance): enhance trade sync logic and error propagation - Pass `startTime` (from `sync_start_date`) to spot and futures trade endpoints on initial sync to optimize data fetching. - Include previously synced futures pairs alongside spot pairs when resolving relevant symbols to properly recover sold-out assets. - Re-raise exceptions in processor rescue blocks to prevent silent failures and ensure errors are correctly propagated to background jobs. - Decrease Binance API `recvWindow` from 60000ms to 5000ms to align with recommended default timeout values.
This commit is contained in:
@@ -86,4 +86,120 @@ class BinanceAccount::ProcessorTest < ActiveSupport::TestCase
|
||||
assert_equal "USD", @account.currency
|
||||
assert_in_delta 1000.0, @account.balance, 0.01
|
||||
end
|
||||
|
||||
test "processes futures trades correctly" do
|
||||
@family.update!(currency: "USD")
|
||||
@ba.update!(raw_payload: { "assets" => [ { "symbol" => "BTC", "total" => "1.0" } ] })
|
||||
|
||||
provider = mock
|
||||
@item.stubs(:binance_provider).returns(provider)
|
||||
@ba.stubs(:binance_item).returns(@item)
|
||||
provider.stubs(:get_spot_trades).returns([])
|
||||
provider.stubs(:get_spot_price).returns("50000.0")
|
||||
provider.stubs(:get_all_p2p_trades).returns([]) # Skip P2P
|
||||
|
||||
# Mock futures trades
|
||||
provider.stubs(:get_futures_trades).returns([])
|
||||
provider.stubs(:get_futures_trades).with("BTCUSDT", limit: 1000, from_id: nil, startTime: nil).returns([
|
||||
{ "id" => 1, "time" => 1610000000000, "qty" => "0.1", "price" => "40000.0", "quoteQty" => "4000.0", "commission" => "0.0", "commissionAsset" => "USDT", "buyer" => true }
|
||||
])
|
||||
|
||||
Security.create!(ticker: "CRYPTO:BTC", name: "Bitcoin", price_provider: "binance_public")
|
||||
|
||||
assert_difference "Entry.count", 1 do
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
end
|
||||
|
||||
assert @account.entries.exists?(external_id: "binance_futures_BTCUSDT_1")
|
||||
end
|
||||
|
||||
test "processes P2P BUY trades with double-entry logic and exact native fiat" do
|
||||
@family.update!(currency: "USD")
|
||||
@account.update!(currency: "USD")
|
||||
|
||||
provider = mock
|
||||
@item.stubs(:binance_provider).returns(provider)
|
||||
@ba.stubs(:binance_item).returns(@item)
|
||||
|
||||
# Silence other importers
|
||||
provider.stubs(:get_spot_trades).returns([])
|
||||
provider.stubs(:get_futures_trades).returns([])
|
||||
|
||||
# Mock the exact TZS/USDT payload with actual fiat transfer amounts
|
||||
provider.stubs(:get_all_p2p_trades).returns([
|
||||
{
|
||||
"orderNumber" => "22883918231657005056",
|
||||
"createTime" => 1777736533166,
|
||||
"tradeType" => "BUY",
|
||||
"asset" => "USDT",
|
||||
"fiat" => "TZS",
|
||||
"totalPrice" => "31500.00",
|
||||
"unitPrice" => "2746.29",
|
||||
"amount" => "11.47", # Gross crypto
|
||||
"takerAmount" => "11.41", # Net crypto
|
||||
"takerCommission" => "0.06" # Crypto fee
|
||||
}
|
||||
])
|
||||
|
||||
Security.create!(ticker: "CRYPTO:USDT", name: "Tether", price_provider: "binance_public")
|
||||
|
||||
# It MUST create 2 entries: 1 Deposit (Transaction) and 1 Purchase (Trade)
|
||||
assert_difference "Entry.count", 2 do
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
end
|
||||
|
||||
# Verify the Deposit (Transaction) - Should be native fiat
|
||||
deposit = @account.entries.find_by(external_id: "binance_p2p_22883918231657005056_funding")
|
||||
assert_not_nil deposit
|
||||
assert_equal "Transaction", deposit.entryable_type
|
||||
assert_equal (-31500.00), deposit.amount.to_f # Negative = Fiat Cash INFLOW
|
||||
assert_equal "TZS", deposit.currency
|
||||
|
||||
# Verify the Buy (Trade) - Should reflect the fiat cost basis
|
||||
trade = @account.entries.find_by(external_id: "binance_p2p_22883918231657005056")
|
||||
assert_not_nil trade
|
||||
assert_equal "Trade", trade.entryable_type
|
||||
assert_equal 31500.00, trade.amount.to_f # Positive = Fiat Cash OUTFLOW
|
||||
assert_equal "TZS", trade.currency
|
||||
assert_equal "Buy", trade.entryable.investment_activity_label
|
||||
|
||||
# Verify the specific crypto math and fiat fee conversion
|
||||
assert_equal 11.41, trade.entryable.qty.to_f
|
||||
|
||||
# Fiat Fee = Crypto Fee (0.06) * Unit Price (2746.29) = 164.7774 (rounds to 164.78)
|
||||
assert_equal 164.78, trade.entryable.fee.to_f
|
||||
end
|
||||
|
||||
test "skips processing if P2P external_id already exists" do
|
||||
@family.update!(currency: "USD")
|
||||
@account.update!(currency: "USD")
|
||||
|
||||
# Pre-create the trade in the database
|
||||
@account.entries.create!(
|
||||
date: Date.current,
|
||||
name: "Existing P2P",
|
||||
amount: 10,
|
||||
currency: "USD",
|
||||
external_id: "binance_p2p_existing_123",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
provider = mock
|
||||
@item.stubs(:binance_provider).returns(provider)
|
||||
@ba.stubs(:binance_item).returns(@item)
|
||||
provider.stubs(:get_spot_trades).returns([])
|
||||
provider.stubs(:get_futures_trades).returns([])
|
||||
|
||||
# Mock a payload with the SAME orderNumber
|
||||
provider.stubs(:get_all_p2p_trades).returns([
|
||||
{ "orderNumber" => "existing_123", "tradeType" => "BUY", "asset" => "USDT", "amount" => "10.0" }
|
||||
])
|
||||
|
||||
Security.create!(ticker: "CRYPTO:USDT", name: "Tether", price_provider: "binance_public")
|
||||
|
||||
# Assert that NO new entries are created
|
||||
assert_no_difference "Entry.count" do
|
||||
BinanceAccount::Processor.new(@ba).process
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user