Skip to content

Commit

Permalink
Merge pull request #2253 from Drakkar-Software/dev
Browse files Browse the repository at this point in the history
Master merge
  • Loading branch information
GuillaumeDSM authored Mar 3, 2023
2 parents 1d749fe + 65c8900 commit 0266642
Show file tree
Hide file tree
Showing 25 changed files with 643 additions and 129 deletions.
23 changes: 23 additions & 0 deletions .gitpod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

tasks:
- name: Initialize OctoBot
init: |
cd OctoBot
python3 -m pip install -Ur requirements.txt
command: |
python3 start.py
ports:
- port: 5001
onOpen: open-preview
name: OctoBot
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: true
addBadge: true


14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

## [0.4.41] - 2023-03-03
### Added
- Trades PNL history for supported trading mode
- Support for OKX futures
- Support for market orders in Dip Analyser
### Updated
- Revamped the trading tab of the web interface
- Reduced required RAM for long-lasting instances
- Optimized disc read/write operations when browsing the web interface
### Fixed
- Orders synchronization and cancel issues
- Future trading positions synchronization issues
- Order creation issues related to order minimum and maximum amounts

## [0.4.40] - 2023-02-17
### Fixed
- Historical portfolio reset
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# OctoBot [0.4.40](https://octobot.click/gh-changelog)
# OctoBot [0.4.41](https://octobot.click/gh-changelog)
[![PyPI](https://img.shields.io/pypi/v/OctoBot.svg)](https://octobot.click/gh-pypi)
[![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)
[![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg)](https://octobot.click/gh-dockerhub)
Expand All @@ -16,6 +16,9 @@
</p>

![Web Interface](../assets/web-interface.gif)



## Description
[Octobot](https://www.octobot.online/) is a powerful, fully modular open-source cryptocurrency trading robot.

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

[![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)


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

Expand Down Expand Up @@ -109,6 +113,11 @@ python3 start.py
Octobot supports many [exchanges](https://octobot.click/gh-exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt).
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).

## Contribute from a browser IDE
Make changes and contribute to OctoBot in a single click with an **already setup and ready to code developer environment** using Gitpod !

[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Drakkar-Software/OctoBot)

## Disclaimer
Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
Expand Down
225 changes: 183 additions & 42 deletions exchanges_tests/abstract_authenticated_exchange_tester.py

Large diffs are not rendered by default.

183 changes: 152 additions & 31 deletions exchanges_tests/abstract_authenticated_future_exchange_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
import contextlib
import decimal
import pytest

import octobot_trading.enums as trading_enums
import octobot_trading.constants as trading_constants
import octobot_trading.errors as trading_errors
from exchanges_tests import abstract_authenticated_exchange_tester


Expand All @@ -27,9 +29,11 @@ class AbstractAuthenticatedFutureExchangeTester(
# enter exchange name as a class variable here*
EXCHANGE_TYPE = trading_enums.ExchangeTypes.FUTURE.value
PORTFOLIO_TYPE_FOR_SIZE = trading_constants.CONFIG_PORTFOLIO_TOTAL
REQUIRES_SYMBOLS_TO_GET_POSITIONS = False
INVERSE_SYMBOL = None
MIN_PORTFOLIO_SIZE = 2 # ensure fetching currency for linear and inverse
SUPPORTS_GET_LEVERAGE = True
SUPPORTS_SET_LEVERAGE = True
SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE = True

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

async def inner_test_get_empty_linear_and_inverse_positions(self):
positions = await self.get_positions()
self._check_position_content(positions)
self._check_positions_content(positions)
position = await self.get_position(self.SYMBOL)
self._check_position_content(position, self.SYMBOL)
for contract_type in (trading_enums.FutureContractType.LINEAR_PERPETUAL,
trading_enums.FutureContractType.INVERSE_PERPETUAL):
if not self.has_empty_position(self.get_filtered_positions(positions, contract_type)):
empty_position_symbol = self.get_other_position_symbol(positions, contract_type)
# test with get_position
empty_position = await self.get_position(empty_position_symbol)
assert self.is_position_empty(empty_position)
# test with get_positions
empty_positions = await self.get_positions([empty_position_symbol])
assert len(empty_positions) == 1
assert self.is_position_empty(empty_positions[0])

def _check_position_content(self, positions):
async def test_get_and_set_leverage(self):
# ensure set_leverage works
async with self.local_exchange_manager():
await self.inner_test_get_and_set_leverage()

async def inner_test_get_and_set_leverage(self):
contract = await self.init_and_get_contract()
origin_margin_type = contract.margin_type
origin_leverage = contract.current_leverage
assert origin_leverage != trading_constants.ZERO
if self.SUPPORTS_GET_LEVERAGE:
assert origin_leverage == await self.get_leverage()
if not self.SUPPORTS_SET_LEVERAGE:
return
new_leverage = origin_leverage + 1
await self.set_leverage(new_leverage)
await self._check_margin_type_and_leverage(origin_margin_type, new_leverage) # did not change margin type
# change leverage back to origin value
await self.set_leverage(origin_leverage)
await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage) # did not change margin type

async def test_get_and_set_margin_type(self):
# ensure set_leverage works
async with self.local_exchange_manager():
await self.inner_test_get_and_set_margin_type(allow_empty_position=True)

async def inner_test_get_and_set_margin_type(self, allow_empty_position=False, symbol=None):
contract = await self.init_and_get_contract(symbol=symbol)
origin_margin_type = contract.margin_type
origin_leverage = contract.current_leverage
new_margin_type = trading_enums.MarginType.CROSS \
if origin_margin_type is trading_enums.MarginType.ISOLATED else trading_enums.MarginType.ISOLATED
if not self.exchange_manager.exchange.SUPPORTS_SET_MARGIN_TYPE:
assert origin_margin_type in (trading_enums.MarginType.ISOLATED, trading_enums.MarginType.CROSS)
with pytest.raises(AttributeError):
await self.exchange_manager.exchange.connector.set_symbol_margin_type(symbol, True)
with pytest.raises(trading_errors.NotSupported):
await self.set_margin_type(new_margin_type, symbol=symbol)
return
await self.set_margin_type(new_margin_type, symbol=symbol)
position = await self.get_position(symbol=symbol)
if allow_empty_position and (
position[trading_enums.ExchangeConstantsPositionColumns.SIZE.value] != trading_constants.ZERO
or self.SUPPORTS_EMPTY_POSITION_SET_MARGIN_TYPE
):
# did not change leverage
await self._check_margin_type_and_leverage(new_margin_type, origin_leverage, symbol=symbol)
# restore margin type
await self.set_margin_type(origin_margin_type, symbol=symbol)
# did not change leverage
await self._check_margin_type_and_leverage(origin_margin_type, origin_leverage, symbol=symbol)

async def set_margin_type(self, margin_type, symbol=None):
await self.exchange_manager.exchange.set_symbol_margin_type(
symbol or self.SYMBOL,
margin_type is trading_enums.MarginType.ISOLATED
)

async def _check_margin_type_and_leverage(self, expected_margin_type, expected_leverage, symbol=None):
margin_type, leverage = await self.get_margin_type_and_leverage_from_position(symbol=symbol)
assert expected_margin_type is margin_type
assert expected_leverage == leverage
if self.SUPPORTS_GET_LEVERAGE:
assert expected_leverage == await self.get_leverage(symbol=symbol)

def _check_positions_content(self, positions):
for position in positions:
self._check_position_content(position, None)

def _check_position_content(self, position, symbol, position_mode=None):
if symbol:
assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value] == symbol
else:
assert position[trading_enums.ExchangeConstantsPositionColumns.SYMBOL.value]
# should not be 0 in octobot
assert position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value] > 0
leverage = position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value]
assert isinstance(leverage, decimal.Decimal)
# should not be 0 in octobot
assert leverage > 0
assert position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] is not None
assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is not None
if position_mode is not None:
assert position[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is position_mode

async def inner_test_create_and_fill_market_orders(self):
portfolio = await self.get_portfolio()
Expand All @@ -62,39 +150,64 @@ async def inner_test_create_and_fill_market_orders(self):
# buy: increase position
buy_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.BUY)
self.check_created_market_order(buy_market, size, trading_enums.TradeOrderSide.BUY)
await self.wait_for_fill(buy_market)
post_buy_portfolio = await self.get_portfolio()
post_buy_position = await self.get_position()
self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
self.check_position_changed(position, post_buy_position, True)
post_order_positions = await self.get_positions()
self.check_position_in_positions(pre_order_positions + post_order_positions)
# sell: reset portfolio & position
sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
await self.wait_for_fill(sell_market)
post_sell_portfolio = await self.get_portfolio()
post_sell_position = await self.get_position()
self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
self.check_position_changed(post_buy_position, post_sell_position, False)
# position is back to what it was at the beginning on the test
self.check_position_size(position, post_sell_position)

async def test_create_bundled_orders(self):
async with self.local_exchange_manager(), self.required_empty_position():
await self.inner_test_create_bundled_orders()
post_buy_portfolio = {}
post_buy_position = None
try:
await self.wait_for_fill(buy_market)
post_buy_portfolio = await self.get_portfolio()
post_buy_position = await self.get_position()
self._check_position_content(post_buy_position, self.SYMBOL,
position_mode=trading_enums.PositionMode.ONE_WAY)
self.check_portfolio_changed(portfolio, post_buy_portfolio, False)
self.check_position_changed(position, post_buy_position, True)
post_order_positions = await self.get_positions()
self.check_position_in_positions(pre_order_positions + post_order_positions)
# now that position is open, test margin type update
await self.inner_test_get_and_set_margin_type()
finally:
# sell: reset portfolio & position
sell_market = await self.create_market_order(current_price, size, trading_enums.TradeOrderSide.SELL)
self.check_created_market_order(sell_market, size, trading_enums.TradeOrderSide.SELL)
await self.wait_for_fill(sell_market)
post_sell_portfolio = await self.get_portfolio()
post_sell_position = await self.get_position()
self.check_portfolio_changed(post_buy_portfolio, post_sell_portfolio, True)
self.check_position_changed(post_buy_position, post_sell_position, False)
# position is back to what it was at the beginning on the test
self.check_position_size(position, post_sell_position)

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

async def get_positions(self):
symbols = None
if self.REQUIRES_SYMBOLS_TO_GET_POSITIONS:
async def get_positions(self, symbols=None):
symbols = symbols or None
if symbols is None and self.exchange_manager.exchange.REQUIRES_SYMBOL_FOR_EMPTY_POSITION:
if self.INVERSE_SYMBOL is None:
raise AssertionError(f"INVERSE_SYMBOL is required")
symbols = [self.SYMBOL, self.INVERSE_SYMBOL]
return await self.exchange_manager.exchange.get_positions(symbols=symbols)

async def init_and_get_contract(self, symbol=None):
symbol = symbol or self.SYMBOL
await self.exchange_manager.exchange.load_pair_future_contract(symbol)
if not self.exchange_manager.exchange.has_pair_future_contract(symbol):
raise AssertionError(f"{symbol} contract not initialized")
return self.exchange_manager.exchange.get_pair_future_contract(symbol)

async def get_margin_type_and_leverage_from_position(self, symbol=None):
position = await self.get_position(symbol=symbol)
return (
position[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value],
position[trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value],
)

async def get_leverage(self, symbol=None):
leverage = await self.exchange_manager.exchange.get_symbol_leverage(symbol or self.SYMBOL)
return leverage[trading_enums.ExchangeConstantsLeveragePropertyColumns.LEVERAGE.value]

async def set_leverage(self, leverage, symbol=None):
return await self.exchange_manager.exchange.set_symbol_leverage(symbol or self.SYMBOL, float(leverage))

@contextlib.asynccontextmanager
async def required_empty_position(self):
position = await self.get_position()
Expand Down Expand Up @@ -168,7 +281,7 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
for position in positions_blacklist
)
for symbol in self.exchange_manager.exchange.connector.client.markets:
if symbol in ignored_symbols:
if symbol in ignored_symbols or self.exchange_manager.exchange.is_expirable_symbol(symbol):
continue
if contract_type is trading_enums.FutureContractType.INVERSE_PERPETUAL \
and self.exchange_manager.exchange.is_inverse_symbol(symbol):
Expand All @@ -179,6 +292,10 @@ def get_other_position_symbol(self, positions_blacklist, contract_type):
raise AssertionError(f"No free symbol for {contract_type}")

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

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

def _get_all_symbols(self):
return [self.SYMBOL, self.INVERSE_SYMBOL]
return [
symbol
for symbol in (self.SYMBOL, self.INVERSE_SYMBOL)
if symbol
]
6 changes: 5 additions & 1 deletion exchanges_tests/test_ascendex.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ async def test_edit_stop_order(self):
# pass if not implemented
pass

async def test_create_bundled_orders(self):
async def test_create_single_bundled_orders(self):
# pass if not implemented
pass

async def test_create_double_bundled_orders(self):
# pass if not implemented
pass
7 changes: 6 additions & 1 deletion exchanges_tests/test_binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class TestBinanceAuthenticatedExchange(
SETTLEMENT_CURRENCY = "BUSD"
SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}"
ORDER_SIZE = 50 # % of portfolio to include in test orders
DUPLICATE_TRADES_RATIO = 0.1 # allow 10% duplicate in trades (due to trade id set to order id)

async def test_get_portfolio(self):
await super().test_get_portfolio()
Expand Down Expand Up @@ -58,6 +59,10 @@ async def test_edit_stop_order(self):
# pass if not implemented
pass

async def test_create_bundled_orders(self):
async def test_create_single_bundled_orders(self):
# pass if not implemented
pass

async def test_create_double_bundled_orders(self):
# pass if not implemented
pass
6 changes: 5 additions & 1 deletion exchanges_tests/test_bitget.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ async def test_edit_stop_order(self):
# pass if not implemented
pass

async def test_create_bundled_orders(self):
async def test_create_single_bundled_orders(self):
# pass if not implemented
pass

async def test_create_double_bundled_orders(self):
# pass if not implemented
pass
Loading

0 comments on commit 0266642

Please sign in to comment.