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:
parent
1044d15b17
commit
6748e897a5
296
freqtrade/custom_orders_strat.py
Normal file
296
freqtrade/custom_orders_strat.py
Normal 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
|
@ -91,7 +91,7 @@ class Exchange(object):
|
|||||||
'secret': exchange_config.get('secret'),
|
'secret': exchange_config.get('secret'),
|
||||||
'password': exchange_config.get('password'),
|
'password': exchange_config.get('password'),
|
||||||
'uid': exchange_config.get('uid', ''),
|
'uid': exchange_config.get('uid', ''),
|
||||||
'enableRateLimit': True,
|
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True),
|
||||||
})
|
})
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
@ -185,6 +185,79 @@ class Exchange(object):
|
|||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
price = ceil(big_price) / pow(10, symbol_prec)
|
||||||
return price
|
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:
|
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
|
@ -330,14 +330,15 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# Pick pair based on buy signals
|
# Pick pair based on buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
thistory = self.exchange.get_ticker_history(_pair, interval)
|
(buy, sell, custom_orders) = self.strategy.get_signal(self.exchange, _pair, interval)
|
||||||
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
|
|
||||||
|
|
||||||
if buy and not sell:
|
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
|
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
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
@ -352,46 +353,63 @@ class FreqtradeBot(object):
|
|||||||
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
||||||
|
|
||||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
||||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
|
||||||
logger.warning(
|
# if custom trades are set over_ride stake_amount test
|
||||||
f'Can\'t open a new trade for {pair_s}: stake amount'
|
|
||||||
f' is too small ({stake_amount} < {min_stake_amount})'
|
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'
|
||||||
|
f' is too small ({stake_amount} < {min_stake_amount})'
|
||||||
|
)
|
||||||
|
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']
|
||||||
|
|
||||||
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': self.exchange.name.capitalize(),
|
||||||
|
'pair': pair_s,
|
||||||
|
'market_url': pair_url,
|
||||||
|
'limit': buy_limit,
|
||||||
|
'stake_amount': stake_amount,
|
||||||
|
'stake_currency': stake_currency,
|
||||||
|
'fiat_currency': fiat_currency
|
||||||
|
})
|
||||||
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
|
trade = Trade(
|
||||||
|
pair=pair,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=amount,
|
||||||
|
fee_open=fee,
|
||||||
|
fee_close=fee,
|
||||||
|
open_rate=buy_limit,
|
||||||
|
open_rate_requested=buy_limit,
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
|
exchange=self.exchange.id,
|
||||||
|
open_order_id=order_id,
|
||||||
|
strategy=self.strategy.get_strategy_name(),
|
||||||
|
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
||||||
)
|
)
|
||||||
return False
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
amount = stake_amount / buy_limit
|
return False
|
||||||
|
|
||||||
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
|
||||||
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
|
||||||
'exchange': self.exchange.name.capitalize(),
|
|
||||||
'pair': pair_s,
|
|
||||||
'market_url': pair_url,
|
|
||||||
'limit': buy_limit,
|
|
||||||
'stake_amount': stake_amount,
|
|
||||||
'stake_currency': stake_currency,
|
|
||||||
'fiat_currency': fiat_currency
|
|
||||||
})
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
|
||||||
trade = Trade(
|
|
||||||
pair=pair,
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=amount,
|
|
||||||
fee_open=fee,
|
|
||||||
fee_close=fee,
|
|
||||||
open_rate=buy_limit,
|
|
||||||
open_rate_requested=buy_limit,
|
|
||||||
open_date=datetime.utcnow(),
|
|
||||||
exchange=self.exchange.id,
|
|
||||||
open_order_id=order_id,
|
|
||||||
strategy=self.strategy.get_strategy_name(),
|
|
||||||
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
|
||||||
)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process_maybe_execute_buy(self) -> bool:
|
def process_maybe_execute_buy(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -497,9 +515,8 @@ class FreqtradeBot(object):
|
|||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
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, custom_orders) = self.strategy.get_signal(self.exchange,
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
trade.pair, self.strategy.ticker_interval)
|
||||||
ticker)
|
|
||||||
|
|
||||||
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
|
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
|
||||||
if should_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
|
@ -14,6 +14,7 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -74,29 +75,29 @@ class IStrategy(ABC):
|
|||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
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 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
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param metadata: Additional information, like the currently traded pair
|
:param pair: Pair currently analyzed
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param metadata: Additional information, like the currently traded pair
|
:param pair: Pair currently analyzed
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -106,31 +107,51 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
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
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
:return DataFrame with ticker data and indicator data
|
: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 = parse_ticker_dataframe(ticker_history)
|
||||||
dataframe = self.advise_indicators(dataframe, metadata)
|
dataframe = self.advise_indicators(dataframe, pair)
|
||||||
dataframe = self.advise_buy(dataframe, metadata)
|
dataframe = self.advise_buy(dataframe, pair)
|
||||||
dataframe = self.advise_sell(dataframe, metadata)
|
dataframe = self.advise_sell(dataframe, pair)
|
||||||
return dataframe
|
|
||||||
|
|
||||||
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
|
Calculates current signal based several technical analysis indicators
|
||||||
:param pair: pair in format ANT/BTC
|
:param pair: pair in format ANT/BTC
|
||||||
:param interval: Interval to use (in min)
|
: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:
|
if not ticker_hist:
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
|
dataframe, custom_orders = self.analyze_ticker(ticker_hist, pair)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Unable to analyze ticker for pair %s: %s',
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
@ -155,13 +176,13 @@ class IStrategy(ABC):
|
|||||||
# Check if dataframe is out of date
|
# Check if dataframe is out of date
|
||||||
signal_date = arrow.get(latest['date'])
|
signal_date = arrow.get(latest['date'])
|
||||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
# if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
||||||
logger.warning(
|
# logger.warning(
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
# 'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair,
|
# pair,
|
||||||
(arrow.utcnow() - signal_date).seconds // 60
|
# (arrow.utcnow() - signal_date).seconds // 60
|
||||||
)
|
# )
|
||||||
return False, False
|
# return False, False
|
||||||
|
|
||||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -171,7 +192,7 @@ class IStrategy(ABC):
|
|||||||
str(buy),
|
str(buy),
|
||||||
str(sell)
|
str(sell)
|
||||||
)
|
)
|
||||||
return buy, sell
|
return buy, sell, custom_orders
|
||||||
|
|
||||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||||
sell: bool) -> SellCheckTuple:
|
sell: bool) -> SellCheckTuple:
|
||||||
@ -212,7 +233,6 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
Based on current profit of the trade and configured (trailing) stoploss,
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
decides to sell or not
|
decides to sell or not
|
||||||
:param current_profit: current profit in percent
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trailing_stop = self.config.get('trailing_stop', False)
|
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
|
# check if we have a special stop loss for positive condition
|
||||||
# and if profit is positive
|
# and if profit is positive
|
||||||
stop_loss_value = self.stoploss
|
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 > 0:
|
||||||
|
|
||||||
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
|
||||||
|
|
||||||
# Ignore mypy error check in configuration that this is a float
|
# Ignore mypy error check in configuration that this is a float
|
||||||
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||||
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
|
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
|
||||||
f"with offset {sl_offset:.4g} "
|
f"since we have profit {current_profit}")
|
||||||
f"since we have profit {current_profit:.4f}%")
|
|
||||||
|
|
||||||
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
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
|
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()}
|
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
|
Populate indicators that will be used in the Buy and Sell strategy
|
||||||
This method should not be overridden.
|
This method should not be overridden.
|
||||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
: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
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
if self._populate_fun_len == 2:
|
if self._populate_fun_len == 2:
|
||||||
@ -291,14 +308,14 @@ class IStrategy(ABC):
|
|||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
return self.populate_indicators(dataframe) # type: ignore
|
return self.populate_indicators(dataframe) # type: ignore
|
||||||
else:
|
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
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
This method should not be overridden.
|
This method should not be overridden.
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param pair: Additional information, like the currently traded pair
|
:param pair: The currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
if self._buy_fun_len == 2:
|
if self._buy_fun_len == 2:
|
||||||
@ -306,14 +323,14 @@ class IStrategy(ABC):
|
|||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
return self.populate_buy_trend(dataframe) # type: ignore
|
return self.populate_buy_trend(dataframe) # type: ignore
|
||||||
else:
|
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
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
This method should not be overridden.
|
This method should not be overridden.
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:param pair: Additional information, like the currently traded pair
|
:param pair: The currently traded pair
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
if self._sell_fun_len == 2:
|
if self._sell_fun_len == 2:
|
||||||
@ -321,4 +338,4 @@ class IStrategy(ABC):
|
|||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
return self.populate_sell_trend(dataframe) # type: ignore
|
return self.populate_sell_trend(dataframe) # type: ignore
|
||||||
else:
|
else:
|
||||||
return self.populate_sell_trend(dataframe, metadata)
|
return self.populate_sell_trend(dataframe, pair)
|
||||||
|
Loading…
Reference in New Issue
Block a user