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.

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]:
print(fxcm_tick_reader.get_available_symbols())
('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, 3, 7)
stop = dt.datetime(2018, 3, 8)
In [5]:
%time td = fxcm_tick_reader('EURUSD', start, stop)
Fetching data from: https://tickdata.fxcorporate.com/EURUSD/2018/10.csv.gz
CPU times: user 2.81 s, sys: 486 ms, total: 3.3 s
Wall time: 3.96 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: 1446427 entries, 03/04/2018 22:00:03.859 to 03/09/2018 21:59:23.423
Data columns (total 2 columns):
Bid    1446427 non-null float64
Ask    1446427 non-null float64
dtypes: float64(2)
memory usage: 33.1+ MB
In [8]:
td.get_raw_data().tail(10)
Out[8]:
Bid Ask
DateTime
03/09/2018 21:58:51.369 1.23055 1.23067
03/09/2018 21:58:52.369 1.23056 1.23067
03/09/2018 21:58:52.883 1.23056 1.23068
03/09/2018 21:59:00.009 1.23056 1.23067
03/09/2018 21:59:00.066 1.23057 1.23071
03/09/2018 21:59:00.075 1.23056 1.23084
03/09/2018 21:59:00.082 1.23056 1.23086
03/09/2018 21:59:00.093 1.23057 1.23086
03/09/2018 21:59:00.603 1.23058 1.23086
03/09/2018 21:59:23.423 1.23034 1.23086

Working with the Tick Data

In [9]:
%time td.get_data().info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1446427 entries, 2018-03-04 22:00:03.859000 to 2018-03-09 21:59:23.423000
Data columns (total 2 columns):
Bid    1446427 non-null float64
Ask    1446427 non-null float64
dtypes: float64(2)
memory usage: 33.1 MB
CPU times: user 5.05 s, sys: 16.6 ms, total: 5.07 s
Wall time: 5.06 s
In [10]:
%time td.get_data().info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 1446427 entries, 2018-03-04 22:00:03.859000 to 2018-03-09 21:59:23.423000
Data columns (total 2 columns):
Bid    1446427 non-null float64
Ask    1446427 non-null float64
dtypes: float64(2)
memory usage: 33.1 MB
CPU times: user 10.5 ms, sys: 2.66 ms, total: 13.1 ms
Wall time: 9.66 ms
In [11]:
%%time
data = td.get_data(start='2018-03-06 08:00:00', end='2018-03-06 16:00:00')
data.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 144516 entries, 2018-03-06 08:00:00.338000 to 2018-03-06 15:59:59.719000
Data columns (total 2 columns):
Bid    144516 non-null float64
Ask    144516 non-null float64
dtypes: float64(2)
memory usage: 3.3 MB
CPU times: user 35.5 ms, sys: 18.1 ms, total: 53.6 ms
Wall time: 51.1 ms
In [12]:
data['Bid'].iplot()

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 [13]:
import fxcmpy
In [14]:
api = fxcmpy.fxcmpy(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', 'CHN50', 'EUSTX50', 'USDOLLAR', 'USOil', 'UKOil', 'SOYF', '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
2018-02-27 22:00:00 106.907 107.320 107.676 106.784 106.944 107.346 107.677 106.785 284055
2018-02-28 22:00:00 107.320 106.661 107.528 106.565 107.346 106.693 107.528 106.566 280668
2018-03-01 22:00:00 106.661 106.220 107.205 106.162 106.693 106.240 107.207 106.164 320867
2018-03-02 22:00:00 106.220 105.712 106.300 105.250 106.240 105.757 106.298 105.251 642426
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
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=150)
candles.tail(10)
Out[21]:
bidopen bidclose bidhigh bidlow askopen askclose askhigh asklow tickqty
date
2018-03-12 16:02:00 1.23184 1.23198 1.23198 1.23174 1.23187 1.23200 1.23200 1.23176 274
2018-03-12 16:03:00 1.23198 1.23203 1.23205 1.23186 1.23200 1.23205 1.23205 1.23187 270
2018-03-12 16:04:00 1.23202 1.23227 1.23233 1.23202 1.23204 1.23228 1.23234 1.23204 261
2018-03-12 16:05:00 1.23228 1.23226 1.23229 1.23220 1.23228 1.23227 1.23230 1.23220 216
2018-03-12 16:06:00 1.23226 1.23215 1.23229 1.23215 1.23227 1.23217 1.23231 1.23215 351
2018-03-12 16:07:00 1.23215 1.23239 1.23240 1.23215 1.23217 1.23242 1.23242 1.23216 171
2018-03-12 16:08:00 1.23239 1.23225 1.23239 1.23217 1.23242 1.23227 1.23242 1.23217 378
2018-03-12 16:09:00 1.23224 1.23224 1.23233 1.23223 1.23226 1.23226 1.23234 1.23221 92
2018-03-12 16:10:00 1.23225 1.23229 1.23235 1.23221 1.23227 1.23230 1.23236 1.23221 297
2018-03-12 16:11:00 1.23229 1.23215 1.23238 1.23215 1.23230 1.23217 1.23241 1.23216 330

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: 150 entries, 2018-03-12 13:42:00 to 2018-03-12 16:11: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 [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, 3, 7),
                          stop=dt.datetime(2018, 3, 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-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 [31]:
data.tail()
Out[31]:
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 [32]:
data.iplot()

Feature Preparation

In [33]:
data['returns'] = np.log(data / data.shift(1))
In [34]:
lags = 3
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', '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],
       [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 [40]:
2 ** len(cols)
Out[40]:
16
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-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 [45]:
%time model.fit(np.sign(data[cols]), np.sign(data['returns']))
CPU times: user 17.6 ms, sys: 3.6 ms, total: 21.2 ms
Wall time: 18.2 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    290
-1.0    280
 0.0      1
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]:
nos = 50
mu = data['returns'].rolling(nos).mean()
v = data['returns'].rolling(nos).std()
def digits(s):
    r = np.where(s < mu - v, 0, np.nan)
    r = np.where(s > mu - v, 1, r)
    r = np.where(s > mu, 2, r)
    r = np.where(s > mu + v, 3, r)
    return r    
In [53]:
data[cols].apply(digits)
bins = [mu - v, mu, mu + v]
train_x, test_x, train_y, test_y = train_test_split(
    data[cols].apply(digits).dropna(),                   
    np.sign(data['returns']).iloc[nos-1:],
    test_size=0.50, random_state=111)
In [54]:
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 [55]:
# the patterns = buckets ** lags
train_x.head(5)
Out[55]:
lag_1 lag_2 lag_3 momentum
date
2018-03-07 04:40:00 1.0 2.0 2.0 2.0
2018-03-07 04:50:00 0.0 1.0 1.0 1.0
2018-03-07 05:10:00 2.0 2.0 1.0 1.0
2018-03-07 05:15:00 2.0 2.0 2.0 2.0
2018-03-07 05:20:00 2.0 2.0 2.0 2.0
In [56]:
test_x.tail(5)
Out[56]:
lag_1 lag_2 lag_3 momentum
date
2018-03-08 23:05:00 2.0 1.0 1.0 2.0
2018-03-08 23:15:00 0.0 1.0 2.0 1.0
2018-03-08 23:25:00 1.0 1.0 0.0 1.0
2018-03-08 23:45:00 2.0 0.0 2.0 1.0
2018-03-08 23:50:00 1.0 2.0 0.0 1.0
In [57]:
4 ** len(cols)
Out[57]:
256
In [58]:
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 [59]:
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 [60]:
model.fit(train_x, train_y)
Out[60]:
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 [61]:
train_pred = model.predict(train_x)
In [62]:
accuracy_score(train_y, train_pred)
Out[62]:
0.7318007662835249
In [63]:
test_pred = model.predict(test_x)
In [64]:
accuracy_score(test_y, test_pred)
Out[64]:
0.5517241379310345

Vectorized Backtesting — Direct Predictions

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

Retrieving Streaming Data

In [71]:
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 [72]:
api.subscribe_market_data('EUR/USD', (output,))
In [74]:
api.get_last_price('EUR/USD')
Out[74]:
Bid     1.23241
Ask     1.23243
High    1.23412
Low     1.22904
Name: 2018-03-12 16:13:54.550000, dtype: float64
  4 | EUR/USD | 2018-03-12 16:13:56.926000 | 1.2324, 1.2324, 1.2341, 1.2290
  5 | EUR/USD | 2018-03-12 16:13:58.018000 | 1.2324, 1.2324, 1.2341, 1.2290
  6 | EUR/USD | 2018-03-12 16:13:58.969000 | 1.2324, 1.2324, 1.2341, 1.2290
  7 | EUR/USD | 2018-03-12 16:13:59.279000 | 1.2324, 1.2324, 1.2341, 1.2290
  8 | EUR/USD | 2018-03-12 16:13:59.904000 | 1.2324, 1.2324, 1.2341, 1.2290
  9 | EUR/USD | 2018-03-12 16:14:00.577000 | 1.2324, 1.2324, 1.2341, 1.2290
 10 | EUR/USD | 2018-03-12 16:14:02.455000 | 1.2324, 1.2324, 1.2341, 1.2290
 11 | EUR/USD | 2018-03-12 16:14:02.980000 | 1.2324, 1.2324, 1.2341, 1.2290
In [75]:
api.unsubscribe_market_data('EUR/USD')
 12 | EUR/USD | 2018-03-12 16:14:03.415000 | 1.2324, 1.2324, 1.2341, 1.2290

Placing Orders via the RESTful API

In [76]:
api.get_open_positions()
Out[76]:
In [77]:
order = api.create_market_buy_order('EUR/USD', 100)
In [78]:
order.get_currency()
Out[78]:
'EUR/USD'
In [79]:
order.get_isBuy()
Out[79]:
True
In [80]:
cols = ['tradeId', 'amountK', 'currency', 'grossPL', 'isBuy']
In [81]:
api.get_open_positions()[cols]
Out[81]:
tradeId amountK currency grossPL isBuy
0 23384542 100 EUR/USD -2 True
In [82]:
order = api.create_market_buy_order('USD/JPY', 50)
In [83]:
api.get_open_positions()[cols]
Out[83]:
tradeId amountK currency grossPL isBuy
0 23384542 100 EUR/USD -1.00000 True
1 23384543 50 USD/JPY 0.93836 True
In [84]:
order = api.create_market_sell_order('EUR/USD', 25)
In [85]:
order = api.create_market_buy_order('USD/JPY', 50)
In [86]:
api.get_open_positions()[cols]
Out[86]:
tradeId amountK currency grossPL isBuy
0 23384543 50 USD/JPY -2.34605 True
1 23384597 75 EUR/USD -2.25000 True
2 23384548 50 USD/JPY -0.46921 True
In [87]:
order = api.create_market_sell_order('EUR/USD', 50)
In [88]:
api.get_open_positions()[cols]
Out[88]:
tradeId amountK currency grossPL isBuy
0 23384543 50 USD/JPY 0.00000 True
1 23384548 50 USD/JPY 1.87675 True
2 23384598 25 EUR/USD -1.75000 True
In [89]:
api.close_all_for_symbol('USD/JPY')
In [90]:
api.get_open_positions()[cols]
Out[90]:
tradeId amountK currency grossPL isBuy
0 23384598 25 EUR/USD -4.75 True
In [91]:
api.close_all()
In [92]:
api.get_open_positions()
Out[92]:

A Time-Based, Automated Algorithm

In [93]:
import time
In [94]:
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()[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  23385959      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 1
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  23386404      100  EUR/USD       -1   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 2
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  23386558      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 3
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  23386769      100  EUR/USD       -2   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []
==================================================
TRADE NO 4
POSITIONS
     tradeId  amountK currency  grossPL  isBuy
0  23386912      100  EUR/USD        3   True
POSITIONS
 Empty DataFrame
Columns: []
Index: []

The Python Quants