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.

Reading FXCM Tick Data

In [1]:
import pandas as pd
import datetime as dt
import cufflinks as cf  # Cufflinks
cf.set_config_file(offline=True)  # set the plotting mode to offline

The Tick Reader Class

In [2]:
from fxcm_tick_reader import fxcm_tick_reader

Available Symbols

In [3]:
fxcm_tick_reader.get_available_symbols()
Out[3]:
('AUDCAD',
 'AUDCHF',
 'AUDJPY',
 'AUDNZD',
 'CADCHF',
 'EURAUD',
 'EURCHF',
 'EURGBP',
 'EURJPY',
 'EURUSD',
 'GBPCHF',
 'GBPJPY',
 'GBPNZD',
 'GBPUSD',
 'GBPCHF',
 'GBPJPY',
 'GBPNZD',
 'NZDCAD',
 'NZDCHF',
 'NZDJPY',
 'NZDUSD',
 'USDCAD',
 'USDCHF',
 'USDJPY')

Retrieving Tick Data

In [4]:
start = dt.datetime(2018, 2, 1)
stop = dt.datetime(2018, 2, 2)
In [5]:
%time td = fxcm_tick_reader('EURUSD', start, stop)
Fetching data from: https://tickdata.fxcorporate.com/EURUSD/2018/5.csv.gz
CPU times: user 4.21 s, sys: 655 ms, total: 4.87 s
Wall time: 6.75 s
In [6]:
type(td)
Out[6]:
fxcm_tick_reader.fxcm_tick_reader
In [7]:
td.get_raw_data().info()
<class 'pandas.core.frame.DataFrame'>
Index: 1929790 entries, 01/28/2018 22:00:46.425 to 02/02/2018 21:59:00.890
Data columns (total 2 columns):
Bid    float64
Ask    float64
dtypes: float64(2)
memory usage: 44.2+ MB
In [8]:
td.get_raw_data().tail(10)
Out[8]:
Bid Ask
DateTime
02/02/2018 21:58:45.157 1.24548 1.24583
02/02/2018 21:58:46.793 1.24548 1.24584
02/02/2018 21:58:46.832 1.24547 1.24584
02/02/2018 21:58:48.107 1.24546 1.24584
02/02/2018 21:58:53.755 1.24548 1.24585
02/02/2018 21:58:53.772 1.24548 1.24586
02/02/2018 21:58:56.070 1.24551 1.24587
02/02/2018 21:59:00.012 1.24551 1.24589
02/02/2018 21:59:00.487 1.24554 1.24592
02/02/2018 21:59:00.890 1.24553 1.24593

Working with the Tick Data

In [9]:
%time td.get_data().info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1929790 entries, 2018-01-28 22:00:46.425000 to 2018-02-02 21:59:00.890000
Data columns (total 2 columns):
Bid    float64
Ask    float64
dtypes: float64(2)
memory usage: 44.2 MB
CPU times: user 8.16 s, sys: 44.3 ms, total: 8.2 s
Wall time: 8.2 s
In [10]:
%time td.get_data().info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1929790 entries, 2018-01-28 22:00:46.425000 to 2018-02-02 21:59:00.890000
Data columns (total 2 columns):
Bid    float64
Ask    float64
dtypes: float64(2)
memory usage: 44.2 MB
CPU times: user 2.28 ms, sys: 126 µs, total: 2.4 ms
Wall time: 2.33 ms
In [11]:
%%time
data = td.get_data(start='2018-02-01 08:00:00', end='2018-02-01 16:00:00')
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 166645 entries, 2018-02-01 08:00:00.010000 to 2018-02-01 15:59:59.995000
Data columns (total 2 columns):
Bid    166645 non-null float64
Ask    166645 non-null float64
dtypes: float64(2)
memory usage: 3.8 MB
CPU times: user 43.2 ms, sys: 7.99 ms, total: 51.2 ms
Wall time: 48.9 ms
In [12]:
data['Bid'].iplot()

Connecting to the FXCM RESTful API

In [13]:
import fxcm
In [14]:
api = fxcm.fxcm(config_file='fxcm.cfg')
In [15]:
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', 'EUSTX50', 'USDOLLAR', 'USOil', 'UKOil', 'NGAS', 'Bund', 'XAU/USD', 'XAG/USD']

Historical Data

Retrieving Historical Data

In [16]:
candles = api.get_candles('USD/JPY', period='D1', number=10)
In [17]:
# 10 most recent days | daily
candles
Out[17]:
bidopen bidclose bidhigh bidlow askopen askclose askhigh asklow tickqty
date
2017-12-28 22:00:00 113.340 112.856 113.344 112.662 113.356 112.887 113.362 112.664 188295
2017-12-29 22:00:00 112.856 112.651 112.969 112.472 112.887 112.729 112.972 112.474 195780
2018-01-16 22:00:00 110.525 110.438 110.983 110.246 110.555 110.472 110.983 110.247 266884
2018-01-24 22:00:00 110.300 109.218 110.337 108.968 110.316 109.234 110.338 108.970 316080
2018-01-25 22:00:00 109.218 109.402 109.703 108.500 109.234 109.430 109.704 108.501 460700
2018-02-02 22:00:00 109.382 110.152 110.483 109.281 109.411 110.195 110.485 109.282 425260
2018-02-07 22:00:00 109.456 109.301 109.702 108.919 109.458 109.346 109.702 108.920 393238
2018-02-08 22:00:00 109.302 108.736 109.784 108.580 109.347 108.752 109.786 108.580 956165
2018-02-09 22:00:00 108.736 108.770 109.310 108.046 108.752 108.824 109.312 108.047 678415
2018-02-11 22:00:00 108.805 108.842 108.932 108.782 108.855 108.896 108.954 108.798 237
In [18]:
start = dt.datetime(2017, 1, 1)
end = dt.datetime(2018, 1, 1)
In [19]:
candles = api.get_candles('USD/JPY', period='D1',
                         start=start, stop=end)
In [20]:
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 [21]:
# 50 most recent 1 minute bars
candles = api.get_candles('EUR/USD', period='m1', number=50)
candles.tail(10)
Out[21]:
bidopen bidclose bidhigh bidlow askopen askclose askhigh asklow tickqty
date
2018-02-12 19:01:00 1.22823 1.22823 1.22824 1.22820 1.22824 1.22823 1.22824 1.22820 52
2018-02-12 19:02:00 1.22823 1.22821 1.22823 1.22814 1.22823 1.22822 1.22824 1.22815 120
2018-02-12 19:03:00 1.22820 1.22834 1.22834 1.22819 1.22821 1.22835 1.22835 1.22819 148
2018-02-12 19:04:00 1.22835 1.22824 1.22838 1.22824 1.22836 1.22823 1.22839 1.22823 112
2018-02-12 19:05:00 1.22824 1.22822 1.22824 1.22817 1.22823 1.22823 1.22824 1.22817 67
2018-02-12 19:06:00 1.22822 1.22832 1.22833 1.22822 1.22823 1.22833 1.22833 1.22821 57
2018-02-12 19:07:00 1.22831 1.22823 1.22833 1.22823 1.22832 1.22821 1.22832 1.22819 87
2018-02-12 19:08:00 1.22823 1.22816 1.22823 1.22816 1.22821 1.22815 1.22824 1.22815 47
2018-02-12 19:09:00 1.22816 1.22812 1.22816 1.22804 1.22816 1.22814 1.22816 1.22802 82
2018-02-12 19:10:00 1.22812 1.22819 1.22824 1.22808 1.22814 1.22820 1.22826 1.22809 84

Visualization of the Data

In [22]:
data = candles[['askopen', 'askhigh', 'asklow', 'askclose']]
data.columns = ['open', 'high', 'low', 'close']
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 50 entries, 2018-02-12 18:21:00 to 2018-02-12 19:10:00
Data columns (total 4 columns):
open     50 non-null float64
high     50 non-null float64
low      50 non-null float64
close    50 non-null float64
dtypes: float64(4)
memory usage: 2.0 KB
In [23]:
qf = cf.QuantFig(data, title='EUR/USD', legend='top',
                 name='EUR/USD', datalegend=False)
In [24]:
qf.iplot()
In [25]:
qf.add_bollinger_bands(periods=10, boll_std=2, colors=['magenta', 'grey'], fill=True)
qf.data.update()
In [26]:
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.

In [27]:
import datetime
import numpy as np
import pandas as pd

Data Retrieval

In [28]:
candles = api.get_candles('EUR/USD', period='m5',
                         start=dt.datetime(2018, 2, 7),
                          stop=dt.datetime(2018, 2, 9))
In [29]:
data = pd.DataFrame(candles[['askclose', 'bidclose']].mean(axis=1),
                    columns=['midclose'])
In [30]:
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 577 entries, 2018-02-07 00:00:00 to 2018-02-09 00:00:00
Data columns (total 1 columns):
midclose    577 non-null float64
dtypes: float64(1)
memory usage: 9.0 KB
In [31]:
data.tail()
Out[31]:
midclose
date
2018-02-08 23:40:00 1.225980
2018-02-08 23:45:00 1.226005
2018-02-08 23:50:00 1.225855
2018-02-08 23:55:00 1.226100
2018-02-09 00:00:00 1.226270
In [32]:
data.iplot()

Feature Preparation

In [33]:
data['returns'] = np.log(data / data.shift(1))
In [34]:
lags = 5
cols = []
for lag in range(1, lags + 1):
    col = 'lag_%s' % lag
    data[col] = data['returns'].shift(lag)
    cols.append(col)
In [35]:
col = 'momentum'
data[col] = data['returns'].rolling(5).mean().shift(1)
cols.append(col)
In [36]:
cols
Out[36]:
['lag_1', 'lag_2', 'lag_3', 'lag_4', 'lag_5', 'momentum']
In [37]:
from pylab import plt
plt.style.use('seaborn')
%matplotlib inline
In [38]:
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 [39]:
# the "patterns" = 2 ** lags
np.digitize(data[cols], bins=[0])[:10]
Out[39]:
array([[1, 1, 1, 1, 1, 1],
       [1, 1, 1, 1, 1, 1],
       [0, 1, 1, 1, 1, 1],
       [1, 0, 1, 1, 1, 1],
       [0, 1, 0, 1, 1, 1],
       [0, 0, 1, 0, 1, 1],
       [0, 0, 0, 1, 0, 0],
       [1, 0, 0, 0, 1, 1],
       [1, 1, 0, 0, 0, 0],
       [1, 1, 1, 0, 0, 1]])
In [40]:
2 ** len(cols)
Out[40]:
64
In [41]:
data.dropna(inplace=True)

Support Vector Machines

In [42]:
from sklearn import svm
In [43]:
model = svm.SVC(C=100)
In [44]:
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 571 entries, 2018-02-07 00:30:00 to 2018-02-09 00:00:00
Data columns (total 9 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
lag_4        571 non-null float64
lag_5        571 non-null float64
momentum     571 non-null float64
direction    571 non-null float64
dtypes: float64(9)
memory usage: 44.6 KB
In [45]:
%time model.fit(np.sign(data[cols]), np.sign(data['returns']))
CPU times: user 18.4 ms, sys: 2.71 ms, total: 21.1 ms
Wall time: 18.5 ms
Out[45]:
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 [46]:
pred = model.predict(np.sign(data[cols]))
pred[:15]
Out[46]:
array([-1., -1.,  1.,  1., -1., -1.,  1., -1., -1., -1., -1., -1., -1.,
       -1.,  1.])

Vectorized Backtesting

In [47]:
data['position'] = pred
In [48]:
data['strategy'] = data['position'] * data['returns']
In [49]:
# unleveraged | no bid-ask spread or transaction costs | only in-sample
data[['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [50]:
data['position'].value_counts()
Out[50]:
-1.0    386
 1.0    185
Name: position, dtype: int64

Train Test Split

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

Split Feature Sets

In [52]:
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=111)
In [53]:
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 [54]:
# the patterns = buckets ** lags
train_x.head(5)
Out[54]:
lag_1 lag_2 lag_3 lag_4 lag_5 momentum
date
2018-02-07 00:45:00 2 2 2 1 1 2
2018-02-07 01:05:00 2 0 2 2 2 2
2018-02-07 01:10:00 1 2 0 2 2 1
2018-02-07 01:15:00 1 1 2 0 2 1
2018-02-07 01:40:00 3 2 1 1 1 2
In [55]:
test_x.head(5)
Out[55]:
lag_1 lag_2 lag_3 lag_4 lag_5 momentum
date
2018-02-07 00:30:00 1 1 1 3 2 2
2018-02-07 00:35:00 2 1 1 1 3 2
2018-02-07 00:40:00 2 2 1 1 1 2
2018-02-07 00:50:00 2 2 2 2 1 2
2018-02-07 00:55:00 2 2 2 2 2 2
In [56]:
4 ** len(cols)
Out[56]:
4096
In [57]:
ax = data['midclose'][train_x.index].plot(style=['b.'], figsize=(10, 6))
data['midclose'][test_x.index].plot(style=['r.'], ax=ax)
data['midclose'].plot(ax=ax, lw=0.5, style=['k--']);
In [58]:
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 [59]:
model.fit(train_x, train_y)
Out[59]:
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 [60]:
train_pred = model.predict(train_x)
In [61]:
accuracy_score(train_y, train_pred)
Out[61]:
0.8456140350877193
In [62]:
test_pred = model.predict(test_x)
In [63]:
accuracy_score(test_y, test_pred)
Out[63]:
0.527972027972028

Vectorized Backtesting — Direct Predictions

In [64]:
pred = model.predict(np.digitize(data[cols], bins=bins))
pred[:15]
Out[64]:
array([ 1., -1., -1.,  1., -1., -1., -1., -1., -1., -1., -1., -1., -1.,
       -1., -1.])
In [65]:
data['position'] = pred
In [66]:
data['strategy'] = data['position'] * data['returns']
In [67]:
# in-sample | unleveraged | no bid-ask spread or transaction costs
data.loc[train_x.index][['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [68]:
# out-of-sample | unleveraged | no bid-ask spread or transaction costs
data.loc[test_x.index][['returns', 'strategy']].cumsum().apply(np.exp).iplot()
In [69]:
# number of trades
sum(data['position'].diff() != 0)
Out[69]:
259

Retrieving Streaming Data

In [70]:
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 [71]:
api.subscribe_market_data('EUR/USD', (output,))
  1 | EUR/USD | 2018-02-12 19:24:26.706000 | 1.2284, 1.2284, 1.2298, 1.2235
  2 | EUR/USD | 2018-02-12 19:24:27.262000 | 1.2284, 1.2283, 1.2298, 1.2235
  3 | EUR/USD | 2018-02-12 19:24:28.078000 | 1.2284, 1.2284, 1.2298, 1.2235
  4 | EUR/USD | 2018-02-12 19:24:28.781000 | 1.2284, 1.2284, 1.2298, 1.2235
  5 | EUR/USD | 2018-02-12 19:24:32.181000 | 1.2284, 1.2284, 1.2298, 1.2235
  6 | EUR/USD | 2018-02-12 19:24:36.751000 | 1.2284, 1.2284, 1.2298, 1.2235
  7 | EUR/USD | 2018-02-12 19:24:37.168000 | 1.2284, 1.2284, 1.2298, 1.2235
  8 | EUR/USD | 2018-02-12 19:24:38.120000 | 1.2284, 1.2284, 1.2298, 1.2235
  9 | EUR/USD | 2018-02-12 19:24:38.164000 | 1.2284, 1.2283, 1.2298, 1.2235
 10 | EUR/USD | 2018-02-12 19:24:39.122000 | 1.2284, 1.2284, 1.2298, 1.2235
 11 | EUR/USD | 2018-02-12 19:24:39.612000 | 1.2284, 1.2284, 1.2298, 1.2235
 12 | EUR/USD | 2018-02-12 19:24:40.706000 | 1.2284, 1.2284, 1.2298, 1.2235
 13 | EUR/USD | 2018-02-12 19:24:44.932000 | 1.2284, 1.2284, 1.2298, 1.2235
 14 | EUR/USD | 2018-02-12 19:24:52.037000 | 1.2284, 1.2284, 1.2298, 1.2235
In [72]:
api.get_last_price('EUR/USD')
Out[72]:
Bid     1.22836
Ask     1.22836
High    1.22977
Low     1.22350
Name: 2018-02-12 19:24:52.037000, dtype: float64
 15 | EUR/USD | 2018-02-12 19:24:54.786000 | 1.2284, 1.2284, 1.2298, 1.2235
 16 | EUR/USD | 2018-02-12 19:24:56.348000 | 1.2284, 1.2284, 1.2298, 1.2235
In [73]:
api.unsubscribe_market_data('EUR/USD')

Placing Orders via the RESTful API

In [74]:
api.get_open_positions()
Out[74]:
In [75]:
order = api.create_market_buy_order('EUR/USD', 100)
In [76]:
order.get_currency()
Out[76]:
'EUR/USD'
In [77]:
order.get_isBuy()
Out[77]:
True
In [78]:
cols = ['tradeId', 'amountK', 'currency', 'grossPL', 'isBuy']
In [79]:
api.get_open_positions()[cols]
Out[79]:
tradeId amountK currency grossPL isBuy
0 21545112 100 EUR/USD -2 True
In [80]:
order = api.create_market_buy_order('USD/JPY', 50)
In [81]:
api.get_open_positions()[cols]
Out[81]:
tradeId amountK currency grossPL isBuy
0 21545112 100 EUR/USD -2.00000 True
1 21545140 50 USD/JPY -1.37921 True
In [82]:
order = api.create_market_sell_order('EUR/USD', 25)
In [83]:
order = api.create_market_buy_order('USD/JPY', 50)
In [84]:
api.get_open_positions()[cols]
Out[84]:
tradeId amountK currency grossPL isBuy
0 21545140 50 USD/JPY -1.37921 True
1 17942944 75 EUR/USD -14.25000 True
2 21545190 50 USD/JPY -0.91947 True
In [85]:
order = api.create_market_sell_order('EUR/USD', 50)
In [86]:
api.get_open_positions()[cols]
Out[86]:
tradeId amountK currency grossPL isBuy
0 21545140 50 USD/JPY -1.37921 True
1 21545190 50 USD/JPY -0.91947 True
2 17942945 25 EUR/USD -4.50000 True
In [87]:
api.close_all_for_symbol('USD/JPY')
In [88]:
api.get_open_positions()[cols]
Out[88]:
tradeId amountK currency grossPL isBuy
0 17942945 25 EUR/USD -4.75 True
In [89]:
api.close_all()
In [90]:
api.get_open_positions()
Out[90]:

A Time-Based, Automated Algorithm

In [91]:
import time
In [92]:
for _ in range(5):
    print(50 * '=')
    print('TRADE NO {}'.format(_))
    order = api.create_market_buy_order('EUR/USD', 100)
    print('POSITIONS\n', api.get_open_positions()[cols])
    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  21545420      100  EUR/USD       -1   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 1
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  21545491      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 2
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  21545509      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 3
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  21545531      100  EUR/USD       -4   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 4
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  21545572      100  EUR/USD       -1   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []

The Python Quants