From e39932a03c0aff93b2eeef73ab7816e7ab309fc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 15:40:50 +0000 Subject: [PATCH 1/5] feat: Add comprehensive Binance Testnet API support - Add TestnetWebSocket for testnet market streams (wss://stream.testnet.binance.vision:9443) - Add TestnetSpot.dispose() for proper resource cleanup - Add comprehensive market data endpoints: - ping(), getExchangeInfo() with filters - getUIKlines(), getTradingDayTicker() - getRollingWindowTicker() for rolling window stats - Proper weight tracking for all endpoints - Add advanced order types for testnet trading: - OCO (One-Cancels-Other) orders - OTO (One-Triggers-Other) orders - OTOCO (One-Triggers-OCO) orders - Cancel-Replace operations - Add testnet failover endpoints in BinanceBase (api1.testnet.binance.vision) - Add Demo Trading API support as alternative testnet: - DemoSpot, DemoTrading, DemoMarket classes - DemoFuturesUsd for futures demo trading - DemoWebSocket (wss://demo-stream.binance.com) - Base URL: https://demo-api.binance.com - Add Binance.demo() factory constructor - Update testnet example with new features documentation --- example/testnet_integration_example.dart | 444 +++++--- lib/src/babel_binance_base.dart | 86 +- lib/src/binance_base.dart | 8 + lib/src/testnet.dart | 1251 +++++++++++++++++++++- 4 files changed, 1542 insertions(+), 247 deletions(-) diff --git a/example/testnet_integration_example.dart b/example/testnet_integration_example.dart index 8a7ecc8..e70b75c 100644 --- a/example/testnet_integration_example.dart +++ b/example/testnet_integration_example.dart @@ -1,24 +1,29 @@ -/// πŸ§ͺ Babel Binance - Testnet Integration Example +/// Babel Binance - Testnet Integration Example /// /// This example demonstrates how to use Binance's official testnet /// for realistic testing without using real money. /// -/// 🌐 Testnet URL: https://testnet.binance.vision/ +/// Available Testnet Environments: +/// - Spot Testnet: https://testnet.binance.vision/ +/// - Demo Trading: https://demo-api.binance.com (alternative) +/// - Futures Testnet: https://testnet.binancefuture.com/ /// -/// ✨ Features covered: -/// βœ… Testnet API key setup -/// βœ… Market data from testnet -/// βœ… Real trading on testnet (no real money) -/// βœ… Futures trading on testnet -/// βœ… Comparison with simulated trading -/// βœ… Best practices for testing strategies +/// Features covered: +/// - Testnet API key setup +/// - Market data from testnet +/// - Real trading on testnet (no real money) +/// - OCO, OTO, OTOCO advanced orders +/// - WebSocket streaming on testnet +/// - Futures trading on testnet +/// - Demo Trading API (alternative testnet) +/// - Best practices for testing strategies import 'package:babel_binance/babel_binance.dart'; void main() async { - print('πŸ§ͺ Binance Testnet Integration Demo'); + print('Binance Testnet Integration Demo'); print('=' * 50); - print('🌐 Testing with real API endpoints but fake money!'); + print('Testing with real API endpoints but fake money!'); print(''); // Step 1: Setup (Important!) @@ -33,34 +38,37 @@ void main() async { // Step 4: Advanced testnet features await step4_AdvancedTestnetFeatures(); - // Step 5: Best practices - step5_BestPractices(); + // Step 5: Demo Trading API + await step5_DemoTradingApi(); + + // Step 6: Best practices + step6_BestPractices(); } -/// πŸ”‘ Step 1: Testnet Setup and API Keys +/// Step 1: Testnet Setup and API Keys Future step1_TestnetSetup() async { - print('πŸ”‘ STEP 1: Testnet Setup'); + print('STEP 1: Testnet Setup'); print('-' * 30); print(''); - print('πŸ“‹ To use the testnet, you need to:'); + print('To use the testnet, you need to:'); print(''); - print('1. 🌐 Visit: https://testnet.binance.vision/'); - print('2. πŸ” Create a testnet account (separate from main Binance)'); - print('3. πŸ—οΈ Generate API keys in the testnet interface'); - print('4. πŸ’° Get free test funds (automatically provided)'); + print('1. Visit: https://testnet.binance.vision/'); + print('2. Create a testnet account (separate from main Binance)'); + print('3. Generate API keys in the testnet interface'); + print('4. Get free test funds (automatically provided)'); print(''); - print('⚠️ IMPORTANT: Testnet API keys are different from live keys!'); - print(' 🚫 Never use live API keys with testnet endpoints'); - print(' 🚫 Never use testnet API keys with live endpoints'); + print('IMPORTANT: Testnet API keys are different from live keys!'); + print(' - Never use live API keys with testnet endpoints'); + print(' - Never use testnet API keys with live endpoints'); print(''); - print('πŸ”§ For this demo, we\'ll show how it works without real keys...'); + print('For this demo, we\'ll show how it works without real keys...'); print(''); await _pause(); } -/// πŸš€ Step 2: Basic Testnet Usage +/// Step 2: Basic Testnet Usage Future step2_BasicTestnetUsage() async { - print('πŸš€ STEP 2: Basic Testnet Usage'); + print('STEP 2: Basic Testnet Usage'); print('-' * 30); print(''); print('Let\'s compare regular API calls vs testnet calls...'); @@ -70,13 +78,13 @@ Future step2_BasicTestnetUsage() async { final binance = Binance(); // Regular production API call - print('πŸ“Š Getting market data from PRODUCTION API...'); + print('Getting market data from PRODUCTION API...'); final prodTicker = await binance.spot.market.get24HrTicker('BTCUSDT'); final prodPrice = double.parse(prodTicker['lastPrice']); final prodChange = double.parse(prodTicker['priceChangePercent']); - print(' πŸ’° Production BTC Price: \$${prodPrice.toStringAsFixed(2)}'); - print(' πŸ“ˆ Production 24h Change: ${prodChange.toStringAsFixed(2)}%'); + print(' Production BTC Price: \$${prodPrice.toStringAsFixed(2)}'); + print(' Production 24h Change: ${prodChange.toStringAsFixed(2)}%'); print(''); // Note: For actual testnet usage, you would do this: @@ -86,40 +94,40 @@ Future step2_BasicTestnetUsage() async { apiKey: 'your_testnet_api_key', apiSecret: 'your_testnet_secret', ); - - print('πŸ§ͺ Getting market data from TESTNET API...'); + + print('Getting market data from TESTNET API...'); final testTicker = await testnetBinance.testnetSpot.market.get24HrTicker('BTCUSDT'); final testPrice = double.parse(testTicker['lastPrice']); final testChange = double.parse(testTicker['priceChangePercent']); - - print(' πŸ’° Testnet BTC Price: \$${testPrice.toStringAsFixed(2)}'); - print(' πŸ“ˆ Testnet 24h Change: ${testChange.toStringAsFixed(2)}%'); + + print(' Testnet BTC Price: \$${testPrice.toStringAsFixed(2)}'); + print(' Testnet 24h Change: ${testChange.toStringAsFixed(2)}%'); */ - print('πŸ§ͺ TESTNET DEMO (simulated data):'); + print('TESTNET DEMO (simulated data):'); print( - ' πŸ’° Testnet BTC Price: \$${(prodPrice * 0.98).toStringAsFixed(2)} (test data)'); + ' Testnet BTC Price: \$${(prodPrice * 0.98).toStringAsFixed(2)} (test data)'); print( - ' πŸ“ˆ Testnet 24h Change: ${(prodChange * 1.1).toStringAsFixed(2)}% (test data)'); + ' Testnet 24h Change: ${(prodChange * 1.1).toStringAsFixed(2)}% (test data)'); print(''); - print('πŸ” Key Differences:'); + print('Key Differences:'); print( - ' 🌐 Different endpoints: testnet.binance.vision vs api.binance.com'); - print(' πŸ’° Test data vs real market data'); - print(' πŸ”‘ Separate API keys required'); - print(' πŸ’Έ No real money involved'); + ' - Different endpoints: testnet.binance.vision vs api.binance.com'); + print(' - Test data vs real market data'); + print(' - Separate API keys required'); + print(' - No real money involved'); } catch (e) { - print('❌ Error: $e'); - print('πŸ’‘ This might be a network issue or rate limiting.'); + print('Error: $e'); + print('This might be a network issue or rate limiting.'); } print(''); await _pause(); } -/// βš–οΈ Step 3: Trading Methods Comparison +/// Step 3: Trading Methods Comparison Future step3_TradingComparison() async { - print('βš–οΈ STEP 3: Trading Methods Comparison'); + print('STEP 3: Trading Methods Comparison'); print('-' * 30); print(''); print('Let\'s compare the three trading methods available:'); @@ -129,14 +137,14 @@ Future step3_TradingComparison() async { try { // Method 1: Simulated Trading (built-in simulation) - print('🎯 Method 1: SIMULATED TRADING'); - print(' πŸ“‹ Description: Built-in simulation with mock data'); - print(' πŸ’° Cost: Free'); - print(' πŸ”‘ API Keys: Not required'); - print(' 🌐 Network: Local simulation'); + print('Method 1: SIMULATED TRADING'); + print(' Description: Built-in simulation with mock data'); + print(' Cost: Free'); + print(' API Keys: Not required'); + print(' Network: Local simulation'); print(''); - print(' πŸ›’ Simulating buy order...'); + print(' Simulating buy order...'); final simOrder = await binance.spot.simulatedTrading.simulatePlaceOrder( symbol: 'BTCUSDT', side: 'BUY', @@ -147,199 +155,281 @@ Future step3_TradingComparison() async { final simQty = double.parse(simOrder['executedQty']); final simCost = double.parse(simOrder['cummulativeQuoteQty']); print( - ' βœ… Simulated: Bought ${simQty} BTC for \$${simCost.toStringAsFixed(2)}'); + ' Simulated: Bought ${simQty} BTC for \$${simCost.toStringAsFixed(2)}'); print(''); // Method 2: Testnet Trading (real API with test money) - print('πŸ§ͺ Method 2: TESTNET TRADING'); - print(' πŸ“‹ Description: Real Binance API with test money'); - print(' πŸ’° Cost: Free (test funds provided)'); - print(' πŸ”‘ API Keys: Testnet keys required'); - print(' 🌐 Network: Real API calls to testnet.binance.vision'); + print('Method 2: TESTNET TRADING'); + print(' Description: Real Binance API with test money'); + print(' Cost: Free (test funds provided)'); + print(' API Keys: Testnet keys required'); + print(' Network: Real API calls to testnet.binance.vision'); print(''); // Note: This would require real testnet API keys - print(' πŸ›’ Would place real order on testnet...'); - print(' βœ… Example: Buy 0.001 BTC with real testnet API'); - print(' πŸ“Š Real order status tracking'); - print(' πŸ“ˆ Real market data (but test environment)'); + print(' Would place real order on testnet...'); + print(' Example: Buy 0.001 BTC with real testnet API'); + print(' Real order status tracking'); + print(' Real market data (but test environment)'); print(''); // Method 3: Live Trading (real money - not demonstrated) - print('πŸ’Έ Method 3: LIVE TRADING'); - print(' πŸ“‹ Description: Real Binance API with real money'); - print(' πŸ’° Cost: Real money at risk'); - print(' πŸ”‘ API Keys: Live production keys required'); - print(' 🌐 Network: Real API calls to api.binance.com'); - print(' ⚠️ NOT demonstrated in this example!'); + print('Method 3: LIVE TRADING'); + print(' Description: Real Binance API with real money'); + print(' Cost: Real money at risk'); + print(' API Keys: Live production keys required'); + print(' Network: Real API calls to api.binance.com'); + print(' [NOT demonstrated in this example!]'); print(''); // Comparison table - print('πŸ“Š COMPARISON TABLE:'); - print(' β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); - print(' β”‚ Feature β”‚ Simulated β”‚ Testnet β”‚ Live β”‚'); - print(' β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€'); - print(' β”‚ Real API calls β”‚ ❌ No β”‚ βœ… Yes β”‚ βœ… Yes β”‚'); - print(' β”‚ Real money β”‚ ❌ No β”‚ ❌ No β”‚ βœ… Yes β”‚'); - print(' β”‚ API keys needed β”‚ ❌ No β”‚ βœ… Yes β”‚ βœ… Yes β”‚'); - print(' β”‚ Order matching β”‚ 🎯 Simulatedβ”‚ βœ… Real β”‚ βœ… Real β”‚'); - print(' β”‚ Market data β”‚ 🎯 Mock β”‚ βœ… Real β”‚ βœ… Real β”‚'); - print(' β”‚ Network latency β”‚ ❌ None β”‚ βœ… Real β”‚ βœ… Real β”‚'); - print(' β”‚ Rate limits β”‚ ❌ None β”‚ βœ… Real β”‚ βœ… Real β”‚'); - print(' β”‚ Best for β”‚ πŸŽ“ Learning β”‚ πŸ§ͺ Testing β”‚ πŸ’° Trading β”‚'); - print(' β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); + print('COMPARISON TABLE:'); + print(' +------------------+-------------+-------------+-------------+'); + print(' | Feature | Simulated | Testnet | Live |'); + print(' +------------------+-------------+-------------+-------------+'); + print(' | Real API calls | No | Yes | Yes |'); + print(' | Real money | No | No | Yes |'); + print(' | API keys needed | No | Yes | Yes |'); + print(' | Order matching | Simulated | Real | Real |'); + print(' | Market data | Mock | Real | Real |'); + print(' | Network latency | None | Real | Real |'); + print(' | Rate limits | None | Real | Real |'); + print(' | Best for | Learning | Testing | Trading |'); + print(' +------------------+-------------+-------------+-------------+'); } catch (e) { - print('❌ Error in trading comparison: $e'); + print('Error in trading comparison: $e'); } print(''); await _pause(); } -/// πŸš€ Step 4: Advanced Testnet Features +/// Step 4: Advanced Testnet Features Future step4_AdvancedTestnetFeatures() async { - print('πŸš€ STEP 4: Advanced Testnet Features'); + print('STEP 4: Advanced Testnet Features'); print('-' * 30); print(''); print('The testnet supports almost all features of the live API:'); print(''); - print('πŸ“Š SPOT TRADING FEATURES:'); - print(' βœ… Market orders (immediate execution)'); - print(' βœ… Limit orders (pending until price reached)'); - print(' βœ… Stop-loss orders'); - print(' βœ… OCO (One-Cancels-Other) orders'); - print(' βœ… Account balance tracking'); - print(' βœ… Trade history'); - print(' βœ… Open orders management'); + print('SPOT TRADING FEATURES:'); + print(' - Market orders (immediate execution)'); + print(' - Limit orders (pending until price reached)'); + print(' - Stop-loss orders'); + print(' - OCO (One-Cancels-Other) orders'); + print(' - OTO (One-Triggers-Other) orders'); + print(' - OTOCO (One-Triggers-OCO) orders'); + print(' - Cancel-Replace operations'); + print(' - Account balance tracking'); + print(' - Trade history'); + print(''); + + print('FUTURES TRADING FEATURES:'); + print(' - Long and short positions'); + print(' - Leverage up to 125x'); + print(' - Margin management'); + print(' - Position sizing'); + print(' - Liquidation simulation'); + print(' - Funding rates'); + print(''); + + print('WEBSOCKET FEATURES:'); + print(' - Real-time price updates'); + print(' - Order book streams'); + print(' - User data streams'); + print(' - Trade streams'); + print(' - Kline/candlestick streams'); + print(' - Testnet WebSocket: wss://stream.testnet.binance.vision:9443'); + print(''); + + print('MARKET DATA ENDPOINTS:'); + print(' - getServerTime(), ping()'); + print(' - getExchangeInfo()'); + print(' - getOrderBook(), getRecentTrades()'); + print(' - getKlines(), getUIKlines()'); + print(' - get24HrTicker(), getTickerPrice()'); + print(' - getRollingWindowTicker()'); + print(' - getTradingDayTicker()'); + print(''); + + // Code example for advanced orders + print('EXAMPLE: OCO ORDER ON TESTNET'); + print('```dart'); + print('// Initialize testnet'); + print('final binance = Binance.testnet('); + print(' apiKey: \'testnet_key\','); + print(' apiSecret: \'testnet_secret\','); + print(');'); + print(''); + print('// Place OCO order (profit target + stop loss)'); + print('final ocoOrder = await binance.testnetSpot.trading.placeOcoOrder('); + print(' symbol: \'BTCUSDT\','); + print(' side: \'SELL\','); + print(' quantity: 0.001,'); + print(' price: 100000.0, // Take profit price'); + print(' stopPrice: 90000.0, // Stop loss trigger'); + print(' stopLimitPrice: 89900.0,'); + print(');'); + print('```'); print(''); - print('πŸš€ FUTURES TRADING FEATURES:'); - print(' βœ… Long and short positions'); - print(' βœ… Leverage up to 125x'); - print(' βœ… Margin management'); - print(' βœ… Position sizing'); - print(' βœ… Liquidation simulation'); - print(' βœ… Funding rates'); + print('EXAMPLE: WEBSOCKET STREAMING ON TESTNET'); + print('```dart'); + print('// Connect to testnet WebSocket'); + print('final binance = Binance.testnet(...);'); + print(''); + print('// Create user data stream'); + print('final listenKey = await binance.testnetSpot.userDataStream.createListenKey();'); print(''); + print('// Connect to WebSocket'); + print('final stream = binance.testnetSpot.webSocket.connectUserDataStream('); + print(' listenKey[\'listenKey\'],'); + print(');'); + print(''); + print('stream.listen((data) {'); + print(' print(\'Account update: \$data\');'); + print('});'); + print('```'); + print(''); + await _pause(); +} - print('πŸ”„ WEBSOCKET FEATURES:'); - print(' βœ… Real-time price updates'); - print(' βœ… Order book streams'); - print(' βœ… User data streams (account updates)'); - print(' βœ… Trade streams'); - print(' βœ… Kline/candlestick streams'); +/// Step 5: Demo Trading API +Future step5_DemoTradingApi() async { + print('STEP 5: Demo Trading API (Alternative Testnet)'); + print('-' * 30); + print(''); + print('Demo Trading is an alternative to testnet.binance.vision'); + print('Use this if testnet.binance.vision is not accessible in your region.'); print(''); - print('πŸ“ˆ MARKET DATA:'); - print(' βœ… Real-time prices (test environment)'); - print(' βœ… Historical klines/candlesticks'); - print(' βœ… Trade history'); - print(' βœ… Order book depth'); - print(' βœ… 24hr statistics'); + print('DEMO TRADING ENDPOINTS:'); + print(' REST API: https://demo-api.binance.com'); + print(' WebSocket: wss://demo-stream.binance.com'); + print(' Futures REST: https://demo-fapi.binance.com'); print(''); - // Code example for testnet usage - print('πŸ’» EXAMPLE TESTNET CODE:'); + print('HOW TO GET DEMO API KEYS:'); + print(' 1. Log in to your Binance account'); + print(' 2. Go to Account Settings > API Management'); + print(' 3. Create a Demo Trading API key'); + print(' 4. Use these keys with the Demo API endpoints'); print(''); + + print('EXAMPLE: USING DEMO TRADING API'); print('```dart'); - print('// Initialize with testnet credentials'); - print('final binance = Binance.testnet('); - print(' apiKey: \'your_testnet_api_key\','); - print(' apiSecret: \'your_testnet_secret\','); + print('// Initialize with demo trading'); + print('final binance = Binance.demo('); + print(' apiKey: \'demo_api_key\','); + print(' apiSecret: \'demo_api_secret\','); print(');'); print(''); - print('// Get testnet market data'); - print( - 'final ticker = await binance.testnetSpot.market.get24HrTicker(\'BTCUSDT\');'); + print('// Use demo spot trading'); + print('final ticker = await binance.demoSpot.market.get24HrTicker(\'BTCUSDT\');'); print(''); - print('// Place testnet order'); - print('final order = await binance.testnetSpot.trading.placeOrder('); + print('// Place order on demo'); + print('final order = await binance.demoSpot.trading.placeOrder('); print(' symbol: \'BTCUSDT\','); print(' side: \'BUY\','); print(' type: \'MARKET\','); print(' quantity: 0.001,'); print(');'); print(''); - print('// Check testnet account'); - print('final account = await binance.testnetSpot.trading.getAccountInfo();'); + print('// Demo futures trading'); + print('final position = await binance.demoFutures.trading.getPositionInfo();'); print('```'); print(''); + + print('TESTNET vs DEMO TRADING:'); + print(' +------------------+-------------------------+-------------------------+'); + print(' | Feature | Testnet | Demo Trading |'); + print(' +------------------+-------------------------+-------------------------+'); + print(' | URL | testnet.binance.vision | demo-api.binance.com |'); + print(' | API Keys | Separate testnet keys | From main account |'); + print(' | Registration | Separate account | Use main account |'); + print(' | Availability | May be geo-restricted | More widely available |'); + print(' | Funds | Auto-provided | Auto-provided |'); + print(' +------------------+-------------------------+-------------------------+'); + print(''); await _pause(); } -/// πŸ’‘ Step 5: Best Practices -void step5_BestPractices() { - print('πŸ’‘ STEP 5: Best Practices for Testing'); +/// Step 6: Best Practices +void step6_BestPractices() { + print('STEP 6: Best Practices for Testing'); print('-' * 30); print(''); - print('🎯 TESTING STRATEGY PROGRESSION:'); - print(''); - print('1. πŸŽ“ START WITH SIMULATED TRADING'); - print(' β€’ Learn the API structure'); - print(' β€’ Test your logic flow'); - print(' β€’ No network dependencies'); - print(' β€’ Instant feedback'); - print(''); - print('2. πŸ§ͺ MOVE TO TESTNET'); - print(' β€’ Test with real API calls'); - print(' β€’ Experience real latency'); - print(' β€’ Handle real error responses'); - print(' β€’ Test rate limiting'); - print(''); - print('3. πŸ“Š PAPER TRADING (if available)'); - print(' β€’ Real market data'); - print(' β€’ Real-time execution simulation'); - print(' β€’ Track performance metrics'); - print(''); - print('4. πŸ’° LIVE TRADING (small amounts)'); - print(' β€’ Start with minimal capital'); - print(' β€’ Monitor closely'); - print(' β€’ Scale up gradually'); - print(''); - print('πŸ›‘οΈ SAFETY GUIDELINES:'); - print(''); - print('βœ… DO:'); - print(' β€’ Test thoroughly on testnet first'); - print(' β€’ Use environment variables for API keys'); - print(' β€’ Implement proper error handling'); - print(' β€’ Set up monitoring and alerts'); - print(' β€’ Keep detailed logs'); - print(' β€’ Start with small amounts'); - print(''); - print('❌ DON\'T:'); - print(' β€’ Skip testing phases'); - print(' β€’ Hard-code API keys'); - print(' β€’ Ignore error responses'); - print(' β€’ Trade without stop-losses'); - print(' β€’ Risk more than you can afford'); - print(' β€’ Mix testnet and live credentials'); - print(''); - print('πŸ”§ ENVIRONMENT SETUP:'); + print('TESTING STRATEGY PROGRESSION:'); + print(''); + print('1. START WITH SIMULATED TRADING'); + print(' - Learn the API structure'); + print(' - Test your logic flow'); + print(' - No network dependencies'); + print(' - Instant feedback'); + print(''); + print('2. MOVE TO TESTNET/DEMO'); + print(' - Test with real API calls'); + print(' - Experience real latency'); + print(' - Handle real error responses'); + print(' - Test rate limiting'); + print(''); + print('3. PAPER TRADING (if available)'); + print(' - Real market data'); + print(' - Real-time execution simulation'); + print(' - Track performance metrics'); + print(''); + print('4. LIVE TRADING (small amounts)'); + print(' - Start with minimal capital'); + print(' - Monitor closely'); + print(' - Scale up gradually'); + print(''); + print('SAFETY GUIDELINES:'); + print(''); + print('DO:'); + print(' - Test thoroughly on testnet first'); + print(' - Use environment variables for API keys'); + print(' - Implement proper error handling'); + print(' - Set up monitoring and alerts'); + print(' - Keep detailed logs'); + print(' - Start with small amounts'); + print(''); + print('DO NOT:'); + print(' - Skip testing phases'); + print(' - Hard-code API keys'); + print(' - Ignore error responses'); + print(' - Trade without stop-losses'); + print(' - Risk more than you can afford'); + print(' - Mix testnet and live credentials'); + print(''); + print('ENVIRONMENT SETUP:'); print(''); print('```bash'); print('# Development environment variables'); print('export BINANCE_TESTNET_API_KEY="your_testnet_key"'); print('export BINANCE_TESTNET_SECRET="your_testnet_secret"'); print(''); + print('# Demo trading environment variables'); + print('export BINANCE_DEMO_API_KEY="your_demo_key"'); + print('export BINANCE_DEMO_SECRET="your_demo_secret"'); + print(''); print('# Production environment variables (separate!)'); print('export BINANCE_API_KEY="your_live_key"'); print('export BINANCE_SECRET_KEY="your_live_secret"'); print('```'); print(''); - print('πŸ“š ADDITIONAL RESOURCES:'); + print('ADDITIONAL RESOURCES:'); print(''); - print('🌐 Testnet: https://testnet.binance.vision/'); - print('πŸ“– API Docs: https://binance-docs.github.io/apidocs/'); - print('πŸ§ͺ WebSocket Test: wss://testnet.binance.vision/ws/'); - print('πŸ“Š Futures Testnet: https://testnet.binancefuture.com/'); + print('Testnet: https://testnet.binance.vision/'); + print('Demo Trading: https://demo-api.binance.com'); + print('API Docs: https://developers.binance.com/docs/'); + print('Futures Testnet: https://testnet.binancefuture.com/'); print(''); - print('πŸŽ‰ Happy testing! Remember: Test first, trade smart! πŸš€'); + print('Happy testing! Remember: Test first, trade smart!'); } // Helper function to pause between steps Future _pause() async { - print('⏳ (Pausing for 3 seconds...)'); + print('(Pausing for 3 seconds...)'); await Future.delayed(Duration(seconds: 3)); print(''); } diff --git a/lib/src/babel_binance_base.dart b/lib/src/babel_binance_base.dart index 5babc5f..1ec5d90 100644 --- a/lib/src/babel_binance_base.dart +++ b/lib/src/babel_binance_base.dart @@ -6,6 +6,35 @@ import './testnet.dart'; import './config/binance_config.dart'; import './logging/logger.dart'; +/// Main entry point for the Binance API wrapper. +/// +/// Provides access to all Binance API endpoints including: +/// - Spot trading (production) +/// - Futures USD-M trading (production) +/// - Margin trading (production) +/// - Testnet APIs (testnet.binance.vision) +/// - Demo Trading APIs (demo-api.binance.com) +/// +/// Example usage: +/// ```dart +/// // Production trading +/// final binance = Binance( +/// apiKey: 'your-api-key', +/// apiSecret: 'your-api-secret', +/// ); +/// +/// // Testnet trading +/// final testnet = Binance.testnet( +/// apiKey: 'testnet-api-key', +/// apiSecret: 'testnet-api-secret', +/// ); +/// +/// // Demo trading (alternative testnet) +/// final demo = Binance.demo( +/// apiKey: 'demo-api-key', +/// apiSecret: 'demo-api-secret', +/// ); +/// ``` class Binance { final Spot spot; final SimulatedConvert simulatedConvert; @@ -13,6 +42,8 @@ class Binance { final Margin margin; final TestnetSpot testnetSpot; final TestnetFuturesUsd testnetFutures; + final DemoSpot demoSpot; + final DemoFuturesUsd demoFutures; final BinanceConfig config; final BinanceLogger logger; @@ -21,21 +52,26 @@ class Binance { String? apiSecret, BinanceConfig? config, BinanceLogger? logger, - }) : config = config ?? BinanceConfig.defaultConfig, - logger = logger ?? const NoOpLogger(), - spot = Spot(apiKey: apiKey, apiSecret: apiSecret), - simulatedConvert = - SimulatedConvert(apiKey: apiKey, apiSecret: apiSecret), - futuresUsd = FuturesUsd(apiKey: apiKey, apiSecret: apiSecret), - margin = Margin(apiKey: apiKey, apiSecret: apiSecret), - testnetSpot = TestnetSpot(apiKey: apiKey, apiSecret: apiSecret), - testnetFutures = - TestnetFuturesUsd(apiKey: apiKey, apiSecret: apiSecret); + }) : config = config ?? BinanceConfig.defaultConfig, + logger = logger ?? const NoOpLogger(), + spot = Spot(apiKey: apiKey, apiSecret: apiSecret), + simulatedConvert = + SimulatedConvert(apiKey: apiKey, apiSecret: apiSecret), + futuresUsd = FuturesUsd(apiKey: apiKey, apiSecret: apiSecret), + margin = Margin(apiKey: apiKey, apiSecret: apiSecret), + testnetSpot = TestnetSpot(apiKey: apiKey, apiSecret: apiSecret), + testnetFutures = TestnetFuturesUsd(apiKey: apiKey, apiSecret: apiSecret), + demoSpot = DemoSpot(apiKey: apiKey, apiSecret: apiSecret), + demoFutures = DemoFuturesUsd(apiKey: apiKey, apiSecret: apiSecret); /// Create a Binance instance specifically configured for testnet /// - /// Use this when you want to test with real API endpoints but test data + /// Use this when you want to test with real API endpoints but test data. /// Get your testnet API keys from: https://testnet.binance.vision/ + /// + /// Available endpoints: + /// - REST API: https://testnet.binance.vision/api + /// - WebSocket: wss://stream.testnet.binance.vision:9443 factory Binance.testnet({ required String apiKey, required String apiSecret, @@ -50,13 +86,35 @@ class Binance { ); } + /// Create a Binance instance specifically configured for Demo Trading + /// + /// Use this when testnet.binance.vision is not accessible in your region. + /// Get your demo API keys from your Binance account settings. + /// + /// Available endpoints: + /// - REST API: https://demo-api.binance.com + /// - WebSocket: wss://demo-stream.binance.com + factory Binance.demo({ + required String apiKey, + required String apiSecret, + BinanceConfig? config, + BinanceLogger? logger, + }) { + return Binance( + apiKey: apiKey, + apiSecret: apiSecret, + config: config, + logger: logger, + ); + } + /// Dispose and clean up resources - void dispose() { + Future dispose() async { spot.market.dispose(); futuresUsd.dispose(); margin.dispose(); - // Note: TestnetSpot and TestnetFuturesUsd don't have dispose methods - // as they are composed of sub-classes that handle their own cleanup + await testnetSpot.dispose(); + await demoSpot.dispose(); } } diff --git a/lib/src/binance_base.dart b/lib/src/binance_base.dart index bc3807c..fb42be2 100644 --- a/lib/src/binance_base.dart +++ b/lib/src/binance_base.dart @@ -131,6 +131,14 @@ class BinanceBase { } } + // Testnet Spot API endpoints with failover + if (host == 'testnet.binance.vision') { + return [ + '$scheme://testnet.binance.vision$port$path', + '$scheme://api1.testnet.binance.vision$port$path', + ]; + } + // For non-Binance domains or unrecognized patterns, return original URL return [baseUrl]; } diff --git a/lib/src/testnet.dart b/lib/src/testnet.dart index fb05d95..40d5a5f 100644 --- a/lib/src/testnet.dart +++ b/lib/src/testnet.dart @@ -1,27 +1,73 @@ import 'binance_base.dart'; +import 'websocket/websocket_client.dart'; +import 'websocket/websocket_config.dart'; +import 'websocket/stream_types.dart'; /// Binance Testnet integration for realistic testing without real money /// /// The testnet provides: /// - Real API endpoints with test data -/// - WebSocket connections +/// - WebSocket connections for market streams /// - All trading functionalities /// - No real money involved /// +/// Available Base URLs: +/// - REST API: https://testnet.binance.vision/api +/// - REST API (Failover): https://api1.testnet.binance.vision/api +/// - WebSocket API: wss://ws-api.testnet.binance.vision/ws-api/v3 +/// - Market Streams: wss://stream.testnet.binance.vision/stream +/// /// Get testnet API keys from: https://testnet.binance.vision/ class TestnetSpot { final TestnetMarket market; final TestnetTrading trading; final TestnetUserDataStream userDataStream; + final TestnetWebSocket webSocket; TestnetSpot({String? apiKey, String? apiSecret}) : market = TestnetMarket(apiKey: apiKey, apiSecret: apiSecret), trading = TestnetTrading(apiKey: apiKey, apiSecret: apiSecret), userDataStream = - TestnetUserDataStream(apiKey: apiKey, apiSecret: apiSecret); + TestnetUserDataStream(apiKey: apiKey, apiSecret: apiSecret), + webSocket = TestnetWebSocket(); + + /// Dispose and clean up resources + Future dispose() async { + market.dispose(); + trading.dispose(); + userDataStream.dispose(); + await webSocket.dispose(); + } +} + +/// WebSocket client for Binance Testnet market streams +/// +/// Base URLs: +/// - Market Streams: wss://stream.testnet.binance.vision:9443 +/// - WebSocket API: wss://ws-api.testnet.binance.vision:9443/ws-api/v3 +class TestnetWebSocket extends BinanceWebSocket { + TestnetWebSocket({WebSocketConfig? config}) + : super( + baseUrl: 'wss://stream.testnet.binance.vision:9443', + config: config, + ); + + /// Alternative constructor using port 443 (for restricted networks) + factory TestnetWebSocket.port443({WebSocketConfig? config}) { + return TestnetWebSocket._( + baseUrl: 'wss://stream.testnet.binance.vision:443', + config: config, + ); + } + + TestnetWebSocket._({required String baseUrl, WebSocketConfig? config}) + : super(baseUrl: baseUrl, config: config); } /// Market data endpoints for Binance Testnet +/// +/// All market data endpoints from the Binance Spot API are available on testnet. +/// Base URL: https://testnet.binance.vision/api class TestnetMarket extends BinanceBase { TestnetMarket({String? apiKey, String? apiSecret}) : super( @@ -30,48 +76,66 @@ class TestnetMarket extends BinanceBase { baseUrl: 'https://testnet.binance.vision', ); - /// Get server time from testnet + // ==================== General Endpoints ==================== + + /// Test connectivity to the Rest API (Weight: 1) + Future> ping() { + return sendRequest('GET', '/api/v3/ping', weight: 1); + } + + /// Get server time from testnet (Weight: 1) Future> getServerTime() { - return sendRequest('GET', '/api/v3/time'); + return sendRequest('GET', '/api/v3/time', weight: 1); } - /// Get exchange information from testnet - Future> getExchangeInfo() { - return sendRequest('GET', '/api/v3/exchangeInfo'); + /// Get exchange information from testnet (Weight: 20) + /// + /// Returns current trading rules and symbol information + Future> getExchangeInfo({ + String? symbol, + List? symbols, + List? permissions, + }) { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (symbols != null) params['symbols'] = symbols; + if (permissions != null) params['permissions'] = permissions; + + return sendRequest('GET', '/api/v3/exchangeInfo', params: params, weight: 20); } - /// Get order book depth from testnet + // ==================== Market Data Endpoints ==================== + + /// Get order book depth from testnet (Weight: 5-50 depending on limit) + /// + /// [limit] Valid limits: 5, 10, 20, 50, 100, 500, 1000, 5000 Future> getOrderBook(String symbol, {int limit = 100}) { + final weight = limit <= 100 ? 5 : (limit <= 500 ? 10 : (limit <= 1000 ? 20 : 50)); return sendRequest('GET', '/api/v3/depth', - params: {'symbol': symbol, 'limit': limit}); - } - - /// Get 24hr ticker statistics from testnet - Future> get24HrTicker(String symbol) { - return sendRequest('GET', '/api/v3/ticker/24hr', - params: {'symbol': symbol}); + params: {'symbol': symbol, 'limit': limit}, weight: weight); } - /// Get recent trades list from testnet - Future> getRecentTrades(String symbol, - {int limit = 500}) async { + /// Get recent trades list from testnet (Weight: 10) + Future> getRecentTrades(String symbol, {int limit = 500}) async { final response = await sendRequest('GET', '/api/v3/trades', - params: {'symbol': symbol, 'limit': limit}); + params: {'symbol': symbol, 'limit': limit}, weight: 10); return response as List; } - /// Get historical trades from testnet + /// Get historical trades from testnet (Weight: 10) + /// + /// Requires API key. Market trades from a more distant past. Future> getHistoricalTrades(String symbol, {int limit = 500, int? fromId}) async { final params = {'symbol': symbol, 'limit': limit}; if (fromId != null) params['fromId'] = fromId; - final response = - await sendRequest('GET', '/api/v3/historicalTrades', params: params); + final response = await sendRequest('GET', '/api/v3/historicalTrades', + params: params, weight: 10); return response as List; } - /// Get compressed/aggregate trades from testnet + /// Get compressed/aggregate trades from testnet (Weight: 2) Future> getAggTrades( String symbol, { int? fromId, @@ -85,16 +149,19 @@ class TestnetMarket extends BinanceBase { if (endTime != null) params['endTime'] = endTime; final response = - await sendRequest('GET', '/api/v3/aggTrades', params: params); + await sendRequest('GET', '/api/v3/aggTrades', params: params, weight: 2); return response as List; } - /// Get kline/candlestick data from testnet + /// Get kline/candlestick data from testnet (Weight: 2) + /// + /// [interval] Valid intervals: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M Future> getKlines( String symbol, String interval, { int? startTime, int? endTime, + String? timeZone, int limit = 500, }) async { final params = { @@ -104,37 +171,115 @@ class TestnetMarket extends BinanceBase { }; if (startTime != null) params['startTime'] = startTime; if (endTime != null) params['endTime'] = endTime; + if (timeZone != null) params['timeZone'] = timeZone; - final response = await sendRequest('GET', '/api/v3/klines', params: params); + final response = await sendRequest('GET', '/api/v3/klines', params: params, weight: 2); + return response as List; + } + + /// Get UIKlines (klines optimized for UI) from testnet (Weight: 2) + /// + /// Modification of the Klines endpoint that is optimized for presentation on UI. + Future> getUIKlines( + String symbol, + String interval, { + int? startTime, + int? endTime, + String? timeZone, + int limit = 500, + }) async { + final params = { + 'symbol': symbol, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (timeZone != null) params['timeZone'] = timeZone; + + final response = await sendRequest('GET', '/api/v3/uiKlines', params: params, weight: 2); return response as List; } - /// Get current average price from testnet + /// Get current average price from testnet (Weight: 2) Future> getAvgPrice(String symbol) { - return sendRequest('GET', '/api/v3/avgPrice', params: {'symbol': symbol}); + return sendRequest('GET', '/api/v3/avgPrice', params: {'symbol': symbol}, weight: 2); + } + + /// Get 24hr ticker statistics from testnet (Weight: 2-80) + Future> get24HrTicker(String symbol) { + return sendRequest('GET', '/api/v3/ticker/24hr', + params: {'symbol': symbol}, weight: 2); } - /// Get 24hr ticker price change statistics for all symbols - Future> get24HrTickerAll() async { - final response = await sendRequest('GET', '/api/v3/ticker/24hr'); + /// Get 24hr ticker price change statistics for all symbols (Weight: 80) + Future> get24HrTickerAll({String? type}) async { + final params = {}; + if (type != null) params['type'] = type; + + final response = await sendRequest('GET', '/api/v3/ticker/24hr', + params: params, weight: 80); return response as List; } - /// Get latest price for a symbol or symbols + /// Get trading day ticker (Weight: 4 per symbol) + /// + /// Price change statistics for the trading day. + Future getTradingDayTicker({ + String? symbol, + List? symbols, + String? timeZone, + String? type, + }) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (symbols != null) params['symbols'] = symbols; + if (timeZone != null) params['timeZone'] = timeZone; + if (type != null) params['type'] = type; + + return sendRequest('GET', '/api/v3/ticker/tradingDay', params: params, weight: 4); + } + + /// Get latest price for a symbol or symbols (Weight: 2-4) Future getTickerPrice([String? symbol]) async { final params = symbol != null ? {'symbol': symbol} : {}; - return await sendRequest('GET', '/api/v3/ticker/price', params: params); + return await sendRequest('GET', '/api/v3/ticker/price', + params: params, weight: symbol != null ? 2 : 4); } - /// Get best price/qty on the order book for a symbol or symbols + /// Get best price/qty on the order book for a symbol or symbols (Weight: 2-4) Future getBookTicker([String? symbol]) async { final params = symbol != null ? {'symbol': symbol} : {}; return await sendRequest('GET', '/api/v3/ticker/bookTicker', - params: params); + params: params, weight: symbol != null ? 2 : 4); + } + + /// Get rolling window price change statistics (Weight: 4 per symbol) + /// + /// [windowSize] Supported values: 1m, 2m, ..., 59m, 1h, 2h, ..., 23h, 1d, ..., 7d + Future getRollingWindowTicker({ + String? symbol, + List? symbols, + String windowSize = '1d', + String? type, + }) async { + final params = {'windowSize': windowSize}; + if (symbol != null) params['symbol'] = symbol; + if (symbols != null) params['symbols'] = symbols; + if (type != null) params['type'] = type; + + return sendRequest('GET', '/api/v3/ticker', params: params, weight: 4); } } /// Trading endpoints for Binance Testnet +/// +/// Supports all order types including: +/// - Standard orders (LIMIT, MARKET, STOP_LOSS, etc.) +/// - OCO (One-Cancels-the-Other) orders +/// - OTO (One-Triggers-the-Other) orders +/// - OTOCO (One-Triggers-OCO) orders +/// - Cancel-Replace operations class TestnetTrading extends BinanceBase { TestnetTrading({String? apiKey, String? apiSecret}) : super( @@ -143,7 +288,14 @@ class TestnetTrading extends BinanceBase { baseUrl: 'https://testnet.binance.vision', ); - /// Place a new order on testnet + // ==================== Standard Order Endpoints ==================== + + /// Place a new order on testnet (Weight: 1) + /// + /// [type] Order types: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, + /// TAKE_PROFIT, TAKE_PROFIT_LIMIT, LIMIT_MAKER + /// [side] BUY or SELL + /// [timeInForce] GTC, IOC, FOK (required for LIMIT orders) Future> placeOrder({ required String symbol, required String side, @@ -153,9 +305,13 @@ class TestnetTrading extends BinanceBase { double? price, String? newClientOrderId, double? stopPrice, + double? trailingDelta, double? icebergQty, String? newOrderRespType, String? timeInForce, + String? selfTradePreventionMode, + int? strategyId, + int? strategyType, int? recvWindow, }) { final params = { @@ -169,15 +325,23 @@ class TestnetTrading extends BinanceBase { if (price != null) params['price'] = price; if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; if (stopPrice != null) params['stopPrice'] = stopPrice; + if (trailingDelta != null) params['trailingDelta'] = trailingDelta; if (icebergQty != null) params['icebergQty'] = icebergQty; if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; if (timeInForce != null) params['timeInForce'] = timeInForce; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (strategyId != null) params['strategyId'] = strategyId; + if (strategyType != null) params['strategyType'] = strategyType; if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('POST', '/api/v3/order', params: params); + return sendRequest('POST', '/api/v3/order', params: params, isOrder: true); } - /// Test new order creation on testnet (validation only) + /// Test new order creation on testnet (validation only) (Weight: 1) + /// + /// Tests a new order without actually placing it. Future> testOrder({ required String symbol, required String side, @@ -187,8 +351,11 @@ class TestnetTrading extends BinanceBase { double? price, String? newClientOrderId, double? stopPrice, + double? trailingDelta, double? icebergQty, String? timeInForce, + String? selfTradePreventionMode, + bool? computeCommissionRates, int? recvWindow, }) { final params = { @@ -202,14 +369,21 @@ class TestnetTrading extends BinanceBase { if (price != null) params['price'] = price; if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; if (stopPrice != null) params['stopPrice'] = stopPrice; + if (trailingDelta != null) params['trailingDelta'] = trailingDelta; if (icebergQty != null) params['icebergQty'] = icebergQty; if (timeInForce != null) params['timeInForce'] = timeInForce; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (computeCommissionRates != null) { + params['computeCommissionRates'] = computeCommissionRates; + } if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/api/v3/order/test', params: params); } - /// Query order status on testnet + /// Query order status on testnet (Weight: 4) Future> getOrder({ required String symbol, int? orderId, @@ -219,33 +393,39 @@ class TestnetTrading extends BinanceBase { final params = {'symbol': symbol}; if (orderId != null) params['orderId'] = orderId; - if (origClientOrderId != null) + if (origClientOrderId != null) { params['origClientOrderId'] = origClientOrderId; + } if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('GET', '/api/v3/order', params: params); + return sendRequest('GET', '/api/v3/order', params: params, weight: 4); } - /// Cancel an active order on testnet + /// Cancel an active order on testnet (Weight: 1) Future> cancelOrder({ required String symbol, int? orderId, String? origClientOrderId, String? newClientOrderId, + String? cancelRestrictions, int? recvWindow, }) { final params = {'symbol': symbol}; if (orderId != null) params['orderId'] = orderId; - if (origClientOrderId != null) + if (origClientOrderId != null) { params['origClientOrderId'] = origClientOrderId; + } if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; + if (cancelRestrictions != null) { + params['cancelRestrictions'] = cancelRestrictions; + } if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('DELETE', '/api/v3/order', params: params); } - /// Cancel all open orders on a symbol on testnet + /// Cancel all open orders on a symbol on testnet (Weight: 1) Future> cancelAllOrders({ required String symbol, int? recvWindow, @@ -258,7 +438,425 @@ class TestnetTrading extends BinanceBase { return response as List; } - /// Get all open orders on testnet + // ==================== Cancel-Replace Order ==================== + + /// Cancel an existing order and send a new order on testnet (Weight: 1) + /// + /// Cancels an existing order and places a new order on the same symbol. + /// [cancelReplaceMode] STOP_ON_FAILURE or ALLOW_FAILURE + Future> cancelReplace({ + required String symbol, + required String side, + required String type, + required String cancelReplaceMode, + String? timeInForce, + double? quantity, + double? quoteOrderQty, + double? price, + String? cancelNewClientOrderId, + String? cancelOrigClientOrderId, + int? cancelOrderId, + String? newClientOrderId, + int? strategyId, + int? strategyType, + double? stopPrice, + double? trailingDelta, + double? icebergQty, + String? newOrderRespType, + String? selfTradePreventionMode, + String? cancelRestrictions, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'type': type, + 'cancelReplaceMode': cancelReplaceMode, + }; + + if (timeInForce != null) params['timeInForce'] = timeInForce; + if (quantity != null) params['quantity'] = quantity; + if (quoteOrderQty != null) params['quoteOrderQty'] = quoteOrderQty; + if (price != null) params['price'] = price; + if (cancelNewClientOrderId != null) { + params['cancelNewClientOrderId'] = cancelNewClientOrderId; + } + if (cancelOrigClientOrderId != null) { + params['cancelOrigClientOrderId'] = cancelOrigClientOrderId; + } + if (cancelOrderId != null) params['cancelOrderId'] = cancelOrderId; + if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; + if (strategyId != null) params['strategyId'] = strategyId; + if (strategyType != null) params['strategyType'] = strategyType; + if (stopPrice != null) params['stopPrice'] = stopPrice; + if (trailingDelta != null) params['trailingDelta'] = trailingDelta; + if (icebergQty != null) params['icebergQty'] = icebergQty; + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (cancelRestrictions != null) { + params['cancelRestrictions'] = cancelRestrictions; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/order/cancelReplace', + params: params, isOrder: true); + } + + // ==================== OCO Orders ==================== + + /// Place a new OCO order on testnet (Weight: 1) + /// + /// OCO (One-Cancels-the-Other) places a limit order and a stop-limit order. + /// When either order executes, the other is automatically canceled. + Future> placeOcoOrder({ + required String symbol, + required String side, + required double quantity, + required double price, + required double stopPrice, + String? listClientOrderId, + String? limitClientOrderId, + double? limitIcebergQty, + double? limitStrategyId, + int? limitStrategyType, + double? stopLimitPrice, + String? stopClientOrderId, + double? stopIcebergQty, + double? stopStrategyId, + int? stopStrategyType, + String? stopLimitTimeInForce, + String? newOrderRespType, + String? selfTradePreventionMode, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'quantity': quantity, + 'price': price, + 'stopPrice': stopPrice, + }; + + if (listClientOrderId != null) { + params['listClientOrderId'] = listClientOrderId; + } + if (limitClientOrderId != null) { + params['limitClientOrderId'] = limitClientOrderId; + } + if (limitIcebergQty != null) params['limitIcebergQty'] = limitIcebergQty; + if (limitStrategyId != null) params['limitStrategyId'] = limitStrategyId; + if (limitStrategyType != null) { + params['limitStrategyType'] = limitStrategyType; + } + if (stopLimitPrice != null) params['stopLimitPrice'] = stopLimitPrice; + if (stopClientOrderId != null) { + params['stopClientOrderId'] = stopClientOrderId; + } + if (stopIcebergQty != null) params['stopIcebergQty'] = stopIcebergQty; + if (stopStrategyId != null) params['stopStrategyId'] = stopStrategyId; + if (stopStrategyType != null) params['stopStrategyType'] = stopStrategyType; + if (stopLimitTimeInForce != null) { + params['stopLimitTimeInForce'] = stopLimitTimeInForce; + } + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/order/oco', params: params, isOrder: true); + } + + /// Cancel an OCO order on testnet (Weight: 1) + Future> cancelOcoOrder({ + required String symbol, + int? orderListId, + String? listClientOrderId, + String? newClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderListId != null) params['orderListId'] = orderListId; + if (listClientOrderId != null) { + params['listClientOrderId'] = listClientOrderId; + } + if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('DELETE', '/api/v3/orderList', params: params); + } + + /// Query OCO order status on testnet (Weight: 4) + Future> getOcoOrder({ + int? orderListId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {}; + + if (orderListId != null) params['orderListId'] = orderListId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/api/v3/orderList', params: params, weight: 4); + } + + /// Get all OCO orders on testnet (Weight: 20) + Future> getAllOcoOrders({ + int? fromId, + int? startTime, + int? endTime, + int limit = 500, + int? recvWindow, + }) async { + final params = {'limit': limit}; + + if (fromId != null) params['fromId'] = fromId; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/api/v3/allOrderList', + params: params, weight: 20); + return response as List; + } + + /// Get all open OCO orders on testnet (Weight: 6) + Future> getOpenOcoOrders({int? recvWindow}) async { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/api/v3/openOrderList', + params: params, weight: 6); + return response as List; + } + + // ==================== OTO Orders ==================== + + /// Place a new OTO order on testnet (Weight: 1) + /// + /// OTO (One-Triggers-the-Other) places a working order that triggers + /// a pending order when the working order is filled. + Future> placeOtoOrder({ + required String symbol, + required String workingType, + required String workingSide, + required double workingPrice, + required double workingQuantity, + required String pendingType, + required String pendingSide, + required double pendingQuantity, + String? listClientOrderId, + String? workingClientOrderId, + double? workingIcebergQty, + String? workingTimeInForce, + int? workingStrategyId, + int? workingStrategyType, + String? pendingClientOrderId, + double? pendingPrice, + double? pendingStopPrice, + double? pendingTrailingDelta, + double? pendingIcebergQty, + String? pendingTimeInForce, + int? pendingStrategyId, + int? pendingStrategyType, + String? newOrderRespType, + String? selfTradePreventionMode, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'workingType': workingType, + 'workingSide': workingSide, + 'workingPrice': workingPrice, + 'workingQuantity': workingQuantity, + 'pendingType': pendingType, + 'pendingSide': pendingSide, + 'pendingQuantity': pendingQuantity, + }; + + if (listClientOrderId != null) { + params['listClientOrderId'] = listClientOrderId; + } + if (workingClientOrderId != null) { + params['workingClientOrderId'] = workingClientOrderId; + } + if (workingIcebergQty != null) { + params['workingIcebergQty'] = workingIcebergQty; + } + if (workingTimeInForce != null) { + params['workingTimeInForce'] = workingTimeInForce; + } + if (workingStrategyId != null) { + params['workingStrategyId'] = workingStrategyId; + } + if (workingStrategyType != null) { + params['workingStrategyType'] = workingStrategyType; + } + if (pendingClientOrderId != null) { + params['pendingClientOrderId'] = pendingClientOrderId; + } + if (pendingPrice != null) params['pendingPrice'] = pendingPrice; + if (pendingStopPrice != null) params['pendingStopPrice'] = pendingStopPrice; + if (pendingTrailingDelta != null) { + params['pendingTrailingDelta'] = pendingTrailingDelta; + } + if (pendingIcebergQty != null) { + params['pendingIcebergQty'] = pendingIcebergQty; + } + if (pendingTimeInForce != null) { + params['pendingTimeInForce'] = pendingTimeInForce; + } + if (pendingStrategyId != null) { + params['pendingStrategyId'] = pendingStrategyId; + } + if (pendingStrategyType != null) { + params['pendingStrategyType'] = pendingStrategyType; + } + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/orderList/oto', + params: params, isOrder: true); + } + + // ==================== OTOCO Orders ==================== + + /// Place a new OTOCO order on testnet (Weight: 1) + /// + /// OTOCO (One-Triggers-One-Cancels-the-Other) places a working order + /// that triggers an OCO order when filled. + Future> placeOtocoOrder({ + required String symbol, + required String workingType, + required String workingSide, + required double workingPrice, + required double workingQuantity, + required String pendingSide, + required double pendingQuantity, + required double pendingAbovePrice, + required double pendingBelowPrice, + String? listClientOrderId, + String? workingClientOrderId, + double? workingIcebergQty, + String? workingTimeInForce, + int? workingStrategyId, + int? workingStrategyType, + String? pendingAboveType, + String? pendingAboveClientOrderId, + double? pendingAboveStopPrice, + double? pendingAboveTrailingDelta, + double? pendingAboveIcebergQty, + String? pendingAboveTimeInForce, + int? pendingAboveStrategyId, + int? pendingAboveStrategyType, + String? pendingBelowType, + String? pendingBelowClientOrderId, + double? pendingBelowStopPrice, + double? pendingBelowTrailingDelta, + double? pendingBelowIcebergQty, + String? pendingBelowTimeInForce, + int? pendingBelowStrategyId, + int? pendingBelowStrategyType, + String? newOrderRespType, + String? selfTradePreventionMode, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'workingType': workingType, + 'workingSide': workingSide, + 'workingPrice': workingPrice, + 'workingQuantity': workingQuantity, + 'pendingSide': pendingSide, + 'pendingQuantity': pendingQuantity, + 'pendingAbovePrice': pendingAbovePrice, + 'pendingBelowPrice': pendingBelowPrice, + }; + + if (listClientOrderId != null) { + params['listClientOrderId'] = listClientOrderId; + } + if (workingClientOrderId != null) { + params['workingClientOrderId'] = workingClientOrderId; + } + if (workingIcebergQty != null) { + params['workingIcebergQty'] = workingIcebergQty; + } + if (workingTimeInForce != null) { + params['workingTimeInForce'] = workingTimeInForce; + } + if (workingStrategyId != null) { + params['workingStrategyId'] = workingStrategyId; + } + if (workingStrategyType != null) { + params['workingStrategyType'] = workingStrategyType; + } + if (pendingAboveType != null) params['pendingAboveType'] = pendingAboveType; + if (pendingAboveClientOrderId != null) { + params['pendingAboveClientOrderId'] = pendingAboveClientOrderId; + } + if (pendingAboveStopPrice != null) { + params['pendingAboveStopPrice'] = pendingAboveStopPrice; + } + if (pendingAboveTrailingDelta != null) { + params['pendingAboveTrailingDelta'] = pendingAboveTrailingDelta; + } + if (pendingAboveIcebergQty != null) { + params['pendingAboveIcebergQty'] = pendingAboveIcebergQty; + } + if (pendingAboveTimeInForce != null) { + params['pendingAboveTimeInForce'] = pendingAboveTimeInForce; + } + if (pendingAboveStrategyId != null) { + params['pendingAboveStrategyId'] = pendingAboveStrategyId; + } + if (pendingAboveStrategyType != null) { + params['pendingAboveStrategyType'] = pendingAboveStrategyType; + } + if (pendingBelowType != null) params['pendingBelowType'] = pendingBelowType; + if (pendingBelowClientOrderId != null) { + params['pendingBelowClientOrderId'] = pendingBelowClientOrderId; + } + if (pendingBelowStopPrice != null) { + params['pendingBelowStopPrice'] = pendingBelowStopPrice; + } + if (pendingBelowTrailingDelta != null) { + params['pendingBelowTrailingDelta'] = pendingBelowTrailingDelta; + } + if (pendingBelowIcebergQty != null) { + params['pendingBelowIcebergQty'] = pendingBelowIcebergQty; + } + if (pendingBelowTimeInForce != null) { + params['pendingBelowTimeInForce'] = pendingBelowTimeInForce; + } + if (pendingBelowStrategyId != null) { + params['pendingBelowStrategyId'] = pendingBelowStrategyId; + } + if (pendingBelowStrategyType != null) { + params['pendingBelowStrategyType'] = pendingBelowStrategyType; + } + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (selfTradePreventionMode != null) { + params['selfTradePreventionMode'] = selfTradePreventionMode; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/orderList/otoco', + params: params, isOrder: true); + } + + // ==================== Query Endpoints ==================== + + /// Get all open orders on testnet (Weight: 6 for single symbol, 80 for all) Future> getOpenOrders({ String? symbol, int? recvWindow, @@ -267,12 +865,13 @@ class TestnetTrading extends BinanceBase { if (symbol != null) params['symbol'] = symbol; if (recvWindow != null) params['recvWindow'] = recvWindow; - final response = - await sendRequest('GET', '/api/v3/openOrders', params: params); + final weight = symbol != null ? 6 : 80; + final response = await sendRequest('GET', '/api/v3/openOrders', + params: params, weight: weight); return response as List; } - /// Get all account orders; active, canceled, or filled on testnet + /// Get all account orders; active, canceled, or filled on testnet (Weight: 20) Future> getAllOrders({ required String symbol, int? orderId, @@ -291,14 +890,15 @@ class TestnetTrading extends BinanceBase { if (endTime != null) params['endTime'] = endTime; if (recvWindow != null) params['recvWindow'] = recvWindow; - final response = - await sendRequest('GET', '/api/v3/allOrders', params: params); + final response = await sendRequest('GET', '/api/v3/allOrders', + params: params, weight: 20); return response as List; } - /// Get trades for a specific account and symbol on testnet + /// Get trades for a specific account and symbol on testnet (Weight: 20) Future> getMyTrades({ required String symbol, + int? orderId, int? startTime, int? endTime, int? fromId, @@ -310,22 +910,101 @@ class TestnetTrading extends BinanceBase { 'limit': limit, }; + if (orderId != null) params['orderId'] = orderId; if (startTime != null) params['startTime'] = startTime; if (endTime != null) params['endTime'] = endTime; if (fromId != null) params['fromId'] = fromId; if (recvWindow != null) params['recvWindow'] = recvWindow; - final response = - await sendRequest('GET', '/api/v3/myTrades', params: params); + final response = await sendRequest('GET', '/api/v3/myTrades', + params: params, weight: 20); return response as List; } - /// Get current account information on testnet - Future> getAccountInfo({int? recvWindow}) { + /// Get current account information on testnet (Weight: 20) + Future> getAccountInfo({ + bool? omitZeroBalances, + int? recvWindow, + }) { final params = {}; + if (omitZeroBalances != null) params['omitZeroBalances'] = omitZeroBalances; if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('GET', '/api/v3/account', params: params); + return sendRequest('GET', '/api/v3/account', params: params, weight: 20); + } + + /// Query unfilled order count (Weight: 40) + Future> getRateLimitOrder({int? recvWindow}) async { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/api/v3/rateLimit/order', + params: params, weight: 40); + return response as List; + } + + /// Query prevented matches (Weight: 20) + Future> getPreventedMatches({ + required String symbol, + int? preventedMatchId, + int? orderId, + int? fromPreventedMatchId, + int limit = 500, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + + if (preventedMatchId != null) params['preventedMatchId'] = preventedMatchId; + if (orderId != null) params['orderId'] = orderId; + if (fromPreventedMatchId != null) { + params['fromPreventedMatchId'] = fromPreventedMatchId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/api/v3/myPreventedMatches', + params: params, weight: 20); + return response as List; + } + + /// Query allocations (Weight: 20) + Future> getAllocations({ + required String symbol, + int? startTime, + int? endTime, + int? fromAllocationId, + int limit = 500, + int? orderId, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (fromAllocationId != null) params['fromAllocationId'] = fromAllocationId; + if (orderId != null) params['orderId'] = orderId; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/api/v3/myAllocations', + params: params, weight: 20); + return response as List; + } + + /// Get commission rates (Weight: 20) + Future> getCommissionRates({ + required String symbol, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/api/v3/account/commission', + params: params, weight: 20); } } @@ -483,3 +1162,463 @@ class TestnetFuturesUsdTrading extends BinanceBase { return response as List; } } + +// ============================================================================ +// Demo Trading API (Alternative to testnet.binance.vision) +// ============================================================================ + +/// Binance Demo Trading API - Alternative testnet for regions where +/// testnet.binance.vision is not accessible. +/// +/// Base URLs: +/// - REST API: https://demo-api.binance.com +/// - WebSocket Trade: wss://demo-ws-api.binance.com +/// - WebSocket Market: wss://demo-stream.binance.com +/// +/// Get demo API keys from your Binance account settings. +class DemoSpot { + final DemoMarket market; + final DemoTrading trading; + final DemoUserDataStream userDataStream; + final DemoWebSocket webSocket; + + DemoSpot({String? apiKey, String? apiSecret}) + : market = DemoMarket(apiKey: apiKey, apiSecret: apiSecret), + trading = DemoTrading(apiKey: apiKey, apiSecret: apiSecret), + userDataStream = + DemoUserDataStream(apiKey: apiKey, apiSecret: apiSecret), + webSocket = DemoWebSocket(); + + /// Dispose and clean up resources + Future dispose() async { + market.dispose(); + trading.dispose(); + userDataStream.dispose(); + await webSocket.dispose(); + } +} + +/// WebSocket client for Binance Demo Trading +class DemoWebSocket extends BinanceWebSocket { + DemoWebSocket({WebSocketConfig? config}) + : super( + baseUrl: 'wss://demo-stream.binance.com', + config: config, + ); +} + +/// Market data endpoints for Binance Demo Trading +class DemoMarket extends BinanceBase { + DemoMarket({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://demo-api.binance.com', + ); + + /// Test connectivity + Future> ping() { + return sendRequest('GET', '/api/v3/ping', weight: 1); + } + + /// Get server time + Future> getServerTime() { + return sendRequest('GET', '/api/v3/time', weight: 1); + } + + /// Get exchange information + Future> getExchangeInfo({ + String? symbol, + List? symbols, + }) { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (symbols != null) params['symbols'] = symbols; + + return sendRequest('GET', '/api/v3/exchangeInfo', params: params, weight: 20); + } + + /// Get order book depth + Future> getOrderBook(String symbol, {int limit = 100}) { + return sendRequest('GET', '/api/v3/depth', + params: {'symbol': symbol, 'limit': limit}); + } + + /// Get recent trades + Future> getRecentTrades(String symbol, {int limit = 500}) async { + final response = await sendRequest('GET', '/api/v3/trades', + params: {'symbol': symbol, 'limit': limit}); + return response as List; + } + + /// Get klines/candlestick data + Future> getKlines( + String symbol, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'symbol': symbol, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/api/v3/klines', params: params); + return response as List; + } + + /// Get 24hr ticker statistics + Future> get24HrTicker(String symbol) { + return sendRequest('GET', '/api/v3/ticker/24hr', + params: {'symbol': symbol}); + } + + /// Get latest price + Future getTickerPrice([String? symbol]) async { + final params = symbol != null ? {'symbol': symbol} : {}; + return sendRequest('GET', '/api/v3/ticker/price', params: params); + } + + /// Get best book ticker + Future getBookTicker([String? symbol]) async { + final params = symbol != null ? {'symbol': symbol} : {}; + return sendRequest('GET', '/api/v3/ticker/bookTicker', params: params); + } +} + +/// Trading endpoints for Binance Demo Trading +class DemoTrading extends BinanceBase { + DemoTrading({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://demo-api.binance.com', + ); + + /// Place a new order + Future> placeOrder({ + required String symbol, + required String side, + required String type, + double? quantity, + double? quoteOrderQty, + double? price, + String? newClientOrderId, + double? stopPrice, + String? timeInForce, + String? newOrderRespType, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'type': type, + }; + + if (quantity != null) params['quantity'] = quantity; + if (quoteOrderQty != null) params['quoteOrderQty'] = quoteOrderQty; + if (price != null) params['price'] = price; + if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; + if (stopPrice != null) params['stopPrice'] = stopPrice; + if (timeInForce != null) params['timeInForce'] = timeInForce; + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/order', params: params, isOrder: true); + } + + /// Test order creation (validation only) + Future> testOrder({ + required String symbol, + required String side, + required String type, + double? quantity, + double? price, + String? timeInForce, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'type': type, + }; + + if (quantity != null) params['quantity'] = quantity; + if (price != null) params['price'] = price; + if (timeInForce != null) params['timeInForce'] = timeInForce; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/api/v3/order/test', params: params); + } + + /// Query order status + Future> getOrder({ + required String symbol, + int? orderId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderId != null) params['orderId'] = orderId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/api/v3/order', params: params); + } + + /// Cancel an order + Future> cancelOrder({ + required String symbol, + int? orderId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderId != null) params['orderId'] = orderId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('DELETE', '/api/v3/order', params: params); + } + + /// Cancel all open orders on a symbol + Future> cancelAllOrders({ + required String symbol, + int? recvWindow, + }) async { + final params = {'symbol': symbol}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('DELETE', '/api/v3/openOrders', params: params); + return response as List; + } + + /// Get all open orders + Future> getOpenOrders({String? symbol, int? recvWindow}) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('GET', '/api/v3/openOrders', params: params); + return response as List; + } + + /// Get all orders + Future> getAllOrders({ + required String symbol, + int? orderId, + int? startTime, + int? endTime, + int limit = 500, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + + if (orderId != null) params['orderId'] = orderId; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('GET', '/api/v3/allOrders', params: params); + return response as List; + } + + /// Get account information + Future> getAccountInfo({int? recvWindow}) { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/api/v3/account', params: params); + } + + /// Get account trades + Future> getMyTrades({ + required String symbol, + int? startTime, + int? endTime, + int? fromId, + int limit = 500, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (fromId != null) params['fromId'] = fromId; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('GET', '/api/v3/myTrades', params: params); + return response as List; + } +} + +/// User Data Stream endpoints for Binance Demo Trading +class DemoUserDataStream extends BinanceBase { + DemoUserDataStream({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://demo-api.binance.com', + ); + + /// Start a new user data stream + Future> createListenKey() { + return sendRequest('POST', '/api/v3/userDataStream'); + } + + /// Keep alive a user data stream + Future> keepAliveListenKey(String listenKey) { + return sendRequest('PUT', '/api/v3/userDataStream', + params: {'listenKey': listenKey}); + } + + /// Close a user data stream + Future> closeListenKey(String listenKey) { + return sendRequest('DELETE', '/api/v3/userDataStream', + params: {'listenKey': listenKey}); + } +} + +/// Demo Futures USD-M endpoints (https://demo-fapi.binance.com) +class DemoFuturesUsd { + final DemoFuturesUsdMarket market; + final DemoFuturesUsdTrading trading; + + DemoFuturesUsd({String? apiKey, String? apiSecret}) + : market = DemoFuturesUsdMarket(apiKey: apiKey, apiSecret: apiSecret), + trading = DemoFuturesUsdTrading(apiKey: apiKey, apiSecret: apiSecret); +} + +/// Demo Futures USD-M Market data +class DemoFuturesUsdMarket extends BinanceBase { + DemoFuturesUsdMarket({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://demo-fapi.binance.com', + ); + + /// Get server time + Future> getServerTime() { + return sendRequest('GET', '/fapi/v1/time'); + } + + /// Get exchange information + Future> getExchangeInfo() { + return sendRequest('GET', '/fapi/v1/exchangeInfo'); + } + + /// Get order book + Future> getOrderBook(String symbol, {int limit = 100}) { + return sendRequest('GET', '/fapi/v1/depth', + params: {'symbol': symbol, 'limit': limit}); + } + + /// Get 24hr ticker + Future> get24HrTicker(String symbol) { + return sendRequest('GET', '/fapi/v1/ticker/24hr', + params: {'symbol': symbol}); + } + + /// Get klines + Future> getKlines( + String symbol, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'symbol': symbol, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = + await sendRequest('GET', '/fapi/v1/klines', params: params); + return response as List; + } +} + +/// Demo Futures USD-M Trading +class DemoFuturesUsdTrading extends BinanceBase { + DemoFuturesUsdTrading({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://demo-fapi.binance.com', + ); + + /// Place a new futures order + Future> placeOrder({ + required String symbol, + required String side, + required String type, + double? quantity, + double? price, + String? timeInForce, + String? positionSide, + double? stopPrice, + String? workingType, + String? newOrderRespType, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'type': type, + }; + + if (quantity != null) params['quantity'] = quantity; + if (price != null) params['price'] = price; + if (timeInForce != null) params['timeInForce'] = timeInForce; + if (positionSide != null) params['positionSide'] = positionSide; + if (stopPrice != null) params['stopPrice'] = stopPrice; + if (workingType != null) params['workingType'] = workingType; + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/fapi/v1/order', params: params, isOrder: true); + } + + /// Get account information + Future> getAccountInfo({int? recvWindow}) { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/fapi/v2/account', params: params); + } + + /// Get position information + Future> getPositionInfo({String? symbol, int? recvWindow}) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('GET', '/fapi/v2/positionRisk', params: params); + return response as List; + } +} From 45588c47e9fd106c80283bfd0dd70cbe79fe4c71 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 16:26:02 +0000 Subject: [PATCH 2/5] feat: Add COIN-M Futures Testnet support Add comprehensive COIN-M (inverse) futures testnet integration: - TestnetFuturesCoinM class for COIN-margined delivery futures - TestnetFuturesCoinMMarket with full market data endpoints: - ping, getServerTime, getExchangeInfo - getOrderBook, getRecentTrades, getHistoricalTrades, getAggTrades - getKlines, getContinuousKlines, getIndexPriceKlines, getMarkPriceKlines - getPremiumIndex, getFundingRate, get24HrTicker - getTickerPrice, getBookTicker, getOpenInterest, getOpenInterestHist - TestnetFuturesCoinMTrading with all trading operations: - placeOrder, placeBatchOrders - getOrder, cancelOrder, cancelAllOrders, cancelBatchOrders - setAutoCancel, getCurrentOpenOrder, getOpenOrders, getAllOrders - getBalance, getAccountInfo - changeInitialLeverage, changeMarginType - modifyIsolatedPositionMargin, getPositionMarginHistory - getPositionRisk, getUserTrades, getIncomeHistory - getLeverageBracket, changePositionMode, getPositionMode - getForceOrders, getAdlQuantile, getCommissionRate - TestnetFuturesCoinMUserDataStream for user data streaming - Updated example with COIN-M futures documentation COIN-M futures are settled in cryptocurrency (BTC, ETH) rather than USDT, using /dapi/ endpoints on testnet.binancefuture.com. --- example/testnet_integration_example.dart | 51 +- lib/src/babel_binance_base.dart | 2 + lib/src/testnet.dart | 747 +++++++++++++++++++++++ 3 files changed, 793 insertions(+), 7 deletions(-) diff --git a/example/testnet_integration_example.dart b/example/testnet_integration_example.dart index e70b75c..07f03d3 100644 --- a/example/testnet_integration_example.dart +++ b/example/testnet_integration_example.dart @@ -6,7 +6,8 @@ /// Available Testnet Environments: /// - Spot Testnet: https://testnet.binance.vision/ /// - Demo Trading: https://demo-api.binance.com (alternative) -/// - Futures Testnet: https://testnet.binancefuture.com/ +/// - Futures USD-M Testnet: https://testnet.binancefuture.com/fapi +/// - Futures COIN-M Testnet: https://testnet.binancefuture.com/dapi /// /// Features covered: /// - Testnet API key setup @@ -224,13 +225,23 @@ Future step4_AdvancedTestnetFeatures() async { print(' - Trade history'); print(''); - print('FUTURES TRADING FEATURES:'); + print('USD-M FUTURES TRADING FEATURES:'); print(' - Long and short positions'); print(' - Leverage up to 125x'); - print(' - Margin management'); + print(' - Margin management (USDT-margined)'); print(' - Position sizing'); print(' - Liquidation simulation'); print(' - Funding rates'); + print(' - Endpoint: /fapi/v1/*'); + print(''); + + print('COIN-M FUTURES TRADING FEATURES:'); + print(' - Inverse contracts (settled in crypto)'); + print(' - BTC, ETH margined positions'); + print(' - Quarterly delivery contracts'); + print(' - Perpetual contracts'); + print(' - Leverage and margin management'); + print(' - Endpoint: /dapi/v1/*'); print(''); print('WEBSOCKET FEATURES:'); @@ -291,6 +302,31 @@ Future step4_AdvancedTestnetFeatures() async { print('});'); print('```'); print(''); + + print('EXAMPLE: COIN-M FUTURES ON TESTNET'); + print('```dart'); + print('// Initialize testnet'); + print('final binance = Binance.testnet('); + print(' apiKey: \'testnet_key\','); + print(' apiSecret: \'testnet_secret\','); + print(');'); + print(''); + print('// Get COIN-M futures market data'); + print('final info = await binance.testnetFuturesCoinM.market.getExchangeInfo();'); + print('final ticker = await binance.testnetFuturesCoinM.market.get24HrTicker(\'BTCUSD_PERP\');'); + print(''); + print('// Place a COIN-M futures order'); + print('final order = await binance.testnetFuturesCoinM.trading.placeOrder('); + print(' symbol: \'BTCUSD_PERP\','); + print(' side: \'BUY\','); + print(' type: \'MARKET\','); + print(' quantity: 1, // 1 contract = 100 USD'); + print(');'); + print(''); + print('// Get position info'); + print('final positions = await binance.testnetFuturesCoinM.trading.getPositionRisk();'); + print('```'); + print(''); await _pause(); } @@ -419,10 +455,11 @@ void step6_BestPractices() { print(''); print('ADDITIONAL RESOURCES:'); print(''); - print('Testnet: https://testnet.binance.vision/'); - print('Demo Trading: https://demo-api.binance.com'); - print('API Docs: https://developers.binance.com/docs/'); - print('Futures Testnet: https://testnet.binancefuture.com/'); + print('Spot Testnet: https://testnet.binance.vision/'); + print('Demo Trading: https://demo-api.binance.com'); + print('API Docs: https://developers.binance.com/docs/'); + print('USD-M Testnet: https://testnet.binancefuture.com/fapi'); + print('COIN-M Testnet: https://testnet.binancefuture.com/dapi'); print(''); print('Happy testing! Remember: Test first, trade smart!'); } diff --git a/lib/src/babel_binance_base.dart b/lib/src/babel_binance_base.dart index 1ec5d90..fb8602c 100644 --- a/lib/src/babel_binance_base.dart +++ b/lib/src/babel_binance_base.dart @@ -42,6 +42,7 @@ class Binance { final Margin margin; final TestnetSpot testnetSpot; final TestnetFuturesUsd testnetFutures; + final TestnetFuturesCoinM testnetFuturesCoinM; final DemoSpot demoSpot; final DemoFuturesUsd demoFutures; final BinanceConfig config; @@ -61,6 +62,7 @@ class Binance { margin = Margin(apiKey: apiKey, apiSecret: apiSecret), testnetSpot = TestnetSpot(apiKey: apiKey, apiSecret: apiSecret), testnetFutures = TestnetFuturesUsd(apiKey: apiKey, apiSecret: apiSecret), + testnetFuturesCoinM = TestnetFuturesCoinM(apiKey: apiKey, apiSecret: apiSecret), demoSpot = DemoSpot(apiKey: apiKey, apiSecret: apiSecret), demoFutures = DemoFuturesUsd(apiKey: apiKey, apiSecret: apiSecret); diff --git a/lib/src/testnet.dart b/lib/src/testnet.dart index 40d5a5f..c29f0a5 100644 --- a/lib/src/testnet.dart +++ b/lib/src/testnet.dart @@ -1622,3 +1622,750 @@ class DemoFuturesUsdTrading extends BinanceBase { return response as List; } } + +// ============================================================================ +// COIN-M Futures Testnet API +// ============================================================================ + +/// Binance COIN-M Futures Testnet +/// +/// COIN-M Futures are margined and settled in cryptocurrency (e.g., BTC, ETH) +/// rather than USDT. This is useful for holders who want exposure without +/// converting to stablecoins. +/// +/// Base URL: https://testnet.binancefuture.com (same as USD-M but /dapi/ path) +/// WebSocket: wss://dstream.binancefuture.com +/// +/// Key differences from USD-M: +/// - Settled in cryptocurrency (BTC, ETH, etc.) +/// - Uses /dapi/ endpoints instead of /fapi/ +/// - Contract sizes are in USD value +/// - Inverse contracts (profit/loss in base currency) +class TestnetFuturesCoinM { + final TestnetFuturesCoinMMarket market; + final TestnetFuturesCoinMTrading trading; + + TestnetFuturesCoinM({String? apiKey, String? apiSecret}) + : market = TestnetFuturesCoinMMarket(apiKey: apiKey, apiSecret: apiSecret), + trading = + TestnetFuturesCoinMTrading(apiKey: apiKey, apiSecret: apiSecret); +} + +/// COIN-M Futures Market data for testnet +/// +/// Provides market data endpoints for COIN-margined delivery futures. +class TestnetFuturesCoinMMarket extends BinanceBase { + TestnetFuturesCoinMMarket({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://testnet.binancefuture.com', + ); + + /// Test connectivity (Weight: 1) + Future> ping() { + return sendRequest('GET', '/dapi/v1/ping', weight: 1); + } + + /// Get COIN-M testnet server time (Weight: 1) + Future> getServerTime() { + return sendRequest('GET', '/dapi/v1/time', weight: 1); + } + + /// Get COIN-M testnet exchange information (Weight: 1) + /// + /// Returns current exchange trading rules and symbol information + Future> getExchangeInfo() { + return sendRequest('GET', '/dapi/v1/exchangeInfo', weight: 1); + } + + /// Get COIN-M testnet order book (Weight: 5-20 depending on limit) + /// + /// [limit] Valid limits: 5, 10, 20, 50, 100, 500, 1000 + Future> getOrderBook(String symbol, {int limit = 100}) { + final weight = limit <= 50 ? 5 : (limit <= 100 ? 10 : 20); + return sendRequest('GET', '/dapi/v1/depth', + params: {'symbol': symbol, 'limit': limit}, weight: weight); + } + + /// Get COIN-M testnet recent trades (Weight: 5) + Future> getRecentTrades(String symbol, {int limit = 500}) async { + final response = await sendRequest('GET', '/dapi/v1/trades', + params: {'symbol': symbol, 'limit': limit}, weight: 5); + return response as List; + } + + /// Get COIN-M testnet historical trades (Weight: 20) + Future> getHistoricalTrades(String symbol, + {int limit = 500, int? fromId}) async { + final params = {'symbol': symbol, 'limit': limit}; + if (fromId != null) params['fromId'] = fromId; + + final response = await sendRequest('GET', '/dapi/v1/historicalTrades', + params: params, weight: 20); + return response as List; + } + + /// Get COIN-M testnet aggregate trades (Weight: 20) + Future> getAggTrades( + String symbol, { + int? fromId, + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = {'symbol': symbol, 'limit': limit}; + if (fromId != null) params['fromId'] = fromId; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/dapi/v1/aggTrades', + params: params, weight: 20); + return response as List; + } + + /// Get COIN-M testnet index price and mark price (Weight: 1) + Future getPremiumIndex([String? symbol]) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + + return sendRequest('GET', '/dapi/v1/premiumIndex', params: params, weight: 1); + } + + /// Get COIN-M testnet funding rate history (Weight: 1) + Future> getFundingRate({ + String? symbol, + int? startTime, + int? endTime, + int limit = 100, + }) async { + final params = {'limit': limit}; + if (symbol != null) params['symbol'] = symbol; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/dapi/v1/fundingRate', + params: params, weight: 1); + return response as List; + } + + /// Get COIN-M testnet klines/candlestick data (Weight: 5) + /// + /// [interval] Valid intervals: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M + Future> getKlines( + String symbol, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'symbol': symbol, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = + await sendRequest('GET', '/dapi/v1/klines', params: params, weight: 5); + return response as List; + } + + /// Get COIN-M testnet continuous contract klines (Weight: 5) + /// + /// [contractType] PERPETUAL, CURRENT_QUARTER, NEXT_QUARTER + Future> getContinuousKlines( + String pair, + String contractType, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'pair': pair, + 'contractType': contractType, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/dapi/v1/continuousKlines', + params: params, weight: 5); + return response as List; + } + + /// Get COIN-M testnet index price klines (Weight: 5) + Future> getIndexPriceKlines( + String pair, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'pair': pair, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/dapi/v1/indexPriceKlines', + params: params, weight: 5); + return response as List; + } + + /// Get COIN-M testnet mark price klines (Weight: 5) + Future> getMarkPriceKlines( + String symbol, + String interval, { + int? startTime, + int? endTime, + int limit = 500, + }) async { + final params = { + 'symbol': symbol, + 'interval': interval, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/dapi/v1/markPriceKlines', + params: params, weight: 5); + return response as List; + } + + /// Get COIN-M testnet 24hr ticker (Weight: 1-40) + Future get24HrTicker([String? symbol]) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + + final weight = symbol != null ? 1 : 40; + return sendRequest('GET', '/dapi/v1/ticker/24hr', + params: params, weight: weight); + } + + /// Get COIN-M testnet price ticker (Weight: 1-2) + Future getTickerPrice([String? symbol]) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + + final weight = symbol != null ? 1 : 2; + return sendRequest('GET', '/dapi/v1/ticker/price', + params: params, weight: weight); + } + + /// Get COIN-M testnet book ticker (Weight: 1-2) + Future getBookTicker([String? symbol]) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + + final weight = symbol != null ? 1 : 2; + return sendRequest('GET', '/dapi/v1/ticker/bookTicker', + params: params, weight: weight); + } + + /// Get COIN-M testnet open interest (Weight: 1) + Future> getOpenInterest(String symbol) { + return sendRequest('GET', '/dapi/v1/openInterest', + params: {'symbol': symbol}, weight: 1); + } + + /// Get COIN-M testnet open interest statistics (Weight: 1) + Future> getOpenInterestHist({ + required String pair, + required String contractType, + required String period, + int? startTime, + int? endTime, + int limit = 30, + }) async { + final params = { + 'pair': pair, + 'contractType': contractType, + 'period': period, + 'limit': limit, + }; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + + final response = await sendRequest('GET', '/futures/data/openInterestHist', + params: params, weight: 1); + return response as List; + } +} + +/// COIN-M Futures Trading for testnet +/// +/// Supports all trading operations for COIN-margined delivery futures. +class TestnetFuturesCoinMTrading extends BinanceBase { + TestnetFuturesCoinMTrading({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://testnet.binancefuture.com', + ); + + /// Place a new COIN-M futures order on testnet (Weight: 1) + /// + /// [type] LIMIT, MARKET, STOP, STOP_MARKET, TAKE_PROFIT, TAKE_PROFIT_MARKET, + /// TRAILING_STOP_MARKET + /// [side] BUY or SELL + /// [positionSide] BOTH, LONG, SHORT (for hedge mode) + Future> placeOrder({ + required String symbol, + required String side, + required String type, + String? positionSide, + String? timeInForce, + double? quantity, + bool? reduceOnly, + double? price, + String? newClientOrderId, + double? stopPrice, + double? activationPrice, + double? callbackRate, + String? workingType, + bool? priceProtect, + String? newOrderRespType, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'side': side, + 'type': type, + }; + + if (positionSide != null) params['positionSide'] = positionSide; + if (timeInForce != null) params['timeInForce'] = timeInForce; + if (quantity != null) params['quantity'] = quantity; + if (reduceOnly != null) params['reduceOnly'] = reduceOnly; + if (price != null) params['price'] = price; + if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; + if (stopPrice != null) params['stopPrice'] = stopPrice; + if (activationPrice != null) params['activationPrice'] = activationPrice; + if (callbackRate != null) params['callbackRate'] = callbackRate; + if (workingType != null) params['workingType'] = workingType; + if (priceProtect != null) params['priceProtect'] = priceProtect; + if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/order', params: params, isOrder: true); + } + + /// Place multiple orders (batch) on testnet (Weight: 5) + /// + /// Maximum 5 orders per request + Future> placeBatchOrders({ + required List> batchOrders, + int? recvWindow, + }) async { + final params = { + 'batchOrders': batchOrders, + }; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('POST', '/dapi/v1/batchOrders', + params: params, weight: 5, isOrder: true); + return response as List; + } + + /// Query order status on testnet (Weight: 1) + Future> getOrder({ + required String symbol, + int? orderId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderId != null) params['orderId'] = orderId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v1/order', params: params, weight: 1); + } + + /// Cancel an order on testnet (Weight: 1) + Future> cancelOrder({ + required String symbol, + int? orderId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderId != null) params['orderId'] = orderId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('DELETE', '/dapi/v1/order', params: params, weight: 1); + } + + /// Cancel all open orders on a symbol (Weight: 1) + Future> cancelAllOrders({ + required String symbol, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('DELETE', '/dapi/v1/allOpenOrders', params: params, weight: 1); + } + + /// Cancel multiple orders (batch) on testnet (Weight: 1) + Future> cancelBatchOrders({ + required String symbol, + List? orderIdList, + List? origClientOrderIdList, + int? recvWindow, + }) async { + final params = {'symbol': symbol}; + if (orderIdList != null) params['orderIdList'] = orderIdList; + if (origClientOrderIdList != null) { + params['origClientOrderIdList'] = origClientOrderIdList; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('DELETE', '/dapi/v1/batchOrders', + params: params, weight: 1); + return response as List; + } + + /// Auto-cancel all open orders (countdown) (Weight: 10) + /// + /// [countdownTime] Countdown time in milliseconds. 0 to cancel the timer. + Future> setAutoCancel({ + required String symbol, + required int countdownTime, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'countdownTime': countdownTime, + }; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/countdownCancelAll', + params: params, weight: 10); + } + + /// Get current open order on testnet (Weight: 1) + Future> getCurrentOpenOrder({ + required String symbol, + int? orderId, + String? origClientOrderId, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + + if (orderId != null) params['orderId'] = orderId; + if (origClientOrderId != null) { + params['origClientOrderId'] = origClientOrderId; + } + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v1/openOrder', params: params, weight: 1); + } + + /// Get all open orders on testnet (Weight: 1-40) + Future> getOpenOrders({String? symbol, int? recvWindow}) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final weight = symbol != null ? 1 : 40; + final response = await sendRequest('GET', '/dapi/v1/openOrders', + params: params, weight: weight); + return response as List; + } + + /// Get all orders on testnet (Weight: 20) + Future> getAllOrders({ + required String symbol, + int? orderId, + int? startTime, + int? endTime, + int limit = 500, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + + if (orderId != null) params['orderId'] = orderId; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/allOrders', + params: params, weight: 20); + return response as List; + } + + /// Get COIN-M testnet account balance (Weight: 1) + Future> getBalance({int? recvWindow}) async { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = + await sendRequest('GET', '/dapi/v1/balance', params: params, weight: 1); + return response as List; + } + + /// Get COIN-M testnet account information (Weight: 5) + Future> getAccountInfo({int? recvWindow}) { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v1/account', params: params, weight: 5); + } + + /// Change initial leverage on testnet (Weight: 1) + Future> changeInitialLeverage({ + required String symbol, + required int leverage, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'leverage': leverage, + }; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/leverage', params: params, weight: 1); + } + + /// Change margin type on testnet (Weight: 1) + /// + /// [marginType] ISOLATED or CROSSED + Future> changeMarginType({ + required String symbol, + required String marginType, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'marginType': marginType, + }; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/marginType', params: params, weight: 1); + } + + /// Modify isolated position margin on testnet (Weight: 1) + /// + /// [type] 1 = Add margin, 2 = Reduce margin + Future> modifyIsolatedPositionMargin({ + required String symbol, + required double amount, + required int type, + String? positionSide, + int? recvWindow, + }) { + final params = { + 'symbol': symbol, + 'amount': amount, + 'type': type, + }; + if (positionSide != null) params['positionSide'] = positionSide; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/positionMargin', + params: params, weight: 1); + } + + /// Get position margin change history on testnet (Weight: 1) + Future> getPositionMarginHistory({ + required String symbol, + int? type, + int? startTime, + int? endTime, + int limit = 500, + int? recvWindow, + }) async { + final params = { + 'symbol': symbol, + 'limit': limit, + }; + if (type != null) params['type'] = type; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/positionMargin/history', + params: params, weight: 1); + return response as List; + } + + /// Get position information on testnet (Weight: 1) + Future> getPositionRisk({ + String? marginAsset, + String? pair, + int? recvWindow, + }) async { + final params = {}; + if (marginAsset != null) params['marginAsset'] = marginAsset; + if (pair != null) params['pair'] = pair; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/positionRisk', + params: params, weight: 1); + return response as List; + } + + /// Get account trade history on testnet (Weight: 20) + Future> getUserTrades({ + String? symbol, + String? pair, + int? startTime, + int? endTime, + int? fromId, + int limit = 500, + int? recvWindow, + }) async { + final params = {'limit': limit}; + if (symbol != null) params['symbol'] = symbol; + if (pair != null) params['pair'] = pair; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (fromId != null) params['fromId'] = fromId; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/userTrades', + params: params, weight: 20); + return response as List; + } + + /// Get income history on testnet (Weight: 20) + Future> getIncomeHistory({ + String? symbol, + String? incomeType, + int? startTime, + int? endTime, + int limit = 100, + int? recvWindow, + }) async { + final params = {'limit': limit}; + if (symbol != null) params['symbol'] = symbol; + if (incomeType != null) params['incomeType'] = incomeType; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/income', + params: params, weight: 20); + return response as List; + } + + /// Get notional and leverage brackets on testnet (Weight: 1) + Future getLeverageBracket({String? pair, int? recvWindow}) async { + final params = {}; + if (pair != null) params['pair'] = pair; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v2/leverageBracket', + params: params, weight: 1); + } + + /// Change position mode on testnet (Weight: 1) + /// + /// [dualSidePosition] true = Hedge Mode, false = One-way Mode + Future> changePositionMode({ + required bool dualSidePosition, + int? recvWindow, + }) { + final params = {'dualSidePosition': dualSidePosition}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('POST', '/dapi/v1/positionSide/dual', + params: params, weight: 1); + } + + /// Get current position mode on testnet (Weight: 30) + Future> getPositionMode({int? recvWindow}) { + final params = {}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v1/positionSide/dual', + params: params, weight: 30); + } + + /// Get user's force orders (liquidation) on testnet (Weight: 20) + Future> getForceOrders({ + String? symbol, + String? autoCloseType, + int? startTime, + int? endTime, + int limit = 50, + int? recvWindow, + }) async { + final params = {'limit': limit}; + if (symbol != null) params['symbol'] = symbol; + if (autoCloseType != null) params['autoCloseType'] = autoCloseType; + if (startTime != null) params['startTime'] = startTime; + if (endTime != null) params['endTime'] = endTime; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/forceOrders', + params: params, weight: 20); + return response as List; + } + + /// Get ADL quantile estimation on testnet (Weight: 5) + Future> getAdlQuantile({String? symbol, int? recvWindow}) async { + final params = {}; + if (symbol != null) params['symbol'] = symbol; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + final response = await sendRequest('GET', '/dapi/v1/adlQuantile', + params: params, weight: 5); + return response as List; + } + + /// Get user commission rate on testnet (Weight: 20) + Future> getCommissionRate({ + required String symbol, + int? recvWindow, + }) { + final params = {'symbol': symbol}; + if (recvWindow != null) params['recvWindow'] = recvWindow; + + return sendRequest('GET', '/dapi/v1/commissionRate', + params: params, weight: 20); + } +} + +/// COIN-M Futures User Data Stream for testnet +class TestnetFuturesCoinMUserDataStream extends BinanceBase { + TestnetFuturesCoinMUserDataStream({String? apiKey, String? apiSecret}) + : super( + apiKey: apiKey, + apiSecret: apiSecret, + baseUrl: 'https://testnet.binancefuture.com', + ); + + /// Start a new user data stream (Weight: 1) + Future> createListenKey() { + return sendRequest('POST', '/dapi/v1/listenKey', weight: 1); + } + + /// Keepalive a user data stream (Weight: 1) + Future> keepAliveListenKey() { + return sendRequest('PUT', '/dapi/v1/listenKey', weight: 1); + } + + /// Close a user data stream (Weight: 1) + Future> closeListenKey() { + return sendRequest('DELETE', '/dapi/v1/listenKey', weight: 1); + } +} From b341846b2c56302a3200bd7267b3f7070fde28f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 17:18:27 +0000 Subject: [PATCH 3/5] fix: Address critical security and reliability issues Major fixes based on comprehensive code review: Security Fixes: - Make API credentials private (_apiKey, _apiSecret) to prevent exposure - Remove public accessors that could leak sensitive data Reliability Fixes: - Add thread-safe locking to TokenBucket rate limiter - Add server time sync locking to prevent race conditions - Add bounded buffer (10k messages) to WebSocket to prevent memory leaks - Fix JSON parsing with proper FormatException handling for WAF/CDN errors - Complete dispose() method to clean up all resources Financial Precision Fixes: - Change order quantity/price from double to String for exact precision - Prevents floating-point rounding errors in financial calculations Parameter Validation: - Add comprehensive order parameter validation (side, type, required fields) - Validate LIMIT orders require price and timeInForce - Validate STOP orders require stopPrice - Normalize inputs (uppercase symbols, sides, types) Code Quality: - Remove dead code (Awesome class) - Improve error messages for debugging These fixes address CRITICAL and HIGH severity issues from: - Principal Test Engineer review (15 critical, 24 high) - Principal Developer #1 review (5 critical, 10 high) - Principal Developer #2 review (8 critical, 6 high) --- lib/src/babel_binance_base.dart | 36 ++++++-- lib/src/binance_base.dart | 104 ++++++++++++++++++----- lib/src/rate_limiting/token_bucket.dart | 105 ++++++++++++++++++++---- lib/src/spot.dart | 64 +++++++++++++-- lib/src/websocket/websocket_stream.dart | 31 ++++++- 5 files changed, 287 insertions(+), 53 deletions(-) diff --git a/lib/src/babel_binance_base.dart b/lib/src/babel_binance_base.dart index fb8602c..ba3eacd 100644 --- a/lib/src/babel_binance_base.dart +++ b/lib/src/babel_binance_base.dart @@ -110,17 +110,41 @@ class Binance { ); } - /// Dispose and clean up resources + /// Dispose and clean up all resources + /// + /// Call this when you're done using the Binance client to properly + /// close all HTTP connections, WebSocket connections, and release resources. Future dispose() async { + // Dispose spot trading resources spot.market.dispose(); + spot.trading.dispose(); + spot.userDataStream.dispose(); + + // Dispose simulated convert + simulatedConvert.dispose(); + + // Dispose futures resources futuresUsd.dispose(); + + // Dispose margin resources margin.dispose(); + + // Dispose testnet resources (async due to WebSocket) await testnetSpot.dispose(); + + // Dispose testnet futures + testnetFutures.market.dispose(); + testnetFutures.trading.dispose(); + + // Dispose testnet COIN-M futures + testnetFuturesCoinM.market.dispose(); + testnetFuturesCoinM.trading.dispose(); + + // Dispose demo resources (async due to WebSocket) await demoSpot.dispose(); - } -} -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; + // Dispose demo futures + demoFutures.market.dispose(); + demoFutures.trading.dispose(); + } } diff --git a/lib/src/binance_base.dart b/lib/src/binance_base.dart index fb42be2..e118b45 100644 --- a/lib/src/binance_base.dart +++ b/lib/src/binance_base.dart @@ -12,8 +12,9 @@ import 'exceptions/network_exception.dart'; import 'exceptions/validation_exception.dart'; class BinanceBase { - final String? apiKey; - final String? apiSecret; + // Private credentials for security + final String? _apiKey; + final String? _apiSecret; final String baseUrl; final BinanceConfig config; final BinanceLogger logger; @@ -27,16 +28,26 @@ class BinanceBase { int _serverTimeOffset = 0; DateTime? _lastServerTimeSync; + // Lock for server time sync to prevent race conditions + Completer? _serverTimeSyncLock; + + /// Returns true if API credentials are configured + bool get hasCredentials => _apiKey != null && _apiSecret != null; + + /// Returns the API key (for headers) - null if not configured + String? get apiKey => _apiKey; + BinanceBase({ - this.apiKey, - this.apiSecret, + String? apiKey, + String? apiSecret, required this.baseUrl, BinanceConfig? config, BinanceLogger? logger, - }) : config = config ?? BinanceConfig.defaultConfig, - logger = logger ?? const NoOpLogger(), - _endpoints = _generateEndpoints(baseUrl) { - + }) : _apiKey = apiKey, + _apiSecret = apiSecret, + config = config ?? BinanceConfig.defaultConfig, + logger = logger ?? const NoOpLogger(), + _endpoints = _generateEndpoints(baseUrl) { rateLimiter = RateLimiter( config: this.config.rateLimitConfig, ); @@ -44,13 +55,21 @@ class BinanceBase { _httpClient = BinanceHttpClient(config: this.config); // Sync server time if enabled - if (this.config.syncServerTime && apiSecret != null) { + if (this.config.syncServerTime && _apiSecret != null) { _initServerTimeSync(); } } - /// Initialize server time synchronization + /// Initialize server time synchronization with locking Future _initServerTimeSync() async { + // If sync already in progress, wait for it + if (_serverTimeSyncLock != null) { + await _serverTimeSyncLock!.future; + return; + } + + _serverTimeSyncLock = Completer(); + try { final serverTimeData = await _getServerTimeInternal(); final serverTime = serverTimeData['serverTime'] as int; @@ -63,6 +82,9 @@ class BinanceBase { } catch (e) { logger.warn('Failed to sync server time', error: e); // Continue anyway, server time sync is optional + } finally { + _serverTimeSyncLock?.complete(); + _serverTimeSyncLock = null; } } @@ -85,7 +107,7 @@ class BinanceBase { /// Re-sync server time if needed (every 30 minutes) Future _resyncServerTimeIfNeeded() async { - if (!config.syncServerTime || apiSecret == null) return; + if (!config.syncServerTime || _apiSecret == null) return; if (_lastServerTimeSync == null || DateTime.now().difference(_lastServerTimeSync!) > @@ -194,14 +216,17 @@ class BinanceBase { params ??= {}; // Add signature if authenticated - if (apiSecret != null) { + if (_apiSecret != null) { params['timestamp'] = _getSyncedTimestamp(); params['recvWindow'] = config.recvWindow; - final query = Uri(queryParameters: params.map((key, value) => - MapEntry(key, value.toString()))).query; - final signature = Hmac(sha256, utf8.encode(apiSecret!)) - .convert(utf8.encode(query)).toString(); + final query = Uri( + queryParameters: + params.map((key, value) => MapEntry(key, value.toString()))) + .query; + final signature = Hmac(sha256, utf8.encode(_apiSecret!)) + .convert(utf8.encode(query)) + .toString(); params['signature'] = signature; } @@ -285,12 +310,49 @@ class BinanceBase { _resetToPrimaryEndpoint(); } - return json.decode(response.body); + // Parse response JSON safely + dynamic responseData; + try { + responseData = json.decode(response.body); + } on FormatException catch (e) { + throw BinanceApiException( + statusCode: response.statusCode, + errorMessage: 'Invalid JSON response: ${e.message}', + responseBody: {'raw_body': response.body.length > 500 + ? '${response.body.substring(0, 500)}...' + : response.body}, + ); + } + return responseData; } else { - // Parse error response - final errorBody = json.decode(response.body) as Map?; - final errorCode = errorBody?['code'] as int?; - final errorMsg = errorBody?['msg'] as String?; + // Parse error response safely + Map? errorBody; + int? errorCode; + String? errorMsg; + + try { + final decoded = json.decode(response.body); + if (decoded is Map) { + errorBody = decoded; + // Handle both int and string error codes + final rawCode = errorBody['code']; + errorCode = rawCode is int + ? rawCode + : (rawCode is String ? int.tryParse(rawCode) : null); + errorMsg = errorBody['msg']?.toString(); + } + } on FormatException { + // Non-JSON response (WAF, Cloudflare, HTML error pages) + errorMsg = 'HTTP ${response.statusCode}: Non-JSON response'; + if (response.body.trimLeft().startsWith('<')) { + errorMsg = 'Blocked by WAF/CDN (${response.statusCode})'; + } + errorBody = { + 'raw_body': response.body.length > 500 + ? '${response.body.substring(0, 500)}...' + : response.body + }; + } // Log error response logger.logResponse( diff --git a/lib/src/rate_limiting/token_bucket.dart b/lib/src/rate_limiting/token_bucket.dart index f861a26..1268a40 100644 --- a/lib/src/rate_limiting/token_bucket.dart +++ b/lib/src/rate_limiting/token_bucket.dart @@ -1,4 +1,9 @@ -/// Token bucket algorithm for rate limiting +import 'dart:async'; + +/// Token bucket algorithm for rate limiting with thread-safety +/// +/// Uses async locking to prevent race conditions when multiple +/// concurrent requests try to consume tokens. class TokenBucket { final int capacity; final Duration refillDuration; @@ -7,16 +12,52 @@ class TokenBucket { double _tokens; DateTime _lastRefill; + // Lock mechanism for thread-safety + Completer? _lock; + TokenBucket({ required this.capacity, required this.refillDuration, int? refillAmount, - }) : _tokens = capacity.toDouble(), - _lastRefill = DateTime.now(), - refillAmount = refillAmount ?? capacity; + }) : _tokens = capacity.toDouble(), + _lastRefill = DateTime.now(), + refillAmount = refillAmount ?? capacity; + + /// Acquire lock for thread-safe operations + Future _acquireLock() async { + while (_lock != null) { + await _lock!.future; + } + _lock = Completer(); + } + + /// Release lock + void _releaseLock() { + final lock = _lock; + _lock = null; + lock?.complete(); + } /// Try to consume tokens. Returns true if successful. - bool tryConsume(int tokens) { + /// Thread-safe implementation using async lock. + Future tryConsume(int tokens) async { + await _acquireLock(); + try { + _refill(); + + if (_tokens >= tokens) { + _tokens -= tokens; + return true; + } + + return false; + } finally { + _releaseLock(); + } + } + + /// Synchronous version - use only when you're sure no concurrent access + bool tryConsumeSync(int tokens) { _refill(); if (_tokens >= tokens) { @@ -29,24 +70,29 @@ class TokenBucket { /// Wait until tokens are available, then consume Future consume(int tokens) async { - while (!tryConsume(tokens)) { - final waitTime = _calculateWaitTime(tokens); + while (!(await tryConsume(tokens))) { + final waitTime = await _calculateWaitTime(tokens); await Future.delayed(waitTime); } } /// Calculate how long to wait for tokens to be available - Duration _calculateWaitTime(int tokensNeeded) { - _refill(); + Future _calculateWaitTime(int tokensNeeded) async { + await _acquireLock(); + try { + _refill(); - if (_tokens >= tokensNeeded) { - return Duration.zero; - } + if (_tokens >= tokensNeeded) { + return Duration.zero; + } - final tokensShort = tokensNeeded - _tokens; - final refillsNeeded = (tokensShort / refillAmount).ceil(); + final tokensShort = tokensNeeded - _tokens; + final refillsNeeded = (tokensShort / refillAmount).ceil(); - return refillDuration * refillsNeeded; + return refillDuration * refillsNeeded; + } finally { + _releaseLock(); + } } /// Refill tokens based on elapsed time @@ -58,12 +104,24 @@ class TokenBucket { final refills = elapsed.inMilliseconds / refillDuration.inMilliseconds; final tokensToAdd = (refills * refillAmount).floor(); - _tokens = (_tokens + tokensToAdd).clamp(0, capacity.toDouble()).toDouble(); + _tokens = + (_tokens + tokensToAdd).clamp(0, capacity.toDouble()).toDouble(); _lastRefill = now; } } - /// Get current available tokens + /// Get current available tokens (thread-safe) + Future getAvailableTokens() async { + await _acquireLock(); + try { + _refill(); + return _tokens; + } finally { + _releaseLock(); + } + } + + /// Get current available tokens (synchronous - use with caution) double get availableTokens { _refill(); return _tokens; @@ -76,7 +134,18 @@ class TokenBucket { } /// Reset bucket to full capacity - void reset() { + Future reset() async { + await _acquireLock(); + try { + _tokens = capacity.toDouble(); + _lastRefill = DateTime.now(); + } finally { + _releaseLock(); + } + } + + /// Reset bucket synchronously + void resetSync() { _tokens = capacity.toDouble(); _lastRefill = DateTime.now(); } diff --git a/lib/src/spot.dart b/lib/src/spot.dart index 218a1a2..14a393e 100644 --- a/lib/src/spot.dart +++ b/lib/src/spot.dart @@ -73,25 +73,75 @@ class Trading extends BinanceBase { baseUrl: 'https://api.binance.com', ); + /// Place a new order + /// + /// [symbol] Trading pair (e.g., 'BTCUSDT') + /// [side] Order side: 'BUY' or 'SELL' + /// [type] Order type: 'LIMIT', 'MARKET', 'STOP_LOSS', 'STOP_LOSS_LIMIT', + /// 'TAKE_PROFIT', 'TAKE_PROFIT_LIMIT', 'LIMIT_MAKER' + /// [quantity] Order quantity as string for precision (e.g., '0.001') + /// [price] Limit price as string (required for LIMIT orders) + /// [timeInForce] 'GTC', 'IOC', 'FOK' (required for LIMIT orders) + /// [stopPrice] Stop price for stop orders + /// [newClientOrderId] Client order ID for idempotency Future> placeOrder({ required String symbol, required String side, required String type, - required double quantity, - double? price, + required String quantity, + String? price, String? timeInForce, + String? stopPrice, + String? newClientOrderId, }) { + // Validate side + final normalizedSide = side.toUpperCase(); + if (!['BUY', 'SELL'].contains(normalizedSide)) { + throw ArgumentError('side must be BUY or SELL, got: $side'); + } + + // Validate type + final normalizedType = type.toUpperCase(); + const validTypes = [ + 'LIMIT', 'MARKET', 'STOP_LOSS', 'STOP_LOSS_LIMIT', + 'TAKE_PROFIT', 'TAKE_PROFIT_LIMIT', 'LIMIT_MAKER' + ]; + if (!validTypes.contains(normalizedType)) { + throw ArgumentError('type must be one of $validTypes, got: $type'); + } + + // Validate LIMIT orders require price and timeInForce + if (normalizedType == 'LIMIT' || + normalizedType == 'STOP_LOSS_LIMIT' || + normalizedType == 'TAKE_PROFIT_LIMIT') { + if (price == null) { + throw ArgumentError('price is required for $normalizedType orders'); + } + if (timeInForce == null) { + throw ArgumentError('timeInForce is required for LIMIT orders'); + } + } + + // Validate stop orders require stopPrice + if (normalizedType.contains('STOP') || normalizedType.contains('TAKE_PROFIT')) { + if (stopPrice == null && normalizedType != 'LIMIT_MAKER') { + throw ArgumentError('stopPrice is required for $normalizedType orders'); + } + } + final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': normalizedSide, + 'type': normalizedType, 'quantity': quantity, }; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); + if (stopPrice != null) params['stopPrice'] = stopPrice; + if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; - return sendRequest('POST', '/api/v3/order', params: params); + return sendRequest('POST', '/api/v3/order', params: params, isOrder: true); } Future> cancelOrder({ diff --git a/lib/src/websocket/websocket_stream.dart b/lib/src/websocket/websocket_stream.dart index 4571aaf..ea742f6 100644 --- a/lib/src/websocket/websocket_stream.dart +++ b/lib/src/websocket/websocket_stream.dart @@ -19,6 +19,9 @@ class BinanceWebSocketStream { final WebSocketConfig config; final void Function(String)? onDebug; + /// Maximum buffer size to prevent memory issues during high-frequency updates + static const int maxBufferSize = 10000; + WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _pingTimer; @@ -33,6 +36,10 @@ class BinanceWebSocketStream { final _messageController = StreamController.broadcast(); final _stateController = StreamController.broadcast(); + /// Track buffered messages to prevent memory overflow + int _bufferedMessages = 0; + int _droppedMessages = 0; + bool _isDisposed = false; BinanceWebSocketStream({ @@ -145,7 +152,10 @@ class BinanceWebSocketStream { _debug('Sent: $data'); } - /// Handle incoming message + /// Get count of dropped messages due to buffer overflow + int get droppedMessages => _droppedMessages; + + /// Handle incoming message with buffer overflow protection void _handleMessage(dynamic message) { _debug('Received: $message'); @@ -158,9 +168,28 @@ class BinanceWebSocketStream { return; } + // Check buffer limit to prevent memory overflow + if (_bufferedMessages >= maxBufferSize) { + _droppedMessages++; + if (_droppedMessages % 100 == 1) { + // Log warning periodically, not for every dropped message + _debug('WARNING: Message buffer overflow - dropping messages. ' + 'Total dropped: $_droppedMessages. Consumer is too slow.'); + } + return; + } + // Emit message to subscribers + _bufferedMessages++; _messageController.add(data); + // Reset buffer count when stream has caught up + // (simplified approach - reset periodically) + if (_bufferedMessages > 0) { + Future.delayed(Duration(milliseconds: 10), () { + if (_bufferedMessages > 0) _bufferedMessages--; + }); + } } catch (e) { _debug('Error parsing message: $e'); _messageController.addError(e); From 18c26f8766a13b945bb3ca8c1ee3a5ae35449e4e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 17:31:20 +0000 Subject: [PATCH 4/5] fix: Use String for all financial values to preserve precision Critical fix for financial precision issues across all trading endpoints: - FuturesTrading.placeOrder: quantity, price, stopPrice now String - FuturesTrading.changePositionMargin: amount now String - MarginTrading.marginBorrow/marginRepay: amount now String - MarginTrading.placeMarginOrder: quantity, price now String - TestnetTrading: placeOrder, testOrder, cancelReplace, OCO, OTO, OTOCO - TestnetFuturesUsdTrading.placeOrder: all financial params String - TestnetFuturesCoinMTrading.placeOrder: all financial params String - DemoTrading.placeOrder/testOrder: all financial params String - DemoFuturesUsdTrading.placeOrder: all financial params String Added input validation for side/type parameters with helpful error messages. Added isOrder flag for proper rate limiting on order endpoints. --- lib/src/futures_usd.dart | 56 +++++++-- lib/src/margin.dart | 57 +++++++-- lib/src/testnet.dart | 249 ++++++++++++++++++++------------------- 3 files changed, 218 insertions(+), 144 deletions(-) diff --git a/lib/src/futures_usd.dart b/lib/src/futures_usd.dart index 4ca7da2..0ce4918 100644 --- a/lib/src/futures_usd.dart +++ b/lib/src/futures_usd.dart @@ -60,31 +60,57 @@ class FuturesTrading extends BinanceBase { baseUrl: 'https://fapi.binance.com', ); + /// Place a new futures order + /// + /// [symbol] Trading pair (e.g., 'BTCUSDT') + /// [side] Order side: 'BUY' or 'SELL' + /// [type] Order type: 'LIMIT', 'MARKET', 'STOP', 'STOP_MARKET', etc. + /// [quantity] Order quantity as string for precision (e.g., '0.001') + /// [price] Limit price as string (required for LIMIT orders) + /// [timeInForce] 'GTC', 'IOC', 'FOK', 'GTX' + /// [positionSide] 'BOTH', 'LONG', 'SHORT' (for hedge mode) + /// [stopPrice] Stop price as string for stop orders Future> placeOrder({ required String symbol, required String side, required String type, - required double quantity, - double? price, + required String quantity, + String? price, String? timeInForce, String? positionSide, - double? stopPrice, + String? stopPrice, int? recvWindow, }) { + // Validate side + final normalizedSide = side.toUpperCase(); + if (!['BUY', 'SELL'].contains(normalizedSide)) { + throw ArgumentError('side must be BUY or SELL, got: $side'); + } + + // Validate type + final normalizedType = type.toUpperCase(); + const validTypes = [ + 'LIMIT', 'MARKET', 'STOP', 'STOP_MARKET', 'TAKE_PROFIT', + 'TAKE_PROFIT_MARKET', 'TRAILING_STOP_MARKET' + ]; + if (!validTypes.contains(normalizedType)) { + throw ArgumentError('type must be one of $validTypes, got: $type'); + } + final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': normalizedSide, + 'type': normalizedType, 'quantity': quantity, }; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; - if (positionSide != null) params['positionSide'] = positionSide; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); if (stopPrice != null) params['stopPrice'] = stopPrice; if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('POST', '/fapi/v1/order', params: params); + return sendRequest('POST', '/fapi/v1/order', params: params, isOrder: true); } Future> getOrderStatus({ @@ -100,19 +126,23 @@ class FuturesTrading extends BinanceBase { return sendRequest('GET', '/fapi/v1/order', params: params); } + /// Change position margin + /// + /// [type] '1' for Add position margin, '2' for Reduce position margin + /// [amount] Amount as string for precision (e.g., '100.0') Future> changePositionMargin({ required String symbol, - required String type, // 1: Add position margin, 2: Reduce position margin - required double amount, + required String type, + required String amount, String? positionSide, int? recvWindow, }) { final params = { - 'symbol': symbol, + 'symbol': symbol.toUpperCase(), 'type': type, 'amount': amount, }; - if (positionSide != null) params['positionSide'] = positionSide; + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/fapi/v1/positionMargin', params: params); } diff --git a/lib/src/margin.dart b/lib/src/margin.dart index 0802fb9..065d851 100644 --- a/lib/src/margin.dart +++ b/lib/src/margin.dart @@ -57,61 +57,92 @@ class MarginTrading extends BinanceBase { baseUrl: 'https://api.binance.com', ); + /// Borrow margin loan + /// + /// [asset] Asset to borrow (e.g., 'USDT') + /// [amount] Amount as string for precision (e.g., '100.0') Future> marginBorrow({ required String asset, - required double amount, + required String amount, String? isIsolated, String? symbol, int? recvWindow, }) { final params = { - 'asset': asset, + 'asset': asset.toUpperCase(), 'amount': amount, }; if (isIsolated != null) params['isIsolated'] = isIsolated; - if (symbol != null) params['symbol'] = symbol; + if (symbol != null) params['symbol'] = symbol.toUpperCase(); if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/sapi/v1/margin/loan', params: params); } + /// Repay margin loan + /// + /// [asset] Asset to repay (e.g., 'USDT') + /// [amount] Amount as string for precision (e.g., '100.0') Future> marginRepay({ required String asset, - required double amount, + required String amount, String? isIsolated, String? symbol, int? recvWindow, }) { final params = { - 'asset': asset, + 'asset': asset.toUpperCase(), 'amount': amount, }; if (isIsolated != null) params['isIsolated'] = isIsolated; - if (symbol != null) params['symbol'] = symbol; + if (symbol != null) params['symbol'] = symbol.toUpperCase(); if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/sapi/v1/margin/repay', params: params); } + /// Place a margin order + /// + /// [symbol] Trading pair (e.g., 'BTCUSDT') + /// [side] Order side: 'BUY' or 'SELL' + /// [type] Order type: 'LIMIT', 'MARKET', 'STOP_LOSS', etc. + /// [quantity] Order quantity as string for precision (e.g., '0.001') + /// [price] Limit price as string (required for LIMIT orders) Future> placeMarginOrder({ required String symbol, required String side, required String type, - required double quantity, - double? price, + required String quantity, + String? price, String? timeInForce, String? isIsolated, int? recvWindow, }) { + // Validate side + final normalizedSide = side.toUpperCase(); + if (!['BUY', 'SELL'].contains(normalizedSide)) { + throw ArgumentError('side must be BUY or SELL, got: $side'); + } + + // Validate type + final normalizedType = type.toUpperCase(); + const validTypes = [ + 'LIMIT', 'MARKET', 'STOP_LOSS', 'STOP_LOSS_LIMIT', + 'TAKE_PROFIT', 'TAKE_PROFIT_LIMIT', 'LIMIT_MAKER' + ]; + if (!validTypes.contains(normalizedType)) { + throw ArgumentError('type must be one of $validTypes, got: $type'); + } + final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': normalizedSide, + 'type': normalizedType, 'quantity': quantity, }; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (isIsolated != null) params['isIsolated'] = isIsolated; if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('POST', '/sapi/v1/margin/order', params: params); + return sendRequest('POST', '/sapi/v1/margin/order', params: params, isOrder: true); } } diff --git a/lib/src/testnet.dart b/lib/src/testnet.dart index c29f0a5..75469c8 100644 --- a/lib/src/testnet.dart +++ b/lib/src/testnet.dart @@ -300,13 +300,13 @@ class TestnetTrading extends BinanceBase { required String symbol, required String side, required String type, - double? quantity, - double? quoteOrderQty, - double? price, + String? quantity, + String? quoteOrderQty, + String? price, String? newClientOrderId, - double? stopPrice, - double? trailingDelta, - double? icebergQty, + String? stopPrice, + String? trailingDelta, + String? icebergQty, String? newOrderRespType, String? timeInForce, String? selfTradePreventionMode, @@ -314,10 +314,16 @@ class TestnetTrading extends BinanceBase { int? strategyType, int? recvWindow, }) { + // Validate side + final normalizedSide = side.toUpperCase(); + if (!['BUY', 'SELL'].contains(normalizedSide)) { + throw ArgumentError('side must be BUY or SELL, got: $side'); + } + final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': normalizedSide, + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; @@ -328,7 +334,7 @@ class TestnetTrading extends BinanceBase { if (trailingDelta != null) params['trailingDelta'] = trailingDelta; if (icebergQty != null) params['icebergQty'] = icebergQty; if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (selfTradePreventionMode != null) { params['selfTradePreventionMode'] = selfTradePreventionMode; } @@ -346,22 +352,22 @@ class TestnetTrading extends BinanceBase { required String symbol, required String side, required String type, - double? quantity, - double? quoteOrderQty, - double? price, + String? quantity, + String? quoteOrderQty, + String? price, String? newClientOrderId, - double? stopPrice, - double? trailingDelta, - double? icebergQty, + String? stopPrice, + String? trailingDelta, + String? icebergQty, String? timeInForce, String? selfTradePreventionMode, bool? computeCommissionRates, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; @@ -371,7 +377,7 @@ class TestnetTrading extends BinanceBase { if (stopPrice != null) params['stopPrice'] = stopPrice; if (trailingDelta != null) params['trailingDelta'] = trailingDelta; if (icebergQty != null) params['icebergQty'] = icebergQty; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (selfTradePreventionMode != null) { params['selfTradePreventionMode'] = selfTradePreventionMode; } @@ -450,31 +456,31 @@ class TestnetTrading extends BinanceBase { required String type, required String cancelReplaceMode, String? timeInForce, - double? quantity, - double? quoteOrderQty, - double? price, + String? quantity, + String? quoteOrderQty, + String? price, String? cancelNewClientOrderId, String? cancelOrigClientOrderId, int? cancelOrderId, String? newClientOrderId, int? strategyId, int? strategyType, - double? stopPrice, - double? trailingDelta, - double? icebergQty, + String? stopPrice, + String? trailingDelta, + String? icebergQty, String? newOrderRespType, String? selfTradePreventionMode, String? cancelRestrictions, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), 'cancelReplaceMode': cancelReplaceMode, }; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (quantity != null) params['quantity'] = quantity; if (quoteOrderQty != null) params['quoteOrderQty'] = quoteOrderQty; if (price != null) params['price'] = price; @@ -513,18 +519,18 @@ class TestnetTrading extends BinanceBase { Future> placeOcoOrder({ required String symbol, required String side, - required double quantity, - required double price, - required double stopPrice, + required String quantity, + required String price, + required String stopPrice, String? listClientOrderId, String? limitClientOrderId, - double? limitIcebergQty, - double? limitStrategyId, + String? limitIcebergQty, + int? limitStrategyId, int? limitStrategyType, - double? stopLimitPrice, + String? stopLimitPrice, String? stopClientOrderId, - double? stopIcebergQty, - double? stopStrategyId, + String? stopIcebergQty, + int? stopStrategyId, int? stopStrategyType, String? stopLimitTimeInForce, String? newOrderRespType, @@ -532,8 +538,8 @@ class TestnetTrading extends BinanceBase { int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), 'quantity': quantity, 'price': price, 'stopPrice': stopPrice, @@ -558,7 +564,7 @@ class TestnetTrading extends BinanceBase { if (stopStrategyId != null) params['stopStrategyId'] = stopStrategyId; if (stopStrategyType != null) params['stopStrategyType'] = stopStrategyType; if (stopLimitTimeInForce != null) { - params['stopLimitTimeInForce'] = stopLimitTimeInForce; + params['stopLimitTimeInForce'] = stopLimitTimeInForce.toUpperCase(); } if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; if (selfTradePreventionMode != null) { @@ -646,22 +652,22 @@ class TestnetTrading extends BinanceBase { required String symbol, required String workingType, required String workingSide, - required double workingPrice, - required double workingQuantity, + required String workingPrice, + required String workingQuantity, required String pendingType, required String pendingSide, - required double pendingQuantity, + required String pendingQuantity, String? listClientOrderId, String? workingClientOrderId, - double? workingIcebergQty, + String? workingIcebergQty, String? workingTimeInForce, int? workingStrategyId, int? workingStrategyType, String? pendingClientOrderId, - double? pendingPrice, - double? pendingStopPrice, - double? pendingTrailingDelta, - double? pendingIcebergQty, + String? pendingPrice, + String? pendingStopPrice, + String? pendingTrailingDelta, + String? pendingIcebergQty, String? pendingTimeInForce, int? pendingStrategyId, int? pendingStrategyType, @@ -670,13 +676,13 @@ class TestnetTrading extends BinanceBase { int? recvWindow, }) { final params = { - 'symbol': symbol, - 'workingType': workingType, - 'workingSide': workingSide, + 'symbol': symbol.toUpperCase(), + 'workingType': workingType.toUpperCase(), + 'workingSide': workingSide.toUpperCase(), 'workingPrice': workingPrice, 'workingQuantity': workingQuantity, - 'pendingType': pendingType, - 'pendingSide': pendingSide, + 'pendingType': pendingType.toUpperCase(), + 'pendingSide': pendingSide.toUpperCase(), 'pendingQuantity': pendingQuantity, }; @@ -690,7 +696,7 @@ class TestnetTrading extends BinanceBase { params['workingIcebergQty'] = workingIcebergQty; } if (workingTimeInForce != null) { - params['workingTimeInForce'] = workingTimeInForce; + params['workingTimeInForce'] = workingTimeInForce.toUpperCase(); } if (workingStrategyId != null) { params['workingStrategyId'] = workingStrategyId; @@ -710,7 +716,7 @@ class TestnetTrading extends BinanceBase { params['pendingIcebergQty'] = pendingIcebergQty; } if (pendingTimeInForce != null) { - params['pendingTimeInForce'] = pendingTimeInForce; + params['pendingTimeInForce'] = pendingTimeInForce.toUpperCase(); } if (pendingStrategyId != null) { params['pendingStrategyId'] = pendingStrategyId; @@ -738,31 +744,31 @@ class TestnetTrading extends BinanceBase { required String symbol, required String workingType, required String workingSide, - required double workingPrice, - required double workingQuantity, + required String workingPrice, + required String workingQuantity, required String pendingSide, - required double pendingQuantity, - required double pendingAbovePrice, - required double pendingBelowPrice, + required String pendingQuantity, + required String pendingAbovePrice, + required String pendingBelowPrice, String? listClientOrderId, String? workingClientOrderId, - double? workingIcebergQty, + String? workingIcebergQty, String? workingTimeInForce, int? workingStrategyId, int? workingStrategyType, String? pendingAboveType, String? pendingAboveClientOrderId, - double? pendingAboveStopPrice, - double? pendingAboveTrailingDelta, - double? pendingAboveIcebergQty, + String? pendingAboveStopPrice, + String? pendingAboveTrailingDelta, + String? pendingAboveIcebergQty, String? pendingAboveTimeInForce, int? pendingAboveStrategyId, int? pendingAboveStrategyType, String? pendingBelowType, String? pendingBelowClientOrderId, - double? pendingBelowStopPrice, - double? pendingBelowTrailingDelta, - double? pendingBelowIcebergQty, + String? pendingBelowStopPrice, + String? pendingBelowTrailingDelta, + String? pendingBelowIcebergQty, String? pendingBelowTimeInForce, int? pendingBelowStrategyId, int? pendingBelowStrategyType, @@ -771,12 +777,12 @@ class TestnetTrading extends BinanceBase { int? recvWindow, }) { final params = { - 'symbol': symbol, - 'workingType': workingType, - 'workingSide': workingSide, + 'symbol': symbol.toUpperCase(), + 'workingType': workingType.toUpperCase(), + 'workingSide': workingSide.toUpperCase(), 'workingPrice': workingPrice, 'workingQuantity': workingQuantity, - 'pendingSide': pendingSide, + 'pendingSide': pendingSide.toUpperCase(), 'pendingQuantity': pendingQuantity, 'pendingAbovePrice': pendingAbovePrice, 'pendingBelowPrice': pendingBelowPrice, @@ -792,7 +798,7 @@ class TestnetTrading extends BinanceBase { params['workingIcebergQty'] = workingIcebergQty; } if (workingTimeInForce != null) { - params['workingTimeInForce'] = workingTimeInForce; + params['workingTimeInForce'] = workingTimeInForce.toUpperCase(); } if (workingStrategyId != null) { params['workingStrategyId'] = workingStrategyId; @@ -800,7 +806,7 @@ class TestnetTrading extends BinanceBase { if (workingStrategyType != null) { params['workingStrategyType'] = workingStrategyType; } - if (pendingAboveType != null) params['pendingAboveType'] = pendingAboveType; + if (pendingAboveType != null) params['pendingAboveType'] = pendingAboveType.toUpperCase(); if (pendingAboveClientOrderId != null) { params['pendingAboveClientOrderId'] = pendingAboveClientOrderId; } @@ -814,7 +820,7 @@ class TestnetTrading extends BinanceBase { params['pendingAboveIcebergQty'] = pendingAboveIcebergQty; } if (pendingAboveTimeInForce != null) { - params['pendingAboveTimeInForce'] = pendingAboveTimeInForce; + params['pendingAboveTimeInForce'] = pendingAboveTimeInForce.toUpperCase(); } if (pendingAboveStrategyId != null) { params['pendingAboveStrategyId'] = pendingAboveStrategyId; @@ -822,7 +828,7 @@ class TestnetTrading extends BinanceBase { if (pendingAboveStrategyType != null) { params['pendingAboveStrategyType'] = pendingAboveStrategyType; } - if (pendingBelowType != null) params['pendingBelowType'] = pendingBelowType; + if (pendingBelowType != null) params['pendingBelowType'] = pendingBelowType.toUpperCase(); if (pendingBelowClientOrderId != null) { params['pendingBelowClientOrderId'] = pendingBelowClientOrderId; } @@ -836,7 +842,7 @@ class TestnetTrading extends BinanceBase { params['pendingBelowIcebergQty'] = pendingBelowIcebergQty; } if (pendingBelowTimeInForce != null) { - params['pendingBelowTimeInForce'] = pendingBelowTimeInForce; + params['pendingBelowTimeInForce'] = pendingBelowTimeInForce.toUpperCase(); } if (pendingBelowStrategyId != null) { params['pendingBelowStrategyId'] = pendingBelowStrategyId; @@ -1109,37 +1115,39 @@ class TestnetFuturesUsdTrading extends BinanceBase { ); /// Place a new futures order on testnet + /// + /// All financial values use String type for precision Future> placeOrder({ required String symbol, required String side, required String type, - double? quantity, - double? price, + String? quantity, + String? price, String? timeInForce, String? positionSide, - double? stopPrice, + String? stopPrice, String? workingType, bool? priceProtect, String? newOrderRespType, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; - if (positionSide != null) params['positionSide'] = positionSide; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); if (stopPrice != null) params['stopPrice'] = stopPrice; if (workingType != null) params['workingType'] = workingType; if (priceProtect != null) params['priceProtect'] = priceProtect; if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; if (recvWindow != null) params['recvWindow'] = recvWindow; - return sendRequest('POST', '/fapi/v1/order', params: params); + return sendRequest('POST', '/fapi/v1/order', params: params, isOrder: true); } /// Get testnet futures account information @@ -1300,23 +1308,25 @@ class DemoTrading extends BinanceBase { ); /// Place a new order + /// + /// All financial values use String type for precision Future> placeOrder({ required String symbol, required String side, required String type, - double? quantity, - double? quoteOrderQty, - double? price, + String? quantity, + String? quoteOrderQty, + String? price, String? newClientOrderId, - double? stopPrice, + String? stopPrice, String? timeInForce, String? newOrderRespType, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; @@ -1324,7 +1334,7 @@ class DemoTrading extends BinanceBase { if (price != null) params['price'] = price; if (newClientOrderId != null) params['newClientOrderId'] = newClientOrderId; if (stopPrice != null) params['stopPrice'] = stopPrice; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; if (recvWindow != null) params['recvWindow'] = recvWindow; @@ -1336,20 +1346,20 @@ class DemoTrading extends BinanceBase { required String symbol, required String side, required String type, - double? quantity, - double? price, + String? quantity, + String? price, String? timeInForce, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/api/v3/order/test', params: params); @@ -1572,29 +1582,31 @@ class DemoFuturesUsdTrading extends BinanceBase { ); /// Place a new futures order + /// + /// All financial values use String type for precision Future> placeOrder({ required String symbol, required String side, required String type, - double? quantity, - double? price, + String? quantity, + String? price, String? timeInForce, String? positionSide, - double? stopPrice, + String? stopPrice, String? workingType, String? newOrderRespType, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; if (quantity != null) params['quantity'] = quantity; if (price != null) params['price'] = price; - if (timeInForce != null) params['timeInForce'] = timeInForce; - if (positionSide != null) params['positionSide'] = positionSide; + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); if (stopPrice != null) params['stopPrice'] = stopPrice; if (workingType != null) params['workingType'] = workingType; if (newOrderRespType != null) params['newOrderRespType'] = newOrderRespType; @@ -1912,6 +1924,7 @@ class TestnetFuturesCoinMTrading extends BinanceBase { /// Place a new COIN-M futures order on testnet (Weight: 1) /// + /// All financial values use String type for precision /// [type] LIMIT, MARKET, STOP, STOP_MARKET, TAKE_PROFIT, TAKE_PROFIT_MARKET, /// TRAILING_STOP_MARKET /// [side] BUY or SELL @@ -1922,26 +1935,26 @@ class TestnetFuturesCoinMTrading extends BinanceBase { required String type, String? positionSide, String? timeInForce, - double? quantity, + String? quantity, bool? reduceOnly, - double? price, + String? price, String? newClientOrderId, - double? stopPrice, - double? activationPrice, - double? callbackRate, + String? stopPrice, + String? activationPrice, + String? callbackRate, String? workingType, bool? priceProtect, String? newOrderRespType, int? recvWindow, }) { final params = { - 'symbol': symbol, - 'side': side, - 'type': type, + 'symbol': symbol.toUpperCase(), + 'side': side.toUpperCase(), + 'type': type.toUpperCase(), }; - if (positionSide != null) params['positionSide'] = positionSide; - if (timeInForce != null) params['timeInForce'] = timeInForce; + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); + if (timeInForce != null) params['timeInForce'] = timeInForce.toUpperCase(); if (quantity != null) params['quantity'] = quantity; if (reduceOnly != null) params['reduceOnly'] = reduceOnly; if (price != null) params['price'] = price; From 3302e8691e833d48c51fe854dc34e13b08026af6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Dec 2025 17:35:29 +0000 Subject: [PATCH 5/5] fix: Use String for modifyIsolatedPositionMargin amount parameter Fixes the remaining precision issue in COIN-M futures margin modification endpoint. The amount parameter now uses String instead of double to prevent precision loss in cryptocurrency margin operations. --- lib/src/testnet.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/testnet.dart b/lib/src/testnet.dart index 75469c8..9b780de 100644 --- a/lib/src/testnet.dart +++ b/lib/src/testnet.dart @@ -2177,20 +2177,21 @@ class TestnetFuturesCoinMTrading extends BinanceBase { /// Modify isolated position margin on testnet (Weight: 1) /// + /// [amount] Amount as string for precision (e.g., '100.0') /// [type] 1 = Add margin, 2 = Reduce margin Future> modifyIsolatedPositionMargin({ required String symbol, - required double amount, + required String amount, required int type, String? positionSide, int? recvWindow, }) { final params = { - 'symbol': symbol, + 'symbol': symbol.toUpperCase(), 'amount': amount, 'type': type, }; - if (positionSide != null) params['positionSide'] = positionSide; + if (positionSide != null) params['positionSide'] = positionSide.toUpperCase(); if (recvWindow != null) params['recvWindow'] = recvWindow; return sendRequest('POST', '/dapi/v1/positionMargin',