The Python Quants

FXCM Algorithmic Trading Initiative

RESTful API & Automated Trading

Dr. Yves J. Hilpisch

The Python Quants GmbH

Risk Disclaimer

Trading forex/CFDs on margin carries a high level of risk and may not be suitable for all investors as you could sustain losses in excess of deposits. Leverage can work against you. Due to the certain restrictions imposed by the local law and regulation, German resident retail client(s) could sustain a total loss of deposited funds but are not subject to subsequent payment obligations beyond the deposited funds. Be aware and fully understand all risks associated with the market and trading. Prior to trading any products, carefully consider your financial situation and experience level. Any opinions, news, research, analyses, prices, or other information is provided as general market commentary, and does not constitute investment advice. FXCM & TPQ will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information.

Speaker Disclaimer

The speaker is neither an employee, agent nor representative of FXCM and is therefore acting independently. The opinions given are their own, constitute general market commentary, and do not constitute the opinion or advice of FXCM or any form of personal or investment advice. FXCM assumes no responsibility for any loss or damage, including but not limited to, any loss or gain arising out of the direct or indirect use of this or any other content. Trading forex/CFDs on margin carries a high level of risk and may not be suitable for all investors as you could sustain losses in excess of deposits.

Imports

In [1]:
import numpy as np
import pandas as pd
import datetime as dt
import cufflinks as cf
import warnings; warnings.simplefilter('ignore')
from statsmodels.tsa.stattools import adfuller
cf.set_config_file(offline=True)

Connecting to the FXCM RESTful API

You can install fxcmpy via

pip install fxcmpy

The documentation is currently found under http://fxcmpy.tpq.io

In [2]:
import fxcmpy
In [3]:
api = fxcmpy.fxcmpy(config_file='fxcm.cfg')
In [4]:
instruments = api.get_instruments()
print(instruments)
['EUR/USD', 'USD/JPY', 'GBP/USD', 'USD/CHF', 'EUR/CHF', 'AUD/USD', 'USD/CAD', 'NZD/USD', 'EUR/GBP', 'EUR/JPY', 'GBP/JPY', 'CHF/JPY', 'GBP/CHF', 'EUR/AUD', 'EUR/CAD', 'AUD/CAD', 'AUD/JPY', 'CAD/JPY', 'NZD/JPY', 'GBP/CAD', 'GBP/NZD', 'GBP/AUD', 'AUD/NZD', 'USD/SEK', 'EUR/SEK', 'EUR/NOK', 'USD/NOK', 'USD/MXN', 'AUD/CHF', 'EUR/NZD', 'USD/ZAR', 'USD/HKD', 'ZAR/JPY', 'USD/TRY', 'EUR/TRY', 'NZD/CHF', 'CAD/CHF', 'NZD/CAD', 'TRY/JPY', 'USD/CNH', 'AUS200', 'ESP35', 'FRA40', 'GER30', 'HKG33', 'JPN225', 'NAS100', 'SPX500', 'UK100', 'US30', 'Copper', 'CHN50', 'EUSTX50', 'USDOLLAR', 'USOil', 'UKOil', 'SOYF', 'NGAS', 'Bund', 'XAU/USD', 'XAG/USD']

Historical Data

Retrieving Historical Data

In [5]:
candles = api.get_candles('USD/JPY', period='D1', number=10)
In [6]:
# 10 most recent days | daily
candles
Out[6]:
bidopen bidclose bidhigh bidlow askopen askclose askhigh asklow tickqty
date
2018-03-05 22:00:00 105.494 106.202 106.238 105.351 105.547 106.220 106.239 105.352 278878
2018-03-06 22:00:00 106.202 106.130 106.463 105.852 106.220 106.146 106.465 105.855 535538
2018-03-07 22:00:00 106.130 106.060 106.229 105.456 106.146 106.091 106.222 105.458 310564
2018-03-08 22:00:00 106.060 106.193 106.315 105.892 106.091 106.247 106.316 105.894 235470
2018-03-09 22:00:00 106.193 106.814 107.050 106.150 106.247 106.822 107.055 106.176 263242
2018-03-11 21:00:00 106.616 106.632 106.680 106.568 106.676 106.680 106.706 106.580 167
2018-03-12 21:00:00 106.632 106.410 106.972 106.310 106.680 106.435 106.974 106.315 177547
2018-03-13 21:00:00 106.410 106.566 107.293 106.254 106.435 106.583 107.298 106.255 335733
2018-03-14 21:00:00 106.566 106.314 106.749 106.064 106.583 106.326 106.750 106.066 296421
2018-03-15 21:00:00 106.314 106.323 106.415 105.785 106.326 106.358 106.417 105.786 290884
In [7]:
start = dt.datetime(2017, 1, 1)
end = dt.datetime(2018, 1, 1)
In [8]:
candles = api.get_candles('USD/JPY', period='D1',
                         start=start, stop=end)
In [9]:
candles['askclose'].iplot()

The parameter period must be one of m1, m5, m15, m30, H1, H2, H3, H4, H6, H8, D1, W1 or M1.

In [10]:
# 50 most recent 1 minute bars
candles = api.get_candles('EUR/USD', period='m1', number=150)
candles.tail(10)
Out[10]:
bidopen bidclose bidhigh bidlow askopen askclose askhigh asklow tickqty
date
2018-03-16 08:59:00 1.23193 1.23182 1.23195 1.23173 1.23196 1.23182 1.23197 1.23173 265
2018-03-16 09:00:00 1.23182 1.23197 1.23199 1.23178 1.23181 1.23198 1.23200 1.23177 262
2018-03-16 09:01:00 1.23197 1.23187 1.23199 1.23175 1.23198 1.23190 1.23200 1.23177 309
2018-03-16 09:02:00 1.23187 1.23189 1.23209 1.23182 1.23190 1.23192 1.23211 1.23182 275
2018-03-16 09:03:00 1.23190 1.23211 1.23211 1.23190 1.23193 1.23213 1.23213 1.23193 192
2018-03-16 09:04:00 1.23211 1.23235 1.23243 1.23211 1.23213 1.23235 1.23242 1.23213 295
2018-03-16 09:05:00 1.23235 1.23221 1.23240 1.23221 1.23235 1.23223 1.23239 1.23222 116
2018-03-16 09:06:00 1.23221 1.23251 1.23252 1.23214 1.23223 1.23252 1.23254 1.23217 203
2018-03-16 09:07:00 1.23250 1.23236 1.23254 1.23229 1.23251 1.23236 1.23255 1.23229 231
2018-03-16 09:08:00 1.23237 1.23250 1.23251 1.23237 1.23237 1.23253 1.23253 1.23236 155

Visualization of the Data

In [11]:
data = candles[['askopen', 'askhigh', 'asklow', 'askclose']]
data.columns = ['open', 'high', 'low', 'close']
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 150 entries, 2018-03-16 06:39:00 to 2018-03-16 09:08:00
Data columns (total 4 columns):
open     150 non-null float64
high     150 non-null float64
low      150 non-null float64
close    150 non-null float64
dtypes: float64(4)
memory usage: 5.9 KB
In [12]:
qf = cf.QuantFig(data, title='EUR/USD', legend='top',
                 name='EUR/USD', datalegend=False)
In [13]:
qf.iplot()
In [14]:
qf.add_bollinger_bands(periods=10, boll_std=2,
                       colors=['magenta', 'grey'], fill=True)
qf.data.update()
In [15]:
qf.iplot()

Using Machine Learning for Market Prediction

The following example is simplified and for illustration purposes only. Among others, it does not consider transactions costs or bid-ask spreads.

Data Retrieval

In [16]:
candles = api.get_candles('EUR/USD', period='m5',
                         start=dt.datetime(2018, 3, 7),
                          stop=dt.datetime(2018, 3, 9))
In [17]:
data = pd.DataFrame(candles[['askclose', 'bidclose']].mean(axis=1),
                    columns=['midclose'])
In [18]:
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 577 entries, 2018-03-07 00:00:00 to 2018-03-09 00:00:00
Data columns (total 1 columns):
midclose    577 non-null float64
dtypes: float64(1)
memory usage: 9.0 KB
In [19]:
data.tail()
Out[19]:
midclose
date
2018-03-08 23:40:00 1.230740
2018-03-08 23:45:00 1.230650
2018-03-08 23:50:00 1.230640
2018-03-08 23:55:00 1.230675
2018-03-09 00:00:00 1.230475
In [20]:
data.iplot()

Feature Preparation

In [21]:
adfuller(data['midclose'])
Out[21]:
(-0.43109097853561518,
 0.90477684775574496,
 12,
 564,
 {'1%': -3.4419977165341673,
  '10%': -2.5695064902419396,
  '5%': -2.866678179017994},
 -7148.7105567169738)
In [22]:
data['returns'] = np.log(data / data.shift(1))
In [23]:
adfuller(data['returns'].dropna())
Out[23]:
(-6.1637622713234856,
 7.0707849747078846e-08,
 11,
 564,
 {'1%': -3.4419977165341673,
  '10%': -2.5695064902419396,
  '5%': -2.866678179017994},
 -7376.5616345073167)
In [24]:
lags = 3
cols = []
for lag in range(1, lags + 1):
    col = 'lag_%s' % lag
    data[col] = data['returns'].shift(lag)
    cols.append(col)
In [25]:
col = 'momentum'
data[col] = data['returns'].rolling(5).mean().shift(1)
cols.append(col)
In [26]:
cols
Out[26]:
['lag_1', 'lag_2', 'lag_3', 'momentum']
In [27]:
from pylab import plt
plt.style.use('seaborn')
%matplotlib inline
In [28]:
data['direction'] = np.sign(data['returns'])
to_plot = ['midclose', 'returns', 'direction']
data[to_plot].iloc[:100].plot(figsize=(10, 6),
        subplots=True, style=['-', '-', 'ro'], title='EUR/USD');
In [29]:
# the "patterns" = 2 ** lags
np.digitize(data[cols], bins=[0])[:10]
Out[29]:
array([[1, 1, 1, 1],
       [1, 1, 1, 1],
       [0, 1, 1, 1],
       [0, 0, 1, 1],
       [0, 0, 0, 1],
       [1, 0, 0, 1],
       [1, 1, 0, 0],
       [0, 1, 1, 0],
       [1, 0, 1, 1],
       [0, 1, 0, 1]])
In [30]:
2 ** len(cols)
Out[30]:
16
In [31]:
data.dropna(inplace=True)

Support Vector Machines

The Model

In [32]:
from sklearn import svm
In [33]:
model = svm.SVC(C=100)

Fitting

In [34]:
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 571 entries, 2018-03-07 00:30:00 to 2018-03-09 00:00:00
Data columns (total 7 columns):
midclose     571 non-null float64
returns      571 non-null float64
lag_1        571 non-null float64
lag_2        571 non-null float64
lag_3        571 non-null float64
momentum     571 non-null float64
direction    571 non-null float64
dtypes: float64(7)
memory usage: 35.7 KB
In [35]:
%time model.fit(np.sign(data[cols]), np.sign(data['returns']))
CPU times: user 19.7 ms, sys: 3.56 ms, total: 23.2 ms
Wall time: 20.4 ms
Out[35]:
SVC(C=100, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

Predicting Market Direction

In [36]:
pred = model.predict(np.sign(data[cols]))
pred[:15]
Out[36]:
array([-1., -1.,  1.,  1.,  1.,  1., -1.,  1., -1.,  1., -1., -1., -1.,
        1.,  1.])

Vectorized Backtesting

In [37]:
data['position'] = pred
In [38]:
data['strategy'] = data['position'] * data['returns']
In [39]:
# unleveraged | no bid-ask spread or transaction costs | only in-sample
data[['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [40]:
data['position'].value_counts()
Out[40]:
 1.0    290
-1.0    280
 0.0      1
Name: position, dtype: int64

Train Test Split

In [41]:
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

Split Feature Sets

In [42]:
mu = data['returns'].mean()
v = data['returns'].std()
bins = [mu - v, mu, mu + v]
train_x, test_x, train_y, test_y = train_test_split(
    data[cols].apply(lambda x: np.digitize(x, bins=bins)),                   
    np.sign(data['returns']),
    test_size=0.50, random_state=1111)
In [43]:
train_x.sort_index(inplace=True)
train_y.sort_index(inplace=True)
test_x.sort_index(inplace=True)
test_y.sort_index(inplace=True)
In [44]:
# the patterns = buckets ** lags
train_x.head(5)
Out[44]:
lag_1 lag_2 lag_3 momentum
date
2018-03-07 00:45:00 1 2 1 2
2018-03-07 01:00:00 3 0 1 1
2018-03-07 01:05:00 1 3 0 1
2018-03-07 01:10:00 1 1 3 1
2018-03-07 01:15:00 1 1 1 1
In [45]:
train_y.tail(5)
Out[45]:
date
2018-03-08 23:00:00    1.0
2018-03-08 23:15:00   -1.0
2018-03-08 23:20:00   -1.0
2018-03-08 23:35:00   -1.0
2018-03-08 23:55:00    1.0
Name: returns, dtype: float64
In [46]:
4 ** len(cols)
Out[46]:
256
In [47]:
ax = data['midclose'].iloc[-100:][train_x.index].plot(style=['bo'], figsize=(10, 6))
data['midclose'].iloc[-100:][test_x.index].plot(style=['ro'], ax=ax)
data['midclose'].iloc[-100:].plot(ax=ax, lw=0.5, style=['k--']);

Model Fitting & Prediction

In [48]:
model.fit(train_x, train_y)
Out[48]:
SVC(C=100, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)
In [49]:
train_pred = model.predict(train_x)
In [50]:
accuracy_score(train_y, train_pred)
Out[50]:
0.69824561403508767
In [51]:
test_pred = model.predict(test_x)
In [52]:
accuracy_score(test_y, test_pred)
Out[52]:
0.56993006993006989

Vectorized Backtesting — Direct Predictions

In [54]:
pred = model.predict(data[cols].apply(
                    lambda x: np.digitize(x, bins=bins)).dropna())
pred[:15]
Out[54]:
array([-1.,  1., -1.,  1.,  1.,  1., -1.,  1., -1., -1.,  1., -1.,  1.,
       -1.,  1.])
In [55]:
data['position'] = pred
In [56]:
data['strategy'] = data['position'] * data['returns']
In [57]:
# in-sample | unleveraged | no bid-ask spread or transaction costs
data.loc[train_x.index][['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [58]:
# out-of-sample | unleveraged | no bid-ask spread or transaction costs
data.loc[test_x.index][['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [59]:
# number of trades
sum(data['position'].diff() != 0)
Out[59]:
333

Retrieving Streaming Data

In [60]:
def output(data, dataframe):
    print('%3d | %s | %s | %6.4f, %6.4f, %6.4f, %6.4f' 
          % (len(dataframe), data['Symbol'],
             pd.to_datetime(int(data['Updated']), unit='ms'), 
             data['Rates'][0], data['Rates'][1],
             data['Rates'][2], data['Rates'][3]))
In [61]:
api.subscribe_market_data('EUR/USD', (output,))
In [63]:
api.get_last_price('EUR/USD')
Out[63]:
Bid     1.23226
Ask     1.23227
High    1.23256
Low     1.22950
Name: 2018-03-16 09:12:36.386000, dtype: float64
  4 | EUR/USD | 2018-03-16 09:12:36.496000 | 1.2323, 1.2323, 1.2326, 1.2295
  5 | EUR/USD | 2018-03-16 09:12:38.448000 | 1.2323, 1.2323, 1.2326, 1.2295
  6 | EUR/USD | 2018-03-16 09:12:38.614000 | 1.2323, 1.2323, 1.2326, 1.2295
  7 | EUR/USD | 2018-03-16 09:12:39.439000 | 1.2323, 1.2323, 1.2326, 1.2295
  8 | EUR/USD | 2018-03-16 09:12:41.896000 | 1.2323, 1.2323, 1.2326, 1.2295
  9 | EUR/USD | 2018-03-16 09:12:42.347000 | 1.2323, 1.2323, 1.2326, 1.2295
 10 | EUR/USD | 2018-03-16 09:12:44.783000 | 1.2322, 1.2322, 1.2326, 1.2295
 11 | EUR/USD | 2018-03-16 09:12:47.476000 | 1.2323, 1.2322, 1.2326, 1.2295
 12 | EUR/USD | 2018-03-16 09:12:47.638000 | 1.2322, 1.2322, 1.2326, 1.2295
In [64]:
api.unsubscribe_market_data('EUR/USD')

Placing Orders via the RESTful API

In [65]:
api.get_open_positions()
Out[65]:
In [66]:
order = api.create_market_buy_order('EUR/USD', 100)
In [67]:
order.get_currency()
Out[67]:
'EUR/USD'
In [68]:
order.get_isBuy()
Out[68]:
True
In [69]:
sel = ['tradeId', 'amountK', 'currency', 'grossPL', 'isBuy']
In [70]:
api.get_open_positions()[sel]
Out[70]:
tradeId amountK currency grossPL isBuy
0 47247426 100 EUR/USD 3 True
In [71]:
order = api.create_market_buy_order('USD/JPY', 50)
In [72]:
api.get_open_positions()[sel]
Out[72]:
tradeId amountK currency grossPL isBuy
0 47247426 100 EUR/USD 3.0000 True
1 47247429 50 USD/JPY -0.4728 True
In [73]:
order = api.create_market_sell_order('EUR/USD', 25)
In [74]:
order = api.create_market_buy_order('USD/JPY', 50)
In [75]:
api.get_open_positions()[sel]
Out[75]:
tradeId amountK currency grossPL isBuy
0 47247429 50 USD/JPY -5.20134 True
1 47247495 75 EUR/USD 12.75000 True
2 47247434 50 USD/JPY -0.94570 True
In [76]:
order = api.create_market_sell_order('EUR/USD', 50)
In [77]:
api.get_open_positions()[sel]
Out[77]:
tradeId amountK currency grossPL isBuy
0 47247429 50 USD/JPY -3.78268 True
1 47247434 50 USD/JPY 0.47284 True
2 47247496 25 EUR/USD 4.25000 True
In [78]:
api.close_all_for_symbol('USD/JPY')
In [79]:
api.get_open_positions()[sel]
Out[79]:
tradeId amountK currency grossPL isBuy
0 47247496 25 EUR/USD 5.25 True
In [80]:
api.close_all()
In [81]:
api.get_open_positions()
Out[81]:

A Time-Based, Automated Algorithm

In [82]:
import time
In [84]:
for _ in range(5):
    print(50 * '=')
    print('TRADE NO {}'.format(_))
    order = api.create_market_buy_order('EUR/USD', 100)
    time.sleep(1)
    print('POSITIONS\n', api.get_open_positions()[sel])
    time.sleep(7)
    api.close_all_for_symbol('EUR/USD')
    print('POSITIONS\n', api.get_open_positions())
    time.sleep(7)
==================================================
TRADE NO 0
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  47247442      100  EUR/USD       14   True
1  47247448      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 1
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  47247452      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 2
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  47247456      100  EUR/USD       -1   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 3
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  47247460      100  EUR/USD       -3   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 4
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  47247467      100  EUR/USD       -1   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []

The Python Quants