Skip to content

Commit 3ce7b9c

Browse files
feat: add demo trading support (#1610)
* update gitignore * bump version * feat: add demo trading support * update url * add futures helper methods * update conf test * fix missing url * 8911 * rm proxy none * update ws_futures_demo_url * skip failing test and update options symbol * remove 3.7 * temporarly only test 3.12 * add several fixes including parallel testing * fix failing tests * run all tox versions * update proxy on github action * add timeout * fix ws proxy * add proxy fix for async ws client * add fix for for options tests * fix failing tests * fix lint * run all tox versions * add max parallel to 1 * skip ws proxy for py37 * comment options test for py37 * remove 3.7 * increase max parallel to 2 --------- Co-authored-by: Pablo <[email protected]>
1 parent efc2f5c commit 3ce7b9c

24 files changed

+427
-115
lines changed

.github/workflows/python-app.yml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ jobs:
1818
runs-on: ubuntu-latest
1919
timeout-minutes: 5
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v5
2222
- name: Set up Python
23-
uses: actions/setup-python@v5
23+
uses: actions/setup-python@v6
2424
with:
2525
python-version: '3.9'
2626
- name: Install Ruff
@@ -31,49 +31,49 @@ jobs:
3131
run: ruff format --check .
3232
continue-on-error: true
3333

34-
build:
34+
test:
3535
needs: lint
3636
runs-on: ubuntu-22.04
3737
timeout-minutes: 40
38+
strategy:
39+
max-parallel: 2
40+
matrix:
41+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
3842
env:
39-
PROXY: "http://51.83.140.52:16301"
43+
PROXY: "http://188.245.226.105:8911"
4044
TEST_TESTNET: "true"
4145
TEST_API_KEY: "u4L8MG2DbshTfTzkx2Xm7NfsHHigvafxeC29HrExEmah1P8JhxXkoOu6KntLICUc"
4246
TEST_API_SECRET: "hBZEqhZUUS6YZkk7AIckjJ3iLjrgEFr5CRtFPp5gjzkrHKKC9DAv4OH25PlT6yq5"
4347
TEST_FUTURES_API_KEY: "227719da8d8499e8d3461587d19f259c0b39c2b462a77c9b748a6119abd74401"
4448
TEST_FUTURES_API_SECRET: "b14b935f9cfacc5dec829008733c40da0588051f29a44625c34967b45c11d73c"
45-
strategy:
46-
max-parallel: 1
47-
matrix:
48-
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
4949
steps:
50-
- uses: actions/checkout@v4
50+
- uses: actions/checkout@v5
5151
- name: Checking env
5252
run: |
5353
echo "PROXY: $PROXY"
54-
env
55-
- name: Set up Python ${{ matrix.python }}
56-
uses: actions/setup-python@v5
54+
echo "Python version: ${{ matrix.python-version }}"
55+
- name: Set up Python ${{ matrix.python-version }}
56+
uses: actions/setup-python@v6
5757
with:
58-
python-version: ${{ matrix.python }}
58+
python-version: ${{ matrix.python-version }}
5959
cache: 'pip'
6060
- name: Install dependencies
6161
run: |
6262
python -m pip install --upgrade pip
6363
pip install pytest pytest-cov pyright tox
6464
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
6565
if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi
66-
- name: Type check with pyright
66+
- name: Type check with pyright (Python 3.12 only)
67+
if: matrix.python-version == '3.12'
6768
run: pyright
6869
- name: Test with tox
6970
run: tox -e py
7071
- name: Coveralls Parallel
7172
uses: coverallsapp/github-action@v2
7273
with:
73-
flag-name: run-${{ join(matrix.*, '-') }}
7474
parallel: true
7575
finish:
76-
needs: build
76+
needs: test
7777
if: ${{ always() }}
7878
runs-on: ubuntu-latest
7979
timeout-minutes: 5

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ Features
6363

6464
- Implementation of all General, Market Data and Account endpoints.
6565
- Asyncio implementation
66-
- Testnet support for Spot, Futures and Vanilla Options
66+
- Demo trading support (by providing demo=True)
67+
- Testnet support for Spot, Futures and Vanilla Options (deprecated)
6768
- Simple handling of authentication include RSA and EDDSA keys
6869
- No need to generate timestamps yourself, the wrapper does it for you
6970
- RecvWindow sent by default

binance/async_client.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(
3131
tld: str = "com",
3232
base_endpoint: str = BaseClient.BASE_ENDPOINT_DEFAULT,
3333
testnet: bool = False,
34+
demo: bool = False,
3435
loop=None,
3536
session_params: Optional[Dict[str, Any]] = None,
3637
private_key: Optional[Union[str, Path]] = None,
@@ -41,13 +42,23 @@ def __init__(
4142
self.https_proxy = https_proxy
4243
self.loop = loop or get_loop()
4344
self._session_params: Dict[str, Any] = session_params or {}
45+
46+
# Convert https_proxy to requests_params format for BaseClient
47+
if https_proxy and requests_params is None:
48+
requests_params = {'proxies': {'http': https_proxy, 'https': https_proxy}}
49+
elif https_proxy and requests_params is not None:
50+
if 'proxies' not in requests_params:
51+
requests_params['proxies'] = {}
52+
requests_params['proxies'].update({'http': https_proxy, 'https': https_proxy})
53+
4454
super().__init__(
4555
api_key,
4656
api_secret,
4757
requests_params,
4858
tld,
4959
base_endpoint,
5060
testnet,
61+
demo,
5162
private_key,
5263
private_key_pass,
5364
time_unit=time_unit,
@@ -62,6 +73,7 @@ async def create(
6273
tld: str = "com",
6374
base_endpoint: str = BaseClient.BASE_ENDPOINT_DEFAULT,
6475
testnet: bool = False,
76+
demo: bool = False,
6577
loop=None,
6678
session_params: Optional[Dict[str, Any]] = None,
6779
private_key: Optional[Union[str, Path]] = None,
@@ -76,6 +88,7 @@ async def create(
7688
tld,
7789
base_endpoint,
7890
testnet,
91+
demo,
7992
loop,
8093
session_params,
8194
private_key,
@@ -151,6 +164,9 @@ async def _request(
151164
url_encoded_data = urlencode(dict_data)
152165
data = f"{url_encoded_data}&signature={signature}"
153166

167+
# Remove proxies from kwargs since aiohttp uses 'proxy' parameter instead
168+
kwargs.pop('proxies', None)
169+
154170
async with getattr(self.session, method)(
155171
yarl.URL(uri, encoded=True),
156172
proxy=self.https_proxy,
@@ -1876,6 +1892,77 @@ async def futures_create_order(self, **params):
18761892
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
18771893
return await self._request_futures_api("post", "order", True, data=params)
18781894

1895+
async def futures_limit_order(self, **params):
1896+
"""Send in a new futures limit order.
1897+
1898+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1899+
1900+
"""
1901+
if "newClientOrderId" not in params:
1902+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1903+
params["type"] = "LIMIT"
1904+
return await self._request_futures_api("post", "order", True, data=params)
1905+
1906+
async def futures_market_order(self, **params):
1907+
"""Send in a new futures market order.
1908+
1909+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1910+
1911+
"""
1912+
if "newClientOrderId" not in params:
1913+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1914+
params["type"] = "MARKET"
1915+
return await self._request_futures_api("post", "order", True, data=params)
1916+
1917+
1918+
async def futures_limit_buy_order(self, **params):
1919+
"""Send in a new futures limit buy order.
1920+
1921+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1922+
1923+
"""
1924+
if "newClientOrderId" not in params:
1925+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1926+
params["side"] = "BUY"
1927+
params["type"] = "LIMIT"
1928+
return await self._request_futures_api("post", "order", True, data=params)
1929+
1930+
async def futures_limit_sell_order(self, **params):
1931+
"""Send in a new futures limit sell order.
1932+
1933+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1934+
1935+
"""
1936+
if "newClientOrderId" not in params:
1937+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1938+
params["side"] = "SELL"
1939+
params["type"] = "LIMIT"
1940+
return await self._request_futures_api("post", "order", True, data=params)
1941+
1942+
async def futures_market_buy_order(self, **params):
1943+
"""Send in a new futures market buy order.
1944+
1945+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1946+
1947+
"""
1948+
if "newClientOrderId" not in params:
1949+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1950+
params["side"] = "BUY"
1951+
params["type"] = "MARKET"
1952+
return await self._request_futures_api("post", "order", True, data=params)
1953+
1954+
async def futures_market_sell_order(self, **params):
1955+
"""Send in a new futures market sell order.
1956+
1957+
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api
1958+
1959+
"""
1960+
if "newClientOrderId" not in params:
1961+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1962+
params["side"] = "SELL"
1963+
params["type"] = "MARKET"
1964+
return await self._request_futures_api("post", "order", True, data=params)
1965+
18791966
async def futures_modify_order(self, **params):
18801967
"""Modify an existing order. Currently only LIMIT order modification is supported.
18811968

binance/base_client.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,28 @@
2222
class BaseClient:
2323
API_URL = "https://api{}.binance.{}/api"
2424
API_TESTNET_URL = "https://testnet.binance.vision/api"
25+
API_DEMO_URL = "https://demo-api.binance.com/api"
2526
MARGIN_API_URL = "https://api{}.binance.{}/sapi"
2627
WEBSITE_URL = "https://www.binance.{}"
2728
FUTURES_URL = "https://fapi.binance.{}/fapi"
2829
FUTURES_TESTNET_URL = "https://testnet.binancefuture.com/fapi"
30+
FUTURES_DEMO_URL = "https://demo-fapi.binance.com/fapi"
2931
FUTURES_DATA_URL = "https://fapi.binance.{}/futures/data"
3032
FUTURES_DATA_TESTNET_URL = "https://testnet.binancefuture.com/futures/data"
3133
FUTURES_COIN_URL = "https://dapi.binance.{}/dapi"
3234
FUTURES_COIN_TESTNET_URL = "https://testnet.binancefuture.com/dapi"
35+
FUTURES_COIN_DEMO_URL = "https://demo-dapi.binance.com/dapi"
3336
FUTURES_COIN_DATA_URL = "https://dapi.binance.{}/futures/data"
3437
FUTURES_COIN_DATA_TESTNET_URL = "https://testnet.binancefuture.com/futures/data"
3538
OPTIONS_URL = "https://eapi.binance.{}/eapi"
3639
OPTIONS_TESTNET_URL = "https://testnet.binanceops.{}/eapi"
3740
PAPI_URL = "https://papi.binance.{}/papi"
3841
WS_API_URL = "wss://ws-api.binance.{}/ws-api/v3"
3942
WS_API_TESTNET_URL = "wss://ws-api.testnet.binance.vision/ws-api/v3"
43+
WS_API_DEMO_URL = "wss://demo-ws-api.binance.com/ws-api/v3"
4044
WS_FUTURES_URL = "wss://ws-fapi.binance.{}/ws-fapi/v1"
4145
WS_FUTURES_TESTNET_URL = "wss://testnet.binancefuture.com/ws-fapi/v1"
46+
WS_FUTURES_DEMO_URL = "wss://testnet.binancefuture.com/ws-fapi/v1"
4247
PUBLIC_API_VERSION = "v3"
4348
PRIVATE_API_VERSION = "v3"
4449
MARGIN_API_VERSION = "v1"
@@ -157,6 +162,7 @@ def __init__(
157162
tld: str = "com",
158163
base_endpoint: str = BASE_ENDPOINT_DEFAULT,
159164
testnet: bool = False,
165+
demo: bool = False,
160166
private_key: Optional[Union[str, Path]] = None,
161167
private_key_pass: Optional[str] = None,
162168
loop: Optional[asyncio.AbstractEventLoop] = None,
@@ -201,15 +207,27 @@ def __init__(
201207
self._requests_params = requests_params
202208
self.response = None
203209
self.testnet = testnet
210+
self.demo = demo
204211
self.timestamp_offset = 0
205-
ws_api_url = self.WS_API_TESTNET_URL if testnet else self.WS_API_URL.format(tld)
212+
ws_api_url = self.WS_API_URL.format(tld)
213+
if testnet:
214+
ws_api_url = self.WS_API_TESTNET_URL
215+
elif demo:
216+
ws_api_url = self.WS_API_DEMO_URL
206217
if self.TIME_UNIT:
207218
ws_api_url += f"?timeUnit={self.TIME_UNIT}"
208-
self.ws_api = WebsocketAPI(url=ws_api_url, tld=tld)
209-
ws_future_url = (
210-
self.WS_FUTURES_TESTNET_URL if testnet else self.WS_FUTURES_URL.format(tld)
211-
)
212-
self.ws_future = WebsocketAPI(url=ws_future_url, tld=tld)
219+
# Extract proxy from requests_params for WebSocket connections
220+
https_proxy = None
221+
if requests_params and 'proxies' in requests_params:
222+
https_proxy = requests_params['proxies'].get('https') or requests_params['proxies'].get('http')
223+
224+
self.ws_api = WebsocketAPI(url=ws_api_url, tld=tld, https_proxy=https_proxy)
225+
ws_future_url = self.WS_FUTURES_URL.format(tld)
226+
if testnet:
227+
ws_future_url = self.WS_FUTURES_TESTNET_URL
228+
elif demo:
229+
ws_future_url = self.WS_FUTURES_DEMO_URL
230+
self.ws_future = WebsocketAPI(url=ws_future_url, tld=tld, https_proxy=https_proxy)
213231
self.loop = loop or get_loop()
214232

215233
def _get_headers(self) -> Dict:
@@ -250,6 +268,8 @@ def _create_api_uri(
250268
url = self.API_URL
251269
if self.testnet:
252270
url = self.API_TESTNET_URL
271+
elif self.demo:
272+
url = self.API_DEMO_URL
253273
v = self.PRIVATE_API_VERSION if signed else version
254274
return url + "/" + v + "/" + path
255275

@@ -273,6 +293,8 @@ def _create_futures_api_uri(self, path: str, version: int = 1) -> str:
273293
url = self.FUTURES_URL
274294
if self.testnet:
275295
url = self.FUTURES_TESTNET_URL
296+
elif self.demo:
297+
url = self.FUTURES_DEMO_URL
276298
options = {
277299
1: self.FUTURES_API_VERSION,
278300
2: self.FUTURES_API_VERSION2,
@@ -290,6 +312,8 @@ def _create_futures_coin_api_url(self, path: str, version: int = 1) -> str:
290312
url = self.FUTURES_COIN_URL
291313
if self.testnet:
292314
url = self.FUTURES_COIN_TESTNET_URL
315+
elif self.demo:
316+
url = self.FUTURES_COIN_DEMO_URL
293317
options = {
294318
1: self.FUTURES_API_VERSION,
295319
2: self.FUTURES_API_VERSION2,

0 commit comments

Comments
 (0)