Skip to content

Commit 0266642

Browse files
authored
Merge pull request #2253 from Drakkar-Software/dev
Master merge
2 parents 1d749fe + 65c8900 commit 0266642

25 files changed

+643
-129
lines changed

.gitpod.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
tasks:
3+
- name: Initialize OctoBot
4+
init: |
5+
cd OctoBot
6+
python3 -m pip install -Ur requirements.txt
7+
command: |
8+
python3 start.py
9+
ports:
10+
- port: 5001
11+
onOpen: open-preview
12+
name: OctoBot
13+
github:
14+
prebuilds:
15+
master: true
16+
branches: true
17+
pullRequests: true
18+
pullRequestsFromForks: true
19+
addCheck: true
20+
addComment: true
21+
addBadge: true
22+
23+

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
*It is strongly advised to perform an update of your tentacles after updating OctoBot. (start.py tentacles --install --all)*
88

9+
## [0.4.41] - 2023-03-03
10+
### Added
11+
- Trades PNL history for supported trading mode
12+
- Support for OKX futures
13+
- Support for market orders in Dip Analyser
14+
### Updated
15+
- Revamped the trading tab of the web interface
16+
- Reduced required RAM for long-lasting instances
17+
- Optimized disc read/write operations when browsing the web interface
18+
### Fixed
19+
- Orders synchronization and cancel issues
20+
- Future trading positions synchronization issues
21+
- Order creation issues related to order minimum and maximum amounts
22+
923
## [0.4.40] - 2023-02-17
1024
### Fixed
1125
- Historical portfolio reset

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# OctoBot [0.4.40](https://octobot.click/gh-changelog)
1+
# OctoBot [0.4.41](https://octobot.click/gh-changelog)
22
[![PyPI](https://img.shields.io/pypi/v/OctoBot.svg)](https://octobot.click/gh-pypi)
33
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e07fb190156d4efb8e7d07aaa5eff2e1)](https://app.codacy.com/gh/Drakkar-Software/OctoBot?utm_source=github.com&utm_medium=referral&utm_content=Drakkar-Software/OctoBot&utm_campaign=Badge_Grade_Dashboard)[![Downloads](https://pepy.tech/badge/octobot/month)](https://pepy.tech/project/octobot)
44
[![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg)](https://octobot.click/gh-dockerhub)
@@ -16,6 +16,9 @@
1616
</p>
1717

1818
![Web Interface](../assets/web-interface.gif)
19+
20+
21+
1922
## Description
2023
[Octobot](https://www.octobot.online/) is a powerful, fully modular open-source cryptocurrency trading robot.
2124

@@ -55,6 +58,7 @@ Register to OctoBot Cloud to deploy your OctoBot in the cloud. No installation r
5558

5659
[![Deploy to OctoBot Cloud](https://dabuttonfactory.com/button.png?t=Deploy+now&f=Roboto-Bold&ts=18&tc=fff&hp=20&vp=15&c=11&bgt=unicolored&bgc=422afb)](https://octobot.click/gh-deploy)
5760

61+
5862
#### [With executable](https://www.octobot.info/installation/with-binary)
5963
Follow the [2 steps installation guide](https://www.octobot.online/executable_installation/)
6064

@@ -109,6 +113,11 @@ python3 start.py
109113
Octobot supports many [exchanges](https://octobot.click/gh-exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt).
110114
To activate trading on an exchange, just configure OctoBot with your API keys as described [on the exchange documentation](https://www.octobot.online/guides/#exchanges).
111115

116+
## Contribute from a browser IDE
117+
Make changes and contribute to OctoBot in a single click with an **already setup and ready to code developer environment** using Gitpod !
118+
119+
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Drakkar-Software/OctoBot)
120+
112121
## Disclaimer
113122
Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
114123
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.

exchanges_tests/abstract_authenticated_exchange_tester.py

Lines changed: 183 additions & 42 deletions
Large diffs are not rendered by default.

exchanges_tests/abstract_authenticated_future_exchange_tester.py

Lines changed: 152 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616
import contextlib
1717
import decimal
18+
import pytest
1819

1920
import octobot_trading.enums as trading_enums
2021
import octobot_trading.constants as trading_constants
22+
import octobot_trading.errors as trading_errors
2123
from exchanges_tests import abstract_authenticated_exchange_tester
2224

2325

@@ -27,9 +29,11 @@ class AbstractAuthenticatedFutureExchangeTester(
2729
# enter exchange name as a class variable here*
2830
EXCHANGE_TYPE = trading_enums.ExchangeTypes.FUTURE.value
2931
PORTFOLIO_TYPE_FOR_SIZE = trading_constants.CONFIG_PORTFOLIO_TOTAL
30-
REQUIRES_SYMBOLS_TO_GET_POSITIONS = False
3132
INVERSE_SYMBOL = None
3233
MIN_PORTFOLIO_SIZE = 2 # ensure fetching currency for linear and inverse
34+
SUPPORTS_GET_LEVERAGE = True
35+
SUPPORTS_SET_LEVERAGE = True
36+
SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE = True
3337

3438
async def test_get_empty_linear_and_inverse_positions(self):
3539
# ensure fetch empty positions
@@ -38,19 +42,103 @@ async def test_get_empty_linear_and_inverse_positions(self):
3842

3943
async def inner_test_get_empty_linear_and_inverse_positions(self):
4044
positions = await self.get_positions()
41-
self._check_position_content(positions)
45+
self._check_positions_content(positions)
46+
position = await self.get_position(self.SYMBOL)
47+
self._check_position_content(position, self.SYMBOL)
4248
for contract_type in (trading_enums.FutureContractType.LINEAR_PERPETUAL,
4349
trading_enums.FutureContractType.INVERSE_PERPETUAL):
4450
if not self.has_empty_position(self.get_filtered_positions(positions, contract_type)):
4551
empty_position_symbol = self.get_other_position_symbol(positions, contract_type)
52+
# test with get_position
4653
empty_position = await self.get_position(empty_position_symbol)
4754
assert self.is_position_empty(empty_position)
55+
# test with get_positions
56+
empty_positions = await self.get_positions([empty_position_symbol])
57+
assert len(empty_positions) == 1
58+
assert self.is_position_empty(empty_positions[0])
4859

49-
def _check_position_content(self, positions):
60+
async def test_get_and_set_leverage(self):
61+
# ensure set_leverage works
62+
async with self.local_exchange_manager():
63+
await self.inner_test_get_and_set_leverage()
64+
65+
async def inner_test_get_and_set_leverage(self):
66+
contract = await self.init_and_get_contract()
67+
origin_margin_type = contract.margin_type
68+
origin_leverage = contract.current_leverage
69+
assert origin_leverage != trading_constants.ZERO
70+
if self.SUPPORTS_GET_LEVERAGE:
71+
assert origin_leverage == await self.get_leverage()
72+
if not self.SUPPORTS_SET_LEVERAGE:
73+
return
74+
new_leverage = origin_leverage + 1
75+
await self.set_leverage(new_leverage)
76+
await self._check_margin_type_and_leverage(origin_margin_type, new_leverage) # did not change margin type
77+
# change leverage back to origin value
78+
await self.set_leverage(origin_leverage)
79+
await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage) # did not change margin type
80+
81+
async def test_get_and_set_margin_type(self):
82+
# ensure set_leverage works
83+
async with self.local_exchange_manager():
84+
await self.inner_test_get_and_set_margin_type(allow_empty_position=True)
85+
86+
async def inner_test_get_and_set_margin_type(self, allow_empty_position=False, symbol=None):
87+
contract = await self.init_and_get_contract(symbol=symbol)
88+
origin_margin_type = contract.margin_type
89+
origin_leverage = contract.current_leverage
90+
new_margin_type = trading_enums.MarginType.CROSS \
91+
if origin_margin_type is trading_enums.MarginType.ISOLATED else trading_enums.MarginType.ISOLATED
92+
if not self.exchange_manager.exchange.SUPPORTS_SET_MARGIN_TYPE:
93+
assert origin_margin_type in (trading_enums.MarginType.ISOLATED, trading_enums.MarginType.CROSS)
94+
with pytest.raises(AttributeError):
95+
await self.exchange_manager.exchange.connector.set_symbol_margin_type(symbol, True)
96+
with pytest.raises(trading_errors.NotSupported):
97+
await self.set_margin_type(new_margin_type, symbol=symbol)
98+
return
99+
await self.set_margin_type(new_margin_type, symbol=symbol)
100+
position = await self.get_position(symbol=symbol)
101+
if allow_empty_position and (
102+
position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] != trading_constants.ZERO
103+
or self.SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE
104+
):
105+
# did not change leverage
106+
await self._check_margin_type_and_leverage(new_margin_type, origin_leverage, symbol=symbol)
107+
# restore margin type
108+
await self.set_margin_type(origin_margin_type, symbol=symbol)
109+
# did not change leverage
110+
await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage, symbol=symbol)
111+
112+
async def set_margin_type(self, margin_type, symbol=None):
113+
await self.exchange_manager.exchange.set_symbol_margin_type(
114+
symbol or self.SYMBOL,
115+
margin_type is trading_enums.MarginType.ISOLATED
116+
)
117+
118+
async def _check_margin_type_and_leverage(self, expected_margin_type, expected_leverage, symbol=None):
119+
margin_type, leverage = await self.get_margin_type_and_leverage_from_position(symbol=symbol)
120+
assert expected_margin_type is margin_type
121+
assert expected_leverage == leverage
122+
if self.SUPPORTS_GET_LEVERAGE:
123+
assert expected_leverage == await self.get_leverage(symbol=symbol)
124+
125+
def _check_positions_content(self, positions):
50126
for position in positions:
127+
self._check_position_content(position, None)
128+
129+
def _check_position_content(self, position, symbol, position_mode=None):
130+
if symbol:
131+
assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value] == symbol
132+
else:
51133
assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value]
52-
# should not be 0 in octobot
53-
assert position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] > 0
134+
leverage = position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value]
135+
assert isinstance(leverage, decimal.Decimal)
136+
# should not be 0 in octobot
137+
assert leverage > 0
138+
assert position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] is not None
139+
assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is not None
140+
if position_mode is not None:
141+
assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is position_mode
54142

55143
async def inner_test_create_and_fill_market_orders(self):
56144
portfolio = await self.get_portfolio()
@@ -62,39 +150,64 @@ async def inner_test_create_and_fill_market_orders(self):
62150
# buy: increase position
63151
buy_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.BUY)
64152
self.check_created_market_order(buy_market, size, trading_enums.TradeOrderSide.BUY)
65-
await self.wait_for_fill(buy_market)
66-
post_buy_portfolio = await self.get_portfolio()
67-
post_buy_position = await self.get_position()
68-
self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
69-
self.check_position_changed(position, post_buy_position, True)
70-
post_order_positions = await self.get_positions()
71-
self.check_position_in_positions(pre_order_positions + post_order_positions)
72-
# sell: reset portfolio & position
73-
sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
74-
self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
75-
await self.wait_for_fill(sell_market)
76-
post_sell_portfolio = await self.get_portfolio()
77-
post_sell_position = await self.get_position()
78-
self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
79-
self.check_position_changed(post_buy_position, post_sell_position, False)
80-
# position is back to what it was at the beginning on the test
81-
self.check_position_size(position, post_sell_position)
82-
83-
async def test_create_bundled_orders(self):
84-
async with self.local_exchange_manager(), self.required_empty_position():
85-
await self.inner_test_create_bundled_orders()
153+
post_buy_portfolio = {}
154+
post_buy_position = None
155+
try:
156+
await self.wait_for_fill(buy_market)
157+
post_buy_portfolio = await self.get_portfolio()
158+
post_buy_position = await self.get_position()
159+
self._check_position_content(post_buy_position, self.SYMBOL,
160+
position_mode=trading_enums.PositionMode.ONE_WAY)
161+
self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
162+
self.check_position_changed(position, post_buy_position, True)
163+
post_order_positions = await self.get_positions()
164+
self.check_position_in_positions(pre_order_positions + post_order_positions)
165+
# now that position is open, test margin type update
166+
await self.inner_test_get_and_set_margin_type()
167+
finally:
168+
# sell: reset portfolio & position
169+
sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
170+
self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
171+
await self.wait_for_fill(sell_market)
172+
post_sell_portfolio = await self.get_portfolio()
173+
post_sell_position = await self.get_position()
174+
self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
175+
self.check_position_changed(post_buy_position, post_sell_position, False)
176+
# position is back to what it was at the beginning on the test
177+
self.check_position_size(position, post_sell_position)
86178

87179
async def get_position(self, symbol=None):
88180
return await self.exchange_manager.exchange.get_position(symbol or self.SYMBOL)
89181

90-
async def get_positions(self):
91-
symbols = None
92-
if self.REQUIRES_SYMBOLS_TO_GET_POSITIONS:
182+
async def get_positions(self, symbols=None):
183+
symbols = symbols or None
184+
if symbols is None and self.exchange_manager.exchange.REQUIRES_SYMBOL_FOR_EMPTY_POSITION:
93185
if self.INVERSE_SYMBOL is None:
94186
raise AssertionError(f"INVERSE_SYMBOL is required")
95187
symbols = [self.SYMBOL, self.INVERSE_SYMBOL]
96188
return await self.exchange_manager.exchange.get_positions(symbols=symbols)
97189

190+
async def init_and_get_contract(self, symbol=None):
191+
symbol = symbol or self.SYMBOL
192+
await self.exchange_manager.exchange.load_pair_future_contract(symbol)
193+
if not self.exchange_manager.exchange.has_pair_future_contract(symbol):
194+
raise AssertionError(f"{symbol} contract not initialized")
195+
return self.exchange_manager.exchange.get_pair_future_contract(symbol)
196+
197+
async def get_margin_type_and_leverage_from_position(self, symbol=None):
198+
position = await self.get_position(symbol=symbol)
199+
return (
200+
position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value],
201+
position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value],
202+
)
203+
204+
async def get_leverage(self, symbol=None):
205+
leverage = await self.exchange_manager.exchange.get_symbol_leverage(symbol or self.SYMBOL)
206+
return leverage[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value]
207+
208+
async def set_leverage(self, leverage, symbol=None):
209+
return await self.exchange_manager.exchange.set_symbol_leverage(symbol or self.SYMBOL, float(leverage))
210+
98211
@contextlib.asynccontextmanager
99212
async def required_empty_position(self):
100213
position = await self.get_position()
@@ -168,7 +281,7 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
168281
for position in positions_blacklist
169282
)
170283
for symbol in self.exchange_manager.exchange.connector.client.markets:
171-
if symbol in ignored_symbols:
284+
if symbol in ignored_symbols or self.exchange_manager.exchange.is_expirable_symbol(symbol):
172285
continue
173286
if contract_type is trading_enums.FutureContractType.INVERSE_PERPETUAL \
174287
and self.exchange_manager.exchange.is_inverse_symbol(symbol):
@@ -179,6 +292,10 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
179292
raise AssertionError(f"No free symbol for {contract_type}")
180293

181294
def is_position_empty(self, position):
295+
if position is None:
296+
raise AssertionError(
297+
f"Fetched empty position should never be None as a symbol parameter is given"
298+
)
182299
return position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] == trading_constants.ZERO
183300

184301
def check_position_in_positions(self, positions, symbol=None):
@@ -204,4 +321,8 @@ def check_theoretical_cost(self, symbol, quantity, price, cost):
204321
assert theoretical_cost * decimal.Decimal("0.8") <= cost <= theoretical_cost * decimal.Decimal("1.2")
205322

206323
def _get_all_symbols(self):
207-
return [self.SYMBOL, self.INVERSE_SYMBOL]
324+
return [
325+
symbol
326+
for symbol in (self.SYMBOL, self.INVERSE_SYMBOL)
327+
if symbol
328+
]

exchanges_tests/test_ascendex.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ async def test_edit_stop_order(self):
5858
# pass if not implemented
5959
pass
6060

61-
async def test_create_bundled_orders(self):
61+
async def test_create_single_bundled_orders(self):
62+
# pass if not implemented
63+
pass
64+
65+
async def test_create_double_bundled_orders(self):
6266
# pass if not implemented
6367
pass

exchanges_tests/test_binance.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TestBinanceAuthenticatedExchange(
3030
SETTLEMENT_CURRENCY = "BUSD"
3131
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
3232
ORDER_SIZE = 50 # % of portfolio to include in test orders
33+
DUPLICATE_TRADES_RATIO = 0.1 # allow 10% duplicate in trades (due to trade id set to order id)
3334

3435
async def test_get_portfolio(self):
3536
await super().test_get_portfolio()
@@ -58,6 +59,10 @@ async def test_edit_stop_order(self):
5859
# pass if not implemented
5960
pass
6061

61-
async def test_create_bundled_orders(self):
62+
async def test_create_single_bundled_orders(self):
63+
# pass if not implemented
64+
pass
65+
66+
async def test_create_double_bundled_orders(self):
6267
# pass if not implemented
6368
pass

exchanges_tests/test_bitget.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ async def test_edit_stop_order(self):
6262
# pass if not implemented
6363
pass
6464

65-
async def test_create_bundled_orders(self):
65+
async def test_create_single_bundled_orders(self):
66+
# pass if not implemented
67+
pass
68+
69+
async def test_create_double_bundled_orders(self):
6670
# pass if not implemented
6771
pass

0 commit comments

Comments
 (0)