Custom_Orders - Demo.

Early demonstration of Strategy controlling
Order Type on the Exchange, Stop / Stop_limit / Market / limit etc
The stake
The price to submit

Demo allows multiple orders to be placed, so ladding buys down to a fib
Or buying and setting stop-loss
Or Take profit in stages etc.

Included a strategy demo with stubs for
limit, market, take_profit and stop_limit orders

Obviously very early code.
Submitted as buys, stops etc are working / showing in GDAX.
and we have hte order IDs to update trade table.
This commit is contained in:
creslin 2018-07-31 20:21:01 +00:00
parent 1044d15b17
commit 6748e897a5
4 changed files with 487 additions and 84 deletions

View File

@ -0,0 +1,296 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
import talib.abstract as ta
from datetime import datetime
import time
import timeit
# --------------------------------
class customorders(IStrategy):
"""
author@: Creslin
"""
minimal_roi = {
"0": 0.20
}
# Optimal stoploss designed for the strategy
stoploss = -0.01
# Optimal ticker interval for the strategy
ticker_interval = '5m'
def get_ticker_indicator(self):
return int(self.ticker_interval[:-1])
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
#
# dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)
time.sleep(1)
return dataframe
# # # If using Berlins expose pairs to strategy
# def advise_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame:
#
# # RSI
# dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)
#
# return dataframe
#####
# EXAMPLE Strat - buys every even minute sells every odd
####
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
if int(datetime.now().strftime('%M')) % 2 == 0: #buy = 1 every even minute
print('----------------we are in strategy', datetime.now().time())
b: float = 9999999.99
print( "buy = 1")
else:
b = 0.00000001
dataframe.loc[
(
# L-RSI When below 0.01 / vfi -26
#(dataframe['lrsi'] < 0.5) &
#(dataframe['vfi'] < -4)
(dataframe['close'] < b )
),
'buy'] = 1
return dataframe
#####
# EXAMPLE Strat - buys every even minute sells every odd
####
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
if int(datetime.now().strftime('%M'))% 2 != 0: ## sell = 1 every ODD minute
print('----------------we are in strategy', datetime.now().time())
s: float = 9999999.99
print("sell = 1")
else:
s = 0.00000001
dataframe.loc[
(
# vfi 0.99
#(dataframe['vfi'] > 0.41)
(dataframe['close'] < s )
),
'sell'] = 1
return dataframe
def stop_stops_plugin(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
stop_stops_pligin. To stop a buy where X trades from Y have stopped out.
i.e to remove a pair from being bought that is on a losing streak.
Only processed if buy=1
"""
"""
Called before a trade executes if exists,
:param dataframe:
:param pair:
:return:
"""
from freqtrade.persistence import Trade
import pandas as pd
df = dataframe
# Only check buys, else return DF untouched
if df.iloc[-1]['buy'] == 1:
print("STOP_STOPS STOP_STOPS STOP_STOPS STOP_STOP STOP_STOPS STOP_STOP STOP_STOPS")
from pandas import set_option
set_option('display.max_rows', 2000)
set_option('display.max_columns', 8)
def closed_pair_trades(pair):
"""
To return list of closed trades for a pair
enriched with open/closd dates, profit, and stake
:param pair:
:return: df_c pair open, close, profit, stake
"""
pair = pair
df_c = pd.DataFrame(columns=['pair', 'profit', 'open_date', 'close_date', 'stake'])
pair_trades = Trade.query.filter(Trade.pair.is_(pair)).all()
for pair_trade in pair_trades:
if pair_trade.is_open == False:
# print("closed trade for", pair, "closed with ", pair_trade.close_profit )
p = pair
pcp = pair_trade.close_profit
od = pair_trade.open_date
cd = pair_trade.close_date
sa = pair_trade.stake_amount
df_c = df_c.append([{'pair':p, 'profit':pcp, 'open_date':od,
'close_date':cd, 'stake':sa}], ignore_index=True)
return df_c
def has_past_perf(df_c, lost_the_last=1, out_of=1):
"""
Return if pair lost_the_last X trades out_of Y trades
:param df_c: dataframe of closed trades for the pair
:param lost_the_last: number of bad trades to look for
:param out_of: within the last number over trades closed
:return: bool
"""
lost_the_last =lost_the_last
out_of = out_of
df_c = df_c
df_c_tail = df_c.tail(out_of)
lost_from_last_count = df_c_tail[df_c_tail['profit'] < 0].shapec
if lost_from_last_count[0] >= lost_the_last:
return True
else:
return False
# load this pairs closed trades into a df
# df_c = closed_pair_trades(pair=pair)
# print("df_c", df_c)
# Get bool if on pairs past profitable trades count, cancel the buy if limit hit
# past_perf = has_past_perf(df_c, lost_the_last=9, out_of=10)
# if past_perf:
# #set buy to 0
# print("STOP STOP SETTING BUY TO ZERO ")
# df.loc['buy'] = 0
dataframe = df
return dataframe
#def money_mgt_plugin(self, dataframe: DataFrame, pair: str) -> DataFrame:
def money_mgt_plugin(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
pre_trade_mgt.
To allow money mamagement and or other checks to be put against dataframe
with buy or sell set to "1"
Returns a list of custom order trades.
Each custom order to include, above the needed fields for exchange
dict['order_type'] = 'a_open_limit'
order_types are later ordered and executed in alphabetical order.
order to enter market are executed before stops and take profits are played in.
Supported types are:
- a_open_limit # enter market with limit order
- a_open_market # enter market with market order
- b_stop_limit # set a stop_limit order
- b_stop_market # set a stop_market order
- c_take_limit # set a take profit limit order
- c_take_market # set a take profit market order
Multipe orders can be processed but an asset may only have the sum of its open value
in either stop or take profit orders.
i.e we can open a $500 for 500 tokens position and have any combination of
stop or take profit orders at any prices that COMBINED do not break 500 tokens.
"one" common scenario may be an 'a_open_limit' or 'a_open_market' order
and 'b_stop_limit' or 'b_stop_market' order to protect capital.
"""
print("MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT MONEY_MGT")
df = dataframe
# If buy = 1 and Exchange is GDAX
if df.iloc[-1]['buy'] == 1:
exchange = self.config['exchange']['name']
# GDAX Stop-Limit Order
if exchange == "gdax":
custom_orders = []
limit = {}
take_profit = {}
# Take profit Market example "b_take_market"
# open market order example "a_open_market"
# Open limit order example
amount = 0.5
limit_price = df.iloc[-1]['close'] * 1.05 # pay 5% for testing order are made
limit = {}
limit["order_type"] = "a_open_limit"
limit["symbol"] = pair
limit["type"] = "limit"
limit["side"] = "buy"
limit["amount"] = amount
limit["price"] = limit_price
params: dict = {}
limit["params"] = params
custom_orders.append(limit)
# Market Example
market_amount = 1
market = {}
market_price = 1.05 # pay 5% for testing order are made
market["order_type"] = "a_open_market"
market["symbol"] = pair
market["type"] = "market"
market["side"] = "buy"
market["amount"] = market_amount
params: dict = {}
market["params"] = params
custom_orders.append(market)
# Stop Limit order
stop_limit = {}
stop_limit_amount = 0.01
stop_limit_price = df.iloc[-1]['close'] * 0.90 # take 10% hard code for testing
stop_price = df.iloc[-1]['close'] * 0.95 # trigger at 7% loss, hard code for testing
stop_limit['order_type'] = 'b_stop_limit'
stop_limit['symbol'] = pair
stop_limit['type'] = 'limit'
stop_limit['side'] = 'sell'
stop_limit['amount'] = stop_limit_amount
stop_limit['price'] = stop_limit_price
params: dict = {}
params['stop'] = 'loss'
params['stop_price'] = stop_price
stop_limit['params'] = params
custom_orders.append(stop_limit)
# Take profit Limit example
take_profit_limit_price = df.iloc[-1]['close'] * 1.2 # take_proft at 20% - for demo
take_profit_amount = 0.01
take_profit["order_type"] = "c_take_profit_limit"
take_profit["order_template"] = (pair, amount, take_profit_limit_price)
take_profit["symbol"] = pair
take_profit["type"] = "limit"
take_profit["side"] = "sell"
take_profit["amount"] = take_profit_amount
take_profit["price"] = take_profit_limit_price
take_profit_params: dict = {}
take_profit["params"] = take_profit_params
# custom_orders.append(take_profit)
#todo Add library of trade templates for binance, polo, gdax...
#todo Code to check have all the fields and sense check
# Cancel Legacy buy/sell signals if there are custom orders.
# Set buy / sell = 0 if we have custom orders
dataframe.loc['buy'] = 0
dataframe.loc['sell'] = 0
return dataframe, custom_orders
return dataframe, None

View File

@ -91,7 +91,7 @@ class Exchange(object):
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
'enableRateLimit': True,
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True),
})
except (KeyError, AttributeError):
raise OperationalException(f'Exchange {name} is not supported')
@ -185,6 +185,79 @@ class Exchange(object):
price = ceil(big_price) / pow(10, symbol_prec)
return price
def submit_custom_order(self, symbol, type, side, amount, price=None, params={}):
pair = symbol
rate = price
try:
return self._api.createOrder(symbol, type, side, amount, price, params)
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def customOrders(self, custom_orders):
'''
To process custom orders.
These are a list of dicts of orders created in strategy.
:param custom_orders:
:return:
'''
sorted_custom_orders = sorted(custom_orders, key=lambda k: k['order_type'])
for order in sorted_custom_orders:
order_type = order.get('order_type') if order.get('order_type') else ""
symbol = order.get('symbol') if order.get('symbol') else ""
type = order.get('type') if order.get('type') else ""
side = order.get('side') if order.get('side') else ""
amount = order.get('amount') if order.get('amount') else None
price = order.get('price') if order.get('price') else None
stop = order.get('stop') if order.get('stop') else ""
stop_price = order.get('stop_price') if order.get('stop_price') else None
params = order.get('params') if order.get('params') else {}
# Set the precision for amount and price(rate) as accepted by the exchange
if amount:
float(amount)
amount = self.symbol_amount_prec(symbol, amount)
float(amount)
if price:
float(price)
price = self.symbol_price_prec(symbol, price)
float(price)
if params.get('stop_price'):
stop_price = params['stop_price']
float(stop_price)
params['stop_price'] = self.symbol_price_prec(symbol, stop_price)
float(stop_price)
# destroy the params dict if its empty, not to pass to CCXT
if not params:
params = ""
# create_order(self, symbol, type, side, amount, price=None, params={})
print(symbol, type, side, amount, price, params)
custom_order_id = self.submit_custom_order(symbol=symbol, type=type, side=side,
amount=amount, price=price, params=params)
print("custom order_id is", custom_order_id)
def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}'

View File

@ -330,14 +330,15 @@ class FreqtradeBot(object):
# Pick pair based on buy signals
for _pair in whitelist:
thistory = self.exchange.get_ticker_history(_pair, interval)
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
(buy, sell, custom_orders) = self.strategy.get_signal(self.exchange, _pair, interval)
if buy and not sell:
return self.execute_buy(_pair, stake_amount)
return self.execute_buy(_pair, stake_amount, custom_orders)
if custom_orders:
return self.execute_buy(_pair, stake_amount, custom_orders)
return False
def execute_buy(self, pair: str, stake_amount: float) -> bool:
def execute_buy(self, pair: str, stake_amount: float, custom_orders: list) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
@ -352,6 +353,13 @@ class FreqtradeBot(object):
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
# if custom trades are set over_ride stake_amount test
if custom_orders:
# todo condider how or if to do this for limit, market, stop cutom_orders.
logger.info("bypassing stake amount test for custom_orders")
else:
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount'
@ -359,6 +367,14 @@ class FreqtradeBot(object):
)
return False
# Call exchange to make order or customOrder
if custom_orders:
# Call custom_oders in exchange/__init__.py
logger.info("Calling customOrders in exchange")
order_ids = self.exchange.customOrders(custom_orders)
else:
amount = stake_amount / buy_limit
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
@ -393,6 +409,8 @@ class FreqtradeBot(object):
Trade.session.flush()
return True
return False
def process_maybe_execute_buy(self) -> bool:
"""
Tries to execute a buy trade in a safe way
@ -497,9 +515,8 @@ class FreqtradeBot(object):
(buy, sell) = (False, False)
experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
ticker = self.exchange.get_ticker_history(trade.pair, self.strategy.ticker_interval)
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
ticker)
(buy, sell, custom_orders) = self.strategy.get_signal(self.exchange,
trade.pair, self.strategy.ticker_interval)
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
if should_sell.sell_flag:

View File

@ -14,6 +14,7 @@ from pandas import DataFrame
from freqtrade import constants
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
@ -74,29 +75,29 @@ class IStrategy(ABC):
self.config = config
@abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:param pair: Pair currently analyzed
:return: a Dataframe with all mandatory indicators for the strategies
"""
@abstractmethod
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_buy_trend(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:param pair: Pair currently analyzed
:return: DataFrame with buy column
"""
@abstractmethod
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:param pair: Pair currently analyzed
:return: DataFrame with sell column
"""
@ -106,31 +107,51 @@ class IStrategy(ABC):
"""
return self.__class__.__name__
def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
def analyze_ticker(self, ticker_history: List[Dict], pair: str) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
:return custom_orders list of dicts containing any custom trades from strategy
"""
dataframe = parse_ticker_dataframe(ticker_history)
dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata)
return dataframe
dataframe = self.advise_indicators(dataframe, pair)
dataframe = self.advise_buy(dataframe, pair)
dataframe = self.advise_sell(dataframe, pair)
def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]:
## Strategy Plugin stop_stops_mgt' if in strategy.
# Analyzes stop-count of past X from Y trades, cancels BUY if True
if hasattr(self, 'stop_stops_plugin'):
dataframe = self.stop_stops_plugin(dataframe, pair)
## Strategy Plugin 'win_rate' if in strategy.
# Calls micro backslap on proposed buy=1 pair. If winrate is poor, cancels BUY
if hasattr(self, 'win_rate_plugin'):
dataframe = self.win_rate_plugin(dataframe, pair)
## Strategy Plugin 'money_mgt' if in strategy.
# Money management for any order in buy. Determines stake size, stop-loss, take-profit
# Returns dataframe, custom_orders (list of dicts)
if hasattr(self, 'money_mgt_plugin'):
dataframe, custom_orders = self.money_mgt_plugin(dataframe, pair)
return dataframe, custom_orders
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC
:param interval: Interval to use (in min)
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
:return: (Buy, Sell, custom_orders) A bool-tuple indicating buy/sell signal,
and any custom_orders
"""
ticker_hist = exchange.get_ticker_history(pair, interval)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False, False
try:
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
dataframe, custom_orders = self.analyze_ticker(ticker_hist, pair)
except ValueError as error:
logger.warning(
'Unable to analyze ticker for pair %s: %s',
@ -155,13 +176,13 @@ class IStrategy(ABC):
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
# if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
# logger.warning(
# 'Outdated history for pair %s. Last tick is %s minutes old',
# pair,
# (arrow.utcnow() - signal_date).seconds // 60
# )
# return False, False
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug(
@ -171,7 +192,7 @@ class IStrategy(ABC):
str(buy),
str(sell)
)
return buy, sell
return buy, sell, custom_orders
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
sell: bool) -> SellCheckTuple:
@ -212,7 +233,6 @@ class IStrategy(ABC):
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
:param current_profit: current profit in percent
"""
trailing_stop = self.config.get('trailing_stop', False)
@ -240,15 +260,12 @@ class IStrategy(ABC):
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.stoploss
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
if 'trailing_stop_positive' in self.config and current_profit > 0:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
f"with offset {sl_offset:.4g} "
f"since we have profit {current_profit:.4f}%")
f"since we have profit {current_profit}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
@ -275,15 +292,15 @@ class IStrategy(ABC):
"""
Creates a dataframe and populates indicators for given ticker data
"""
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), pair)
for pair, pair_data in tickerdata.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
This method should not be overridden.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:param pair: The currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
if self._populate_fun_len == 2:
@ -291,14 +308,14 @@ class IStrategy(ABC):
"the current function headers!", DeprecationWarning)
return self.populate_indicators(dataframe) # type: ignore
else:
return self.populate_indicators(dataframe, metadata)
return self.populate_indicators(dataframe, pair)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_buy(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:param pair: The currently traded pair
:return: DataFrame with buy column
"""
if self._buy_fun_len == 2:
@ -306,14 +323,14 @@ class IStrategy(ABC):
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
else:
return self.populate_buy_trend(dataframe, metadata)
return self.populate_buy_trend(dataframe, pair)
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_sell(self, dataframe: DataFrame, pair: str) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:param pair: The currently traded pair
:return: DataFrame with sell column
"""
if self._sell_fun_len == 2:
@ -321,4 +338,4 @@ class IStrategy(ABC):
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
else:
return self.populate_sell_trend(dataframe, metadata)
return self.populate_sell_trend(dataframe, pair)