Today, even small entities that trade complex instruments or are granted sufficient leverage can threaten the global financial system. —Paul Singer
Today, it is easier than ever to get started with trading in the financial markets. There is a large number of online trading platforms (brokers) available from which an algorithmic trader can choose. The choice of a platform might be influenced by multiple factors:
This chapter focuses on Oanda, an online trading platform that is well suited to deploy automated, algorithmic trading strategies, even for retail traders. The following is a brief description of Oanda along the criteria as outlined previously:
Figure 8-1. Oanda trading application fxTrade Practice
####################################
For more details on CFDs, see the Investopedia CFD page or the more detailed Wikipedia CFD page. There are CFDs available on currency pairs (for example, EUR/USD), commodities (for example, gold), stock indices (for example, S&P 500 stock index), bonds (for example, German 10 Year Bund), and more. One can think of a product range that basically allows one to implement global macro strategies. Financially speaking, CFDs are derivative products that derive their payoff based on the development of prices for other instruments. In addition, trading activity (liquidity) influences the price of CFDs. Although a CFD might be based on the S&P 500 index, it is a completely different product issued, quoted, and supported by Oanda (or a similar provider).
This brings along certain risks that traders should be aware of. A recent event that illustrates this issue is the Swiss Franc event that led to a number of insolvencies/ ɪnˈsɑːlvənsi /破产,无力偿还,倒闭 in the online broker space. See, for instance, the article Currency Brokers Fall Over Like Dominoes多米诺骨牌 After SNB(Swiss National Bank) Decison on Swiss Franc.
####################################
The chapter is organized as follows.
The goal of this chapter is to make use of the approaches and technologies as introduced in previous chapters to automatically trade on the Oanda platform.
The process for setting up an account with Oanda is simple and efficient. You can choose between a real account and a free demo (“practice”) account, which absolutely suffices to implement what follows (see Figures 8-3 and 8-4). https://www.oanda.com/apply/select
Figure 8-3. Oanda account registration (account types)
Figure 8-4. Oanda account registration (registration form)
If the registration is successful and you are logged in to the account on the platform, you should see a starting page, as shown in Figure 8-5. In the middle, you will find a download link for the fxTrade Practice for Desktop application, which you should install. Once it is running, it looks similar to the screenshot shown in Figure 8-1.https://www.oanda.com/demo-account/
Figure 8-5. Oanda account starting page
After registration, getting access to the APIs of Oanda is an easy affair. The major ingredients needed are the account number and the access token (API key). You will find the account number, for instance, in the area My Services(https://www.oanda.com/demo-account/tpa/personal_token) . The access token can be generated in the area Manage API Access (see Figure 8-6).
From now on, the configparser module is used to manage account credentials. The module expects a text file—with a filename, say, of pyalgo.cfg—in the following format for use with an Oanda practice account:
[oanda]
account_id = '101-001-23023947-001'
access_token = '795cc4bb80b7e2eb0c7c314ee196dbae-2871ba6122cb4c670a2439e07f998611'
account_type = practice
To access the API via Python, it is recommended to use the Python wrapper package tpqoa (see GitHub repository) that in turn relies on the v20 package from Oanda (see GitHub repository)
https://git-scm.com/download/win
After install 64-bit Git for Windows Setup. then
git clone https://github.com/yhilpisch/tpqoa
cd tpqoa
python setup.py install
Next, use the following command:https://github.com/yhilpisch/tpqoa
pip install git+https://github.com/yhilpisch/tpqoa.git
A major benefit of working with the Oanda platform is that the complete price history of all Oanda instruments is accessible via the RESTful API. In this context, complete history refers to the different CFDs themselves, not the underlying instruments they are defined on.
For an overview of what instruments can be traded for a given account, use the .get_instruments() method. It only retrieves the display names and technical instruments, names from the API. More details are available via the API, such as minimum position size:
oanda.get_instruments() error : solution
import v20
# http://developer.oanda.com/rest-live-v20/development-guide/
# REST API
# 120 requests per second. Excess requests will receive HTTP 429 error.
# This restriction is applied against the requesting IP address.
PRACTICE_API_HOST = 'api-fxpractice.oanda.com' # fxTrade Practice
# Stream API
# 20 active streams. Requests above this threshold will be rejected.
# This restriction is applied against the requesting IP address.
PRACTICE_STREAM_HOST = 'stream-fxpractice.oanda.com' # fxTrade Practice
# REST API
LIVE_API_HOST = 'api-fxtrade.oanda.com' # fxTrade
# Stream API
LIVE_STREAM_HOST = 'stream-fxtrade.oanda.com' # fxTrade
PORT = '443'
account_id = '101-001-23023947-001'
token = '795cc4bb80b7e2eb0c7c314ee196dbae-2871ba6122cb4c670a2439e07f998611'
ctx = v20.Context(PRACTICE_API_HOST, PORT, token=token)
ctx_stream = v20.Context(PRACTICE_STREAM_HOST, PORT, token=token)
def get_instruments():
''' Retrieves and returns all instruments for the given account. '''
resp = ctx.account.instruments( account_id )
instruments = resp.get('instruments')
instruments = [ ins.dict()
for ins in instruments
]
instruments = [ (ins['displayName'], ins['name'] )
for ins in instruments
]
return sorted( instruments )
get_instruments()[:15]
The example that follows uses the instrument EUR_USD based on the EUR/USD currency pair. The goal is to backtest momentum-based strategies on one-minute bars. The data used is for two days in May 2020.
#######################
import pandas as pd
suffix = '.000000000Z'
def transform_datetime(dt):
''' Transforms Python datetime object to string.
if dt = '2020-05-27'
then pd.Timestamp(dt) ==> Timestamp('2020-05-27 00:00:00')
Timestamp.to_pydatetime():
Convert a Timestamp object to a native Python datetime object.
pd.Timestamp(dt).to_pydatetime() ==> datetime.datetime(2020, 5, 27, 0, 0)
pd.Timestamp(dt).to_pydatetime().isoformat('T') ==> '2020-05-27T00:00:00'
'''
if isinstance( dt, str ):
dt = pd.Timestamp(dt).to_pydatetime()
return dt.isoformat('T') + suffix
start = '2020-05-27'
transform_datetime(start)
def retrieve_data(instrument, start, end, granularity, price):
raw = ctx.instrument.candles( instrument=instrument,
fromTime=start, toTime=end,
granularity=granularity, price=price
)
raw = raw.get('candles')
raw = [ Candlestick_object.dict()
for Candlestick_object in raw
]
# raw :
# [ {'time': '2020-05-27T04:00:00.000000000Z',
# 'mid': {'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616},
# 'volume': 22,
# 'complete': True
# },
# ...
if price == 'A': # 'ask'
for cs in raw:
cs.update( cs['ask'] )
del cs['ask']
elif price == 'B': # 'bid'
for cs in raw:
cs.update( cs['bid'] )
del cs['bid']
elif price == 'M': # 'mid'
for cs in raw:
# https://www.programiz.com/python-programming/methods/dictionary/update
# The update() method updates the dictionary with
# the elements from another dictionary object
# or from an iterable of key/value pairs.
cs.update( cs['mid'] )
# {'time': '2020-05-27T04:00:00.000000000Z',
# 'mid': {'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616},
# 'volume': 22,
# 'complete': True,
# 'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616 ### <== cs['mid']
# }
del cs['mid']
# raw :
# [ { 'time': '2020-05-27T04:00:00.000000000Z',
# 'volume': 22,
# 'complete': True,
# 'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616
# },
# ...
else:
raise ValueError("price must be either 'B', 'A' or 'M'.")
if len(raw) == 0:
return pd.DataFrame() # return empty DataFrame if no data
#else:
data = pd.DataFrame( raw )
# convert isoformat to Timestamp when possible, otherwise datetime.datetime
data['time'] = pd.to_datetime( data['time'] )
# 2020-05-27T04:00:00.000000000Z ==> 2020-05-27 04:00:00+00:00
data = data.set_index('time')
data.index = pd.DatetimeIndex( data.index )
for col in list('ohlc'):
data[col] = data[col].astype(float)
return data
instrument = 'EUR_USD'
start = '2020-05-27'
end = '2020-05-29'
granularity = 'M1'
price = 'M'
retrieve_data(instrument, start, end, granularity, price)
http://developer.oanda.com/rest-live-v20/instrument-df/
Value Description
S5 5 second candlesticks, minute alignment
S10 10 second candlesticks, minute alignment
S15 15 second candlesticks, minute alignment
S30 30 second candlesticks, minute alignment
M1 1 minute candlesticks, minute alignment
M2 2 minute candlesticks, hour alignment
M4 4 minute candlesticks, hour alignment
M5 5 minute candlesticks, hour alignment
M10 10 minute candlesticks, hour alignment
M15 15 minute candlesticks, hour alignment
M30 30 minute candlesticks, hour alignment
H1 1 hour candlesticks, hour alignment
H2 2 hour candlesticks, day alignment
H3 3 hour candlesticks, day alignment
H4 4 hour candlesticks, day alignment
H6 6 hour candlesticks, day alignment
H8 8 hour candlesticks, day alignment
H12 12 hour candlesticks, day alignment
D 1 day candlesticks, day alignment
W 1 week candlesticks, aligned to start of week
M 1 month candlesticks, aligned to first day of the month
The first step is to retrieve the raw data from Oanda:
import pandas as pd
suffix = '.000000000Z'
def transform_datetime(dt):
''' Transforms Python datetime object to string.
if dt = '2020-05-27'
then pd.Timestamp(dt) ==> Timestamp('2020-05-27 00:00:00')
Timestamp.to_pydatetime():
Convert a Timestamp object to a native Python datetime object.
pd.Timestamp(dt).to_pydatetime() ==> datetime.datetime(2020, 5, 27, 0, 0)
pd.Timestamp(dt).to_pydatetime().isoformat('T') ==> '2020-05-27T00:00:00'
'''
if isinstance( dt, str ):
dt = pd.Timestamp(dt).to_pydatetime()
return dt.isoformat('T') + suffix
def retrieve_data(instrument, start, end, granularity, price):
raw = ctx.instrument.candles( instrument=instrument,
fromTime=start, toTime=end,
granularity=granularity, price=price
)
raw = raw.get('candles')
raw = [ Candlestick_object.dict()
for Candlestick_object in raw
]
# raw :
# [ {'time': '2020-05-27T04:00:00.000000000Z',
# 'mid': {'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616},
# 'volume': 22,
# 'complete': True
# },
# ...
if price == 'A': # 'ask'
for cs in raw:
cs.update( cs['ask'] )
del cs['ask']
elif price == 'B': # 'bid'
for cs in raw:
cs.update( cs['bid'] )
del cs['bid']
elif price == 'M': # 'mid'
for cs in raw:
# https://www.programiz.com/python-programming/methods/dictionary/update
# The update() method updates the dictionary with
# the elements from another dictionary object
# or from an iterable of key/value pairs.
cs.update( cs['mid'] )
# {'time': '2020-05-27T04:00:00.000000000Z',
# 'mid': {'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616},
# 'volume': 22,
# 'complete': True,
# 'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616 ### <== cs['mid']
# }
del cs['mid']
# raw :
# [ { 'time': '2020-05-27T04:00:00.000000000Z',
# 'volume': 22,
# 'complete': True,
# 'o': 1.09623, 'h': 1.0963, 'l': 1.09616, 'c': 1.09616
# },
# ...
else:
raise ValueError("price must be either 'B', 'A' or 'M'.")
if len(raw) == 0:
return pd.DataFrame() # return empty DataFrame if no data
#else:
data = pd.DataFrame( raw )
# convert isoformat to Timestamp when possible, otherwise datetime.datetime
data['time'] = pd.to_datetime( data['time'] )
# 2020-05-27T04:00:00.000000000Z ==> 2020-05-27 04:00:00+00:00
data = data.set_index('time')
data.index = pd.DatetimeIndex( data.index )
for col in list('ohlc'):
data[col] = data[col].astype(float)
return data
MAX_REQUEST_COUNT = float(5000)
def get_history( instrument, start, end,
granularity, price, localize=True ):
''' Retrieves historical data for instrument.
Parameters
==========
instrument: string
valid instrument name
start, end: datetime, str
Python datetime or string objects for start and end
granularity: string
a string like 'S5', 'M1' or 'D'
price: string
one of 'A' (ask), 'B' (bid) or 'M' (middle)
Returns
=======
data: pd.DataFrame
pandas DataFrame object with data
'''
if granularity.startswith('S') \
or granularity.startswith('M') \
or granularity.startswith('H'):
# filter(function, iterable object)
# for example, granularity = 'M11' ==> "".join( filter() ) ==> '11' ==> float() ==>11.0
# for example, granularity = 'M1' ==> "".join( filter() ) ==> '1' ==> float() ==>1.0
multiplier = float( "".join( filter ( str.isdigit, granularity )
)
)
if granularity.startswith('S'):
# freq = '1h' = 60 minutes = 3600 seconds
# for example:
# price = 19.99
# f"The price of this book is {price}"
# ==> 'The price of this book is 19.99'
freq = f"{int( MAX_REQUEST_COUNT * multiplier / float(3600) ) }H"
else:
# freq = '1D' = 24 hours = 24 * 60 = 1440 minutes
# int( 5000 * 1.0 / float(1440) ) ==> int(3.4722222222222223) = 3
freq = f"{int( MAX_REQUEST_COUNT * multiplier / float(1440) ) }D"
data = pd.DataFrame()
# https://pandas.pydata.org/docs/reference/api/pandas.date_range.html
# pd.date_range(start='1/1/2018', periods=5, freq='3M')
# DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31',
# '2019-01-31'],
# dtype='datetime64[ns]', freq='3M'
dr = pd.date_range(start, end, freq=freq)
for index in range( len(dr) ):
batch_start = transform_datetime( dr[index] )
if index != len(dr) - 1:
batch_end = transform_datetime( dr[index+1] )
else:
batch_end = transform_datetime( end )
batch = retrieve_data( instrument,
batch_start, batch_end,
granularity,
price # 'A': ask, 'B': bid, 'M': mid
)
data = pd.concat([data,
batch
], axis=0)
else:
start = transform_datetime( start )
end = transform_datetime( end )
data = retrieve_data( instrument,
start, end,
granularity,
price
)
if localize: # Passing 'None' will remove the time zone information preserving local time.
data.index = data.index.tz_localize(None)
return data[['o', 'h', 'l', 'c', 'volume', 'complete']]
# Defines the parameter values.
instrument = 'EUR_USD'
start = '2020-08-10'
end = '2020-08-12'
granularity = 'M1' # 1 minute candlesticks, minute alignment
price = 'M' # 'A': ask, 'B': bid, 'M': mid
data = get_history( instrument,
start, end,
granularity,
price
)
data
# Shows the meta information for the retrieved data set.
data.info()
# Shows the first five data rows for two columns.
data[ ['c' , 'volume'] ].head()
For quantitative analysis of returns, we are interested in the logarithm of returns. Why use log returns over simple returns? There are several reasons(Try to multiply many small numbers in Python. Eventually it rounds off to 0.https://blog.csdn.net/Linli522362242/article/details/122955700,http://web.vu.lt/mif/a.buteikis/wp-content/uploads/2019/02/Lecture_03.pdf), but the most important of them is normalization, and this avoids the problem of negative prices.
We can use the shift() function of pandas to shift the values by a certain number of periods. The dropna() method removes the unused values at the end of the logarithmic calculation transformation. The log() method of NumPy helps to calculate the logarithm of all values in the DataFrame object as a vector,
import matplotlib.pyplot as plt
data['c'].hist( figsize=(10,5), color='blue', bins=100)
plt.show()
A histogram(histogram vs barshttps://blog.csdn.net/Linli522362242/article/details/123116143) can be used to give us a rough sense of the data density estimation over a bin interval of 100:
data['c'].plot( subplots=False,
figsize=(10,5),
color='blue'
)
Normalization:
simple return:
( data['c']-data['c'].shift(1) ).dropna().hist(figsize=(10,5), color='blue', bins=100)
Log returns(log_return):between two times 0 < s < t are normally distributed.
import numpy as np
import matplotlib.pyplot as plt
data['returns'] = np.log( data['c']/data['c'].shift(1) ).dropna()
data['returns'].hist( figsize=(10,5), color='blue', bins=100 )
plt.show()
( data['c']-data['c'].shift(1) ).dropna().plot( subplots=False,
figsize=(10,5),
color='blue',
)
plt.show()
data['returns'].plot( subplots=False,
figsize=(10,5),
color='blue'
)
plt.show()
# Mean and standard deviation of differenced data
df_rolling = data['c'].rolling(12)
df_rolling_ma = df_residual_diff_rolling.mean()
df_rolling_std = df_residual_diff_rolling.std()
# Plot the stationary data
plt.figure( figsize=(12,8) )
plt.plot( data['c'], label='Differenced', c='k' )
plt.plot( df_rolling_ma, label='Mean', c='b' )
plt.plot( df_rolling_std, label='Std', c='y' )
plt.legend()
from the figure, we know the prices is not stationary.https://blog.csdn.net/Linli522362242/article/details/121406833 Therefore, we will need to make this time series stationary.https://blog.csdn.net/Linli522362242/article/details/126113926
# Mean and standard deviation of differenced data
df_rolling = data['c'].rolling(12)
df_rolling_ma = df_residual_diff_rolling.mean()
df_rolling_std = df_residual_diff_rolling.std()
# Plot the stationary data
plt.figure( figsize=(12,8) )
plt.plot( data['c'], label='Differenced', c='k' )
#plt.plot( df_rolling_ma, label='Mean', c='b' )
#plt.plot( df_rolling_std, label='Std', c='y' )
plt.legend()
the log return:
# Mean and standard deviation of differenced data
df_residual_diff_rolling = data['returns'].rolling(12)
df_residual_diff_rolling_ma = df_residual_diff_rolling.mean()
df_residual_diff_rolling_std = df_residual_diff_rolling.std()
# Plot the stationary data
plt.figure( figsize=(12,8) )
plt.plot( data['returns'], label='Differenced', c='k' )
plt.plot( df_residual_diff_rolling_ma, label='Mean', c='b' )
plt.plot( df_residual_diff_rolling_std, label='Std', c='y' )
plt.legend()
The second step is to implement the vectorized backtesting. The idea is to simultaneously backtest a couple of momentum strategies. The code is straightforward and concise (see also Chapter 4).
For simplicity, the following code uses close ( c ) values of mid prices(price = 'M') only :
import numpy as np
import matplotlib.pyplot as plt
# Calculates the log returns based on the close values of the mid prices.
data['returns'] = np.log( data['c']/data['c'].shift(1) )
data
Momentum, also referred to as MOM, is an important measure of speed and magnitude of price moves. This is often a key indicator of trend/breakout-based trading algorithms.
In its simplest form, momentum is simply the difference between the current price and price of some fixed time periods in the past.
Often, we use simple/exponential moving averages of the MOM indicator, as shown here, to detect sustained trends:
https://blog.csdn.net/Linli522362242/article/details/121406833
Here, the following applies:: Price at time t
: Price n time periods before time t ( or price at time t-n)
Here n=1, then ==>
Simple moving average, which we will refer to as SMA, is a basic technical analysis indicator. The simple moving average, as you may have guessed from its name, is computed by adding up the price of an instrument over a certain period of time divided by the number of time periods. It is basically the price average over a certain time period, with equal weight being used for each price. The time period over which it is averaged is often referred to as the lookback period or history. Let's have a look at the following formula of the simple moving average:
Here, the following applies:
Find a moving average for log_return:
cols = []
# granularity = 'M1' # 1 minute candlesticks, minute alignment
# [15, 30, 60, 120] : [15 minutes, 30 minutes, 60 minutes, 120 minutes]
for momentum in [15, 30, 60, 120]:
col = 'position_{}'.format( momentum )
# direction( Average log_return for Consecutive Periods ) as our prediction
data[col] = np.sign( data['returns'].rolling( momentum ).mean() )
cols.append(col)
data
from pylab import plt
plt.style.use( 'seaborn' )
import matplotlib as mpl
mpl.rcParams['font.family'] = 'serif'
strats = ['returns']
the direction of average log_return for consecutive periods as our decision (buy or sell)
for col in cols:
# col.split('_')[1] : the number of time periods(unit = minute)
# cols[0]='position_15'==> cols[0].split('_')[1] ==> '15'
strat = 'strategy_{}'.format( col.split('_')[1] )
# the direction of average log_return for consecutive periods as our decision
# direction(our decision: buy or sell) * today log_return ==> today actual log_return(Profit or Loss)
data[strat] = data[col].shift(1) * data['returns']
strats.append(strat)
colormap={
'returns':'m', # Cumulative return <== cumsum().apply( np.exp ) # the passive benchmark investment
'strategy_15':'y', # yellow
'strategy_30':'b', #blue
'strategy_60':'k', #black
'strategy_120':'r' #red
}
# data[strats].dropna().cumsum().apply( np.exp ) ==> normal return
data[strats].dropna().cumsum().apply( np.exp ).plot( figsize=(12,8),
title='Gross performance of diferent Momentum Strategies for EUR_USD instrument(minute bars)',
style=colormap
)
plt.show()
Plots the cumulative performances for the instrument and the strategies.
when using Momentum Strategy, 15 minutes as consecutive periods to predict the direction of log return in the market is better
Figure 8-7. Gross performance of diferent momentum strategies for EUR_USD instrument
(minute bars)
In general, when you buy a share of a stock for, say, 100 USD, the profit and loss (P&L) calculations are straightforward: if the stock price rises by 1 USD, you earn 1 USD (unrealized profit); if the stock price falls by 1 USD, you lose 1 USD (unrealized loss). If you buy 10 shares, just multiply the results by 10.
Trading CFDs on the Oanda platform involves leverage and margin. This significantly influences the P&L calculation. For an introduction to and overview of this topic refer to Oanda fxTrade Margin Rules. A simple example can illustrate the major aspects in this context.
Consider that a EUR-based algorithmic trader wants to trade the EUR_USD instrument on the Oanda platform and wants to get a long exposure of 10,000 EUR at an ask price of 1.1. Without leverage and margin, the trader (or Python program) would buy 10,000 units of the CFD(Contracts for Difference).
Note that for some instruments,
If the price of the instrument (exchange rate) rises to 1.105 (as the midpoint rate between bid and ask prices), the absolute profit is 10,000 x (1.105-1.1) = 10,000 x 0.005 = 50 EUR or 0.5%(=50/10,000).
What impact do leverage and margining have? Suppose the algorithmic trader chooses a leverage ratio of 20:1, which translates into a 5% margin保证金,押金 (= 100% / 20). This in turn implies that the trader only needs to put up a margin upfront of 10,000 EUR x 5% = 500 EUR to get the same exposure. If the price of the instrument then rises to 1.105, the absolute profit stays the same at 50 EUR, but the relative profit rises to 50 EUR / 500 EUR = 10%. The return is considerably amplified by a factor of 20; this is the benefit of leverage when things go as desired.
What happens if things go south? Assume the instrument price drops to 1.08 (as the midpoint rate between bid and ask prices), leading to an absolute loss of 10,000 x (1.08 - 1.1) = -200 EUR or 2%=(-200/10,000). The relative loss now is -200 EUR / 500 EUR = -40%.
Leveraged trading does not only amplify potentials profits, but it also amplifies potential losses. With leveraged trading based on a 10:1 factor (100%/10=10% margin, 10,000 EUR x 10% = 1000 EUR ), a 10% adverse move in the base instrument already wipes out the complete margin( 10,000 x ( 1.1 x (-10%) ) = -1100 EUR ). In other words, a 10% move leads to a 100% loss. Therefore, you should make sure to fully understand all risks involved in leveraged trading. You should also make sure to apply appropriate risk measures, such as stop loss orders, that are in line with your risk profile and appetite.
You have a USD account美元账户意味需要使用美元支付 with maximum leverage set to 20:1 and a long 10,000 EUR/GBP open position. The current rate for EUR/USD is 1.1320/1.1321, therefore the current midpoint rate of EUR/USD is 1.13205( = (1.1320+1.1321)/2 ).
For the leverage calculation, the lower of the maximum regulated leverage and your selected leverage is used. The regulator监管机构 allows 50:1 leverage on EUR/GBP, but because you have selected a 20:1 leverage for your account, a leverage of 20:1 (or 5% margin保证金 requirement= 100% /20 ) is used.
Your margin used is position size x Margin Requirement = 10,000 EUR x 5% = 500 EUR. The Margin Used in your account currency = 500 x 1.13205 = 566.025 USD.
You have a USD account with balance of 1,000 USD and a long 10,000 EUR/USD open position opened at a rate of 1.1200. The current rate of EUR/USD is 1.13200/1.13210, therefore the current midpoint rate of EUR/USD is 1.13205( = (1.1320+1.1321)/2 ).
Your unrealized P/L calculated by the current midpoint rate is (current midpoint rate – open rate) x position size =(Theoretical Exit Price – Average Open Price) * Position= (1.13205 – 1.1200) x 10,000 = 120.50 USD.
Your Margin Closeout Value保证金平仓值 is 1,000 + 120.50 = 1,120.50 USD.
https://blog.csdn.net/Linli522362242/article/details/121896073
Figure 8-8 shows the amplifying effect on the performance of the momentum strategies for a leverage ratio of 20:1. The initial margin of 5%(= 100% / 20) suffices to cover potential losses since it is not eaten up even in the worst case depicted:
# With leveraged trading based on a 20:1 factor
# The return is considerably amplified by a factor of 20
data[strats].dropna().cumsum().apply( lambda x: x*20 )\
.apply( np.exp).plot( figsize=(12,8),
title='Gross performance of momentum strategies for EUR_USD instrument with 20:1 leverage (minute bars)',
style=colormap
)
plt.show()
Working with streaming data is again made simple and straightforward by the Python wrapper package tpqoa. The package, in combination with the v20 package, takes care of the socket communication such that the algorithmic trader only needs to decide what to do with the streaming data:
############
https://github.com/oanda/v20-python/blob/master/src/v20/pricing.py
"pricing.ClientPrice",
"pricing.PricingHeartbeat",
https://oanda-api-v20.readthedocs.io/en/master/endpoints/pricing/pricingstream.html
{
"status": "tradeable",
"instrument": "EUR_JPY",
"asks": [
{
"price": "114.312",
"liquidity": 1000000
},
{
"price": "114.313",
"liquidity": 2000000
},
{
"price": "114.314",
"liquidity": 5000000
},
{
"price": "114.316",
"liquidity": 10000000
}
],
"time": "2016-10-27T08:38:43.094548890Z",
"closeoutAsk": "114.316",
"type": "PRICE",
"closeoutBid": "114.291",
"bids": [
{
"price": "114.295",
"liquidity": 1000000
},
{
"price": "114.294",
"liquidity": 2000000
},
{
"price": "114.293",
"liquidity": 5000000
},
{
"price": "114.291",
"liquidity": 10000000
}
]
},
{
"type": "HEARTBEAT",
"time": "2016-10-27T08:38:44.327443673Z"
},
{
...
############
def on_success(time, bid, ask):
''' Method called when new data is retrieved. '''
print(time, bid, ask)
stop_stream = False
def stream_data(instrument, stop=None, ret=False, callback=None):
''' Starts a real-time data stream.
Parameters
==========
instrument: string
valid instrument name
stop : int
stops the streaming after a certain number of ticks retrieved.
'''
stream_instrument = instrument
ticks = 0
# https://github.com/oanda/v20-python/blob/master/src/v20/pricing.py
# accountID : Account Identifier
# instruments : List of Instruments to stream Prices for.
# snapshot: Flag that enables/disables the sending of a pricing snapshot
# when initially connecting to the stream.
response = ctx_stream.pricing.stream( account_id,
snapshot=True,
instruments=instrument
)
# https://github.com/oanda/v20-python/blob/master/src/v20/response.py
# response.parts() ==> call self.line_parser ==> class Parser() in the pricing.py
msgs = []
for msg_type, msg in response.parts():
msgs.append(msg)
# print(msg_type, msg) ###########
if msg_type == "pricing.PricingHeartbeat":
continue
elif msg_type == 'pricing.ClientPrice':
ticks += 1
time = msg.time
# along with list objects in the bids and asks properties.
# Typically, Level 1 quotes are available,
# so we read the first item of the each list.
# Each item in the list is a price_bucket object,
# from which we extract the bid and ask price.
if callback is not None:
callback( msg.instrument, msg.time,
float( msg.bids[0].dict()['price'] ),
float( msg.asks[0].dict()['price'] )
)
else:
# msg.time == msg['time'] or msg.bids == msg['bids']
on_success( msg.time,
float( msg.bids[0].dict()['price'] ),
float( msg.asks[0].dict()['price'] )
)
if stop is not None:
if ticks >= stop:
if ret: # if return msgs
return msgs
break
if stop_stream:
if ret:
return msgs
break
instrument = 'EUR_USD'
stream_data(instrument, stop=10)
Similarly, it is straightforward to place market buy or sell orders with the create_order() method:
https://developer.oanda.com/rest-live-v20/transaction-df/
Definitions
{
#
# The Client ID of the Order/Trade
#
id : (ClientID),
#
# A tag associated with the Order/Trade
#
tag : (ClientTag),
#
# A comment associated with the Order/Trade
#
comment : (ClientComment)
}
{
#
# The price that the Stop Loss Order will be triggered at. Only one of the
# price and distance fields may be specified.
#
price : (PriceValue),
#
# Specifies the distance (in price units) from the Trade’s open price to use
# as the Stop Loss Order price.
# Only one of the distance and price <= price : (PriceValue),
#
distance : (DecimalNumber),
#
# The time in force for the created Stop Loss Order. This may only be GTC,
# GTD or GFD.
#
timeInForce : (TimeInForce, default=GTC),
#
# The date when the Stop Loss Order will be cancelled on if timeInForce is
# GTD.
#
gtdTime : (DateTime),
#
# The Client Extensions to add to the Stop Loss Order when created.
#
clientExtensions : (ClientExtensions),
#
# Flag indicating that the price for the Stop Loss Order is guaranteed. The
# default value depends on the GuaranteedStopLossOrderMode of the account,
# if it is REQUIRED, the default will be true, for DISABLED or ENABLED the
# default is false.
#
#
# Deprecated: Will be removed in a future API update.
#
guaranteed : (boolean, deprecated)
}
{
#
# The distance (in price units) from the Trade’s fill price that the
# Trailing Stop Loss Order will be triggered at.
#
distance : (DecimalNumber),
#
# The time in force for the created Trailing Stop Loss Order. This may only
# be GTC, GTD or GFD.
#
timeInForce : (TimeInForce, default=GTC),
#
# The date when the Trailing Stop Loss Order will be cancelled on if
# timeInForce is GTD.
#
gtdTime : (DateTime),
#
# The Client Extensions to add to the Trailing Stop Loss Order when
# created.
#
clientExtensions : (ClientExtensions)
}
{
#
# The price that the Take Profit Order will be triggered at. Only one of
# the price and distance fields may be specified.
#
price : (PriceValue),
#
# The time in force for the created Take Profit Order. This may only be
# GTC, GTD or GFD.
#
timeInForce : (TimeInForce, default=GTC),
#
# The date when the Take Profit Order will be cancelled on if timeInForce
# is GTD.
#
gtdTime : (DateTime),
#
# The Client Extensions to add to the Take Profit Order when created.
#
clientExtensions : (ClientExtensions)
}
def market(self, accountID, **kwargs): # 4524
"""
Shortcut to create a Market Order in an Account
Args:
accountID : The ID of the Account
kwargs : The arguments to create a MarketOrderRequest
Returns:
v20.response.Response containing the results from submitting
the request
"""
return self.create(
accountID,
order=MarketOrderRequest(**kwargs)
)
class MarketOrderRequest(BaseEntity): # 2185
"""
A MarketOrderRequest specifies the parameters that may be set when creating
a Market Order.
"""
#
# Format string used when generating a summary for this object
#
_summary_format = "{units} units of {instrument}"
#
# Format string used when generating a name for this object
#
_name_format = "Market Order Request"
#
# Property metadata for this object
#
_properties = spec_properties.order_MarketOrderRequest
def __init__(self, **kwargs):
"""
Create a new MarketOrderRequest instance
"""
super(MarketOrderRequest, self).__init__()
#
# The type of the Order to Create. Must be set to "MARKET" when
# creating a Market Order.
#
self.type = kwargs.get("type", "MARKET")
#
# The Market Order's Instrument.
#
self.instrument = kwargs.get("instrument")
#
# The quantity requested to be filled by the Market Order. A posititive
# number of units results in a long Order, and a negative number of
# units results in a short Order.
#
self.units = kwargs.get("units")
#
# The time-in-force requested for the Market Order. Restricted to FOK
# or IOC for a MarketOrder.
#
self.timeInForce = kwargs.get("timeInForce", "FOK")
#
# The worst price that the client is willing to have the Market Order
# filled at.
#
self.priceBound = kwargs.get("priceBound")
#
# Specification of how Positions in the Account are modified when the
# Order is filled.
#
self.positionFill = kwargs.get("positionFill", "DEFAULT")
#
# The client extensions to add to the Order. Do not set, modify, or
# delete clientExtensions if your account is associated with MT4.
#
self.clientExtensions = kwargs.get("clientExtensions")
#
# TakeProfitDetails specifies the details of a Take Profit Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Take Profit, or when a Trade's
# dependent Take Profit Order is modified directly through the Trade.
#
self.takeProfitOnFill = kwargs.get("takeProfitOnFill")
#
# StopLossDetails specifies the details of a Stop Loss Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Stop Loss, or when a Trade's
# dependent Stop Loss Order is modified directly through the Trade.
#
self.stopLossOnFill = kwargs.get("stopLossOnFill")
#
# TrailingStopLossDetails specifies the details of a Trailing Stop Loss
# Order to be created on behalf of a client. This may happen when an
# Order is filled that opens a Trade requiring a Trailing Stop Loss, or
# when a Trade's dependent Trailing Stop Loss Order is modified
# directly through the Trade.
#
self.trailingStopLossOnFill = kwargs.get("trailingStopLossOnFill")
#
# Client Extensions to add to the Trade created when the Order is
# filled (if such a Trade is created). Do not set, modify, or delete
# tradeClientExtensions if your account is associated with MT4.
#
self.tradeClientExtensions = kwargs.get("tradeClientExtensions")
def market_if_touched(self, accountID, **kwargs): # 4618
"""
Shortcut to create a MarketIfTouched Order in an Account
Args:
accountID : The ID of the Account
kwargs : The arguments to create a MarketIfTouchedOrderRequest
Returns:
v20.response.Response containing the results from submitting
the request
"""
return self.create(
accountID,
order=MarketIfTouchedOrderRequest(**kwargs)
)
class MarketIfTouchedOrderRequest(BaseEntity): # 2718
"""
A MarketIfTouchedOrderRequest specifies the parameters that may be set when
creating a Market-if-Touched Order.
"""
#
# Format string used when generating a summary for this object
#
_summary_format = "{units} units of {instrument} @ {price}"
#
# Format string used when generating a name for this object
#
_name_format = "MIT Order Request"
#
# Property metadata for this object
#
_properties = spec_properties.order_MarketIfTouchedOrderRequest
def __init__(self, **kwargs):
"""
Create a new MarketIfTouchedOrderRequest instance
"""
super(MarketIfTouchedOrderRequest, self).__init__()
#
# The type of the Order to Create. Must be set to "MARKET_IF_TOUCHED"
# when creating a Market If Touched Order.
#
self.type = kwargs.get("type", "MARKET_IF_TOUCHED")
#
# The MarketIfTouched Order's Instrument.
#
self.instrument = kwargs.get("instrument")
#
# The quantity requested to be filled by the MarketIfTouched Order. A
# posititive number of units results in a long Order, and a negative
# number of units results in a short Order.
#
self.units = kwargs.get("units")
#
# The price threshold specified for the MarketIfTouched Order. The
# MarketIfTouched Order will only be filled by a market price that
# crosses this price from the direction of the market price at the time
# when the Order was created (the initialMarketPrice). Depending on the
# value of the Order's price and initialMarketPrice, the
# MarketIfTouchedOrder will behave like a Limit or a Stop Order.
#
self.price = kwargs.get("price")
#
# The worst market price that may be used to fill this MarketIfTouched
# Order.
#
self.priceBound = kwargs.get("priceBound")
#
# The time-in-force requested for the MarketIfTouched Order. Restricted
# to "GTC", "GFD" and "GTD" for MarketIfTouched Orders.
#
self.timeInForce = kwargs.get("timeInForce", "GTC")
#
# The date/time when the MarketIfTouched Order will be cancelled if its
# timeInForce is "GTD".
#
self.gtdTime = kwargs.get("gtdTime")
#
# Specification of how Positions in the Account are modified when the
# Order is filled.
#
self.positionFill = kwargs.get("positionFill", "DEFAULT")
#
# Specification of which price component should be used when
# determining if an Order should be triggered and filled. This allows
# Orders to be triggered based on the bid, ask, mid, default (ask for
# buy, bid for sell) or inverse (ask for sell, bid for buy) price
# depending on the desired behaviour. Orders are always filled using
# their default price component. This feature is only provided through
# the REST API. Clients who choose to specify a non-default trigger
# condition will not see it reflected in any of OANDA's proprietary or
# partner trading platforms, their transaction history or their account
# statements. OANDA platforms always assume that an Order's trigger
# condition is set to the default value when indicating the distance
# from an Order's trigger price, and will always provide the default
# trigger condition when creating or modifying an Order. A special
# restriction applies when creating a guaranteed Stop Loss Order. In
# this case the TriggerCondition value must either be "DEFAULT", or the
# "natural" trigger side "DEFAULT" results in. So for a Stop Loss Order
# for a long trade valid values are "DEFAULT" and "BID", and for short
# trades "DEFAULT" and "ASK" are valid.
#
self.triggerCondition = kwargs.get("triggerCondition", "DEFAULT")
#
# The client extensions to add to the Order. Do not set, modify, or
# delete clientExtensions if your account is associated with MT4.
#
self.clientExtensions = kwargs.get("clientExtensions")
#
# TakeProfitDetails specifies the details of a Take Profit Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Take Profit, or when a Trade's
# dependent Take Profit Order is modified directly through the Trade.
#
self.takeProfitOnFill = kwargs.get("takeProfitOnFill")
#
# StopLossDetails specifies the details of a Stop Loss Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Stop Loss, or when a Trade's
# dependent Stop Loss Order is modified directly through the Trade.
#
self.stopLossOnFill = kwargs.get("stopLossOnFill")
#
# TrailingStopLossDetails specifies the details of a Trailing Stop Loss
# Order to be created on behalf of a client. This may happen when an
# Order is filled that opens a Trade requiring a Trailing Stop Loss, or
# when a Trade's dependent Trailing Stop Loss Order is modified
# directly through the Trade.
#
self.trailingStopLossOnFill = kwargs.get("trailingStopLossOnFill")
#
# Client Extensions to add to the Trade created when the Order is
# filled (if such a Trade is created). Do not set, modify, or delete
# tradeClientExtensions if your account is associated with MT4.
#
self.tradeClientExtensions = kwargs.get("tradeClientExtensions")
def limit(self, accountID, **kwargs): # 4542
"""
Shortcut to create a Limit Order in an Account
Args:
accountID : The ID of the Account
kwargs : The arguments to create a LimitOrderRequest
Returns:
v20.response.Response containing the results from submitting
the request
"""
return self.create(
accountID,
order=LimitOrderRequest(**kwargs)
)
class LimitOrderRequest(BaseEntity): # 2340
"""
A LimitOrderRequest specifies the parameters that may be set when creating
a Limit Order.
"""
#
# Format string used when generating a summary for this object
#
_summary_format = "{units} units of {instrument} @ {price}"
#
# Format string used when generating a name for this object
#
_name_format = "Limit Order Request"
#
# Property metadata for this object
#
_properties = spec_properties.order_LimitOrderRequest
def __init__(self, **kwargs):
"""
Create a new LimitOrderRequest instance
"""
super(LimitOrderRequest, self).__init__()
#
# The type of the Order to Create. Must be set to "LIMIT" when creating
# a Market Order.
#
self.type = kwargs.get("type", "LIMIT")
#
# The Limit Order's Instrument.
#
self.instrument = kwargs.get("instrument")
#
# The quantity requested to be filled by the Limit Order. A posititive
# number of units results in a long Order, and a negative number of
# units results in a short Order.
#
self.units = kwargs.get("units")
#
# The price threshold specified for the Limit Order. The Limit Order
# will only be filled by a market price that is equal to or better than
# this price.
#
self.price = kwargs.get("price")
#
# The time-in-force requested for the Limit Order.
#
self.timeInForce = kwargs.get("timeInForce", "GTC")
#
# The date/time when the Limit Order will be cancelled if its
# timeInForce is "GTD".
#
self.gtdTime = kwargs.get("gtdTime")
#
# Specification of how Positions in the Account are modified when the
# Order is filled.
#
self.positionFill = kwargs.get("positionFill", "DEFAULT")
#
# Specification of which price component should be used when
# determining if an Order should be triggered and filled. This allows
# Orders to be triggered based on the bid, ask, mid, default (ask for
# buy, bid for sell) or inverse (ask for sell, bid for buy) price
# depending on the desired behaviour. Orders are always filled using
# their default price component. This feature is only provided through
# the REST API. Clients who choose to specify a non-default trigger
# condition will not see it reflected in any of OANDA's proprietary or
# partner trading platforms, their transaction history or their account
# statements. OANDA platforms always assume that an Order's trigger
# condition is set to the default value when indicating the distance
# from an Order's trigger price, and will always provide the default
# trigger condition when creating or modifying an Order. A special
# restriction applies when creating a guaranteed Stop Loss Order. In
# this case the TriggerCondition value must either be "DEFAULT", or the
# "natural" trigger side "DEFAULT" results in. So for a Stop Loss Order
# for a long trade valid values are "DEFAULT" and "BID", and for short
# trades "DEFAULT" and "ASK" are valid.
#
self.triggerCondition = kwargs.get("triggerCondition", "DEFAULT")
#
# The client extensions to add to the Order. Do not set, modify, or
# delete clientExtensions if your account is associated with MT4.
#
self.clientExtensions = kwargs.get("clientExtensions")
#
# TakeProfitDetails specifies the details of a Take Profit Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Take Profit, or when a Trade's
# dependent Take Profit Order is modified directly through the Trade.
#
self.takeProfitOnFill = kwargs.get("takeProfitOnFill")
#
# StopLossDetails specifies the details of a Stop Loss Order to be
# created on behalf of a client. This may happen when an Order is
# filled that opens a Trade requiring a Stop Loss, or when a Trade's
# dependent Stop Loss Order is modified directly through the Trade.
#
self.stopLossOnFill = kwargs.get("stopLossOnFill")
#
# TrailingStopLossDetails specifies the details of a Trailing Stop Loss
# Order to be created on behalf of a client. This may happen when an
# Order is filled that opens a Trade requiring a Trailing Stop Loss, or
# when a Trade's dependent Trailing Stop Loss Order is modified
# directly through the Trade.
#
self.trailingStopLossOnFill = kwargs.get("trailingStopLossOnFill")
#
# Client Extensions to add to the Trade created when the Order is
# filled (if such a Trade is created). Do not set, modify, or delete
# tradeClientExtensions if your account is associated with MT4.
#
self.tradeClientExtensions = kwargs.get("tradeClientExtensions")
...
Similarly, it is straightforward to place market buy or sell orders with the create_order() method:
from v20.transaction import StopLossDetails, ClientExtensions
from v20.transaction import TrailingStopLossDetails, TakeProfitDetails
def create_order( instrument, units, price=None, sl_distance=None,
tsl_distance=None, tp_price=None, comment=None,
touch=False, suppress=False, ret=False
):
''' Places order with Oanda.
Parameters
==========
instrument: string
valid instrument name
units: int
number of units of instrument to be bought
(positive int, eg 'units=50')
or to be sold (negative int, eg 'units=-100')
price: float
limit order price, touch order price
sl_distance: float
stop loss distance price, mandatory eg in Germany
Specifies the distance (in price units) from the Trade’s
open price to use
as the Stop Loss Order price.
tsl_distance: float
trailing stop loss distance
The distance (in price units) from the Trade’s fill price that
the Trailing Stop Loss Order will be triggered at.
tp_price: float
take profit price to be used for the trade
The price that the Take Profit Order will be triggered at.
comment: str
string
A comment associated with the Order/Trade
touch: boolean
market_if_touched order (requires price to be set)
suppress: boolean
whether to suppress print out
ret: boolean
whether to return the order object
'''
client_ext = ClientExtensions( comment=comment )\
if comment is not None else None
sl_details = ( StopLossDetails( distance=sl_distance,
clientExtensions=client_ext
)
if sl_distance is not None else None
)
tsl_details = ( TrailingStopLossDetails( distance=tsl_distance,
clientExtensions=client_ext
)
if tsl_distance is not None else None
)
tp_details = ( TakeProfitDetails( price=tp_price,
clientExtensions=client_ext
)
if tp_price is not None else None
)
if price is None:
# https://github.com/oanda/v20-python/blob/master/src/v20/order.py
# def market( self, accountID, **kwargs )
# return self.create(
# accountID,
# order=MarketOrderRequest(**kwargs) ==>
# ) # ==> response=self.ctx.request(request) ==> return response
# https://github.com/oanda/v20-python/blob/f28192f4a31bce038cf6dfa302f5878bec192fe5/src/v20/order.py#L2185
# A MarketOrderRequest specifies the parameters that may be set
# when creating a Market Order.
# class MarketOrderRequest(BaseEntity):
# type : The type of the Order to Create. Must be set to "MARKET" when creating a Market Order.
# self.type = kwargs.get("type", "MARKET")
# The Market Order's Instrument
# self.instrument = kwargs.get("instrument")
# The quantity requested to be filled by the Market Order.
# A posititive number of units results in a long Order, and
# a negative number of units results in a short Order.
# self.units = kwargs.get("units")
# A MarketOrder is an order that is filled immediately upon creation
# using the current market price.
request = ctx.order.market( account_id,
instrument = instrument,
units = units,
stopLossOnFill = sl_details,
trailingStopLossOnFill = tsl_details,
takeProfitOnFill = tp_details,
type='MARKET'
)
elif touch:
# if MarketIfTouchedOrder is an order that is created
# with a price threshold (here is our touch order 'price'),
# and will only be filled by
# a market price that is touches or crossess the threshold
request = ctx.order.market_if_touched( account_id,
instrument=instrument,
price=price, # price threshold
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details,
type='MARKET_IF_TOUCHED'
)
else:
# A LimitOrder is an order that is created with a price threshold,
# and wll only be filled by a price that
# is equal to or better than the threshold.
request = ctx.order.limit( self.account_id,
instrument=instrument,
price=price,
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details,
type='LIMIT'
)
# First checking if the order is rejected
if 'orderRejectTransaction' in request.body:
order = request.get('orderRejectTransaction')
elif 'orderFillTransaction' in request.body:
order = request.get('orderFillTransaction')
elif 'orderCreateTransaction' in request.body:
order = request.get('orderCreateTransaction')
else:
# This case does not happen. But keeping this for completeness.
order = None
if not suppress and order is not None:
print('\n\n', order.dict(), '\n')
if ret is True:
return order.dict() if order is not None else None
Opens a long position via market order.
create_order( instrument, 1000 )
{'id': '130',
'time': '2022-08-22T15:08:25.049898265Z',
'userID': 23023947,
'accountID': '101-001-23023947-001',
'batchID': '129',
'requestID': '61010693239914447',
'type': 'ORDER_FILL',
'orderID': '129',
'instrument': 'EUR_USD',
'units': '1000.0',
'gainQuoteHomeConversionFactor': '1.0',
'lossQuoteHomeConversionFactor': '1.0',
'price': 0.99616,
'fullVWAP': 0.99616,
'fullPrice': {'type': 'PRICE',
'bids': [{'price': 0.99601, 'liquidity': '10000000'}],
'asks': [{'price': 0.99616, 'liquidity': '10000000'}],
'closeoutBid': 0.99601,
'closeoutAsk': 0.99616
},
'reason': 'MARKET_ORDER',
'pl': '0.0',
'financing': '0.0',
'commission': '0.0',
'guaranteedExecutionFee': '0.0',
'accountBalance': '99999.945',
'tradeOpened': {'tradeID': '130',
'units': '1000.0',
'price': 0.99616,
'guaranteedExecutionFee': '0.0',
'halfSpreadCost': '0.075',
'initialMarginRequired': '19.9216'
},
'halfSpreadCost': '0.075'
} https://www.oanda.com/demo-account/transaction-history/
Goes short after closing the long position via market order.
create_order( instrument, -1500 )
Closes the short position via market order.
create_order( instrument, 500 )
https://www.oanda.com/demo-account/transaction-history/
Although the Oanda API allows the placement of different order types, this chapter and the following chapter mainly focus on market orders to instantly go long or short whenever a new signal appears.
This section presents a custom class that automatically trades the EUR_USD instrument on the Oanda platform based on a momentum strategy. It is called MomentumTrader. The following walks through the class line by line, beginning with the 0 method. The class itself inherits from the tpqoa class:
The major method is the .on_success() method, which implements the trading logic for the momentum strategy:
import numpy as np
import pandas as pd
from v20.transaction import StopLossDetails, ClientExtensions
from v20.transaction import TrailingStopLossDetails, TakeProfitDetails
class MomentumTrader():
def __init__( self, instrument, bar_length, momentum,
units, *args, **kwargs
):
# Initial position value (market neutral).
self.position = 0
# Instrument to be traded.
self.instrument = instrument
# Length of the bar for the resampling of the tick data.
self.momentum = momentum
# Number of intervals for momentum calculation.
self.bar_length = bar_length
# Number of units to be traded.
self.units = units
# An empty DataFrame object to be filled with tick data.
self.raw_data = pd.DataFrame()
# The initial minimum bar length for the start of the trading itself
# the number of periods for our mean calculation.
self.min_length = self.momentum + 1
stop_stream = False
def stream_data(self, instrument, stop=None, ret=False, callback=None):
''' Starts a real-time data stream.
Parameters
==========
instrument: string
valid instrument name
stop : int
stops the streaming after a certain number of ticks retrieved.
'''
stream_instrument = instrument
self.ticks= 0
# https://github.com/oanda/v20-python/blob/master/src/v20/pricing.py
# accountID : Account Identifier
# instruments : List of Instruments to stream Prices for.
# snapshot: Flag that enables/disables the sending of a pricing snapshot
# when initially connecting to the stream.
response = ctx_stream.pricing.stream( account_id,
snapshot=True,
instruments=instrument
)
# https://github.com/oanda/v20-python/blob/master/src/v20/response.py
# response.parts() ==> call self.line_parser ==> class Parser() in the pricing.py
msgs = []
for msg_type, msg in response.parts():
msgs.append(msg)
# print(msg_type, msg) ###########
if msg_type == "pricing.PricingHeartbeat":
continue
elif msg_type == 'pricing.ClientPrice':
self.ticks += 1
time = msg.time
# along with list objects in the bids and asks properties.
# Typically, Level 1 quotes are available,
# so we read the first item of the each list.
# Each item in the list is a price_bucket object,
# from which we extract the bid and ask price.
if callback is not None:
callback( msg.instrument, msg.time,
float( msg.bids[0].dict()['price'] ),
float( msg.asks[0].dict()['price'] )
)
else:
# msg.time == msg['time'] or msg.bids == msg['bids']
self.on_success( msg.time,
float( msg.bids[0].dict()['price'] ),
float( msg.asks[0].dict()['price'] )
)
if stop is not None:
if self.ticks >= stop:
if ret: # if return msgs
return msgs
break
if stop_stream:
if ret:
return msgs
break
def create_order( self, instrument, units, price=None, sl_distance=None,
tsl_distance=None, tp_price=None, comment=None,
touch=False, suppress=False, ret=False
):
''' Places order with Oanda.
Parameters
==========
instrument: string
valid instrument name
units: int
number of units of instrument to be bought
(positive int, eg 'units=50')
or to be sold (negative int, eg 'units=-100')
price: float
limit order price, touch order price
sl_distance: float
stop loss distance price, mandatory eg in Germany
Specifies the distance (in price units) from the Trade’s
open price to use
as the Stop Loss Order price.
tsl_distance: float
trailing stop loss distance
The distance (in price units) from the Trade’s fill price that
the Trailing Stop Loss Order will be triggered at.
tp_price: float
take profit price to be used for the trade
The price that the Take Profit Order will be triggered at.
comment: str
string
A comment associated with the Order/Trade
touch: boolean
market_if_touched order (requires price to be set)
suppress: boolean
whether to suppress print out
ret: boolean
whether to return the order object
'''
client_ext = ClientExtensions( comment=comment )\
if comment is not None else None
sl_details = ( StopLossDetails( distance=sl_distance,
clientExtensions=client_ext
)
if sl_distance is not None else None
)
tsl_details = ( TrailingStopLossDetails( distance=tsl_distance,
clientExtensions=client_ext
)
if tsl_distance is not None else None
)
tp_details = ( TakeProfitDetails( price=tp_price,
clientExtensions=client_ext
)
if tp_price is not None else None
)
if price is None:
# https://github.com/oanda/v20-python/blob/master/src/v20/order.py
# def market( self, accountID, **kwargs )
# return self.create(
# accountID,
# order=MarketOrderRequest(**kwargs) ==>
# ) # ==> response=self.ctx.request(request) ==> return response
# https://github.com/oanda/v20-python/blob/f28192f4a31bce038cf6dfa302f5878bec192fe5/src/v20/order.py#L2185
# A MarketOrderRequest specifies the parameters that may be set
# when creating a Market Order.
# class MarketOrderRequest(BaseEntity):
# type : The type of the Order to Create. Must be set to "MARKET" when creating a Market Order.
# self.type = kwargs.get("type", "MARKET")
# The Market Order's Instrument
# self.instrument = kwargs.get("instrument")
# The quantity requested to be filled by the Market Order.
# A posititive number of units results in a long Order, and
# a negative number of units results in a short Order.
# self.units = kwargs.get("units")
# A MarketOrder is an order that is filled immediately upon creation
# using the current market price.
request = ctx.order.market( account_id,
instrument = instrument,
units = units,
stopLossOnFill = sl_details,
trailingStopLossOnFill = tsl_details,
takeProfitOnFill = tp_details,
type='MARKET'
)
elif touch:
# if MarketIfTouchedOrder is an order that is created
# with a price threshold (here is our touch order 'price'),
# and will only be filled by
# a market price that is touches or crossess the threshold
request = ctx.order.market_if_touched( account_id,
instrument=instrument,
price=price, # price threshold
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details,
type='MARKET_IF_TOUCHED'
)
else:
# A LimitOrder is an order that is created with a price threshold,
# and wll only be filled by a price that
# is equal to or better than the threshold.
request = ctx.order.limit( self.account_id,
instrument=instrument,
price=price,
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details,
type='LIMIT'
)
# First checking if the order is rejected
if 'orderRejectTransaction' in request.body:
order = request.get('orderRejectTransaction')
elif 'orderFillTransaction' in request.body:
order = request.get('orderFillTransaction')
elif 'orderCreateTransaction' in request.body:
order = request.get('orderCreateTransaction')
else:
# This case does not happen. But keeping this for completeness.
order = None
if not suppress and order is not None:
print('\n\n', order.dict(), '\n')
if ret is True:
return order.dict() if order is not None else None
# This method is called whenever new tick data arrives.
def on_success( self, time, bid, ask ):
'''Takes actions when new tick data arrives.'''
print(self.ticks, end=' ')
# The tick data is collected and stored.
self.raw_data = self.raw_data.append( pd.DataFrame({
'bid': bid,
'ask': ask,
},
index=[pd.Timestamp(time)]
)
)
# The tick data is then resampled to the appropriate bar length.
# ffill: propagate last valid observation forward to next valid
self.data = self.raw_data.resample( self.bar_length,
label='right'
).last().ffill().iloc[:-1]
self.data['mid'] = self.data.mean( axis=1 ) # mid = (bid + ask)/2
# Calculates the log returns based on the close values of the mid prices.
self.data['returns'] = np.log( self.data['mid'] /
self.data['mid'].shift(1)
)
# Momentum Strategy
# direction( Average log_return for Consecutive Periods ) as our prediction(go long/short)
# The signal (positioning) is derived based on the momentum parameter/attribute
# (via an online algorithm).
self.data['position']=np.sign( self.data['returns'].rolling( self.momentum ).mean() )
# When there is enough or new data, the trading logic is applied
# and the minimum length is increased by one every time.
if len(self.data) > self.min_length:
self.min_length += 1
# Checks whether the latest positioning (“signal”) is 1 (long)
if self.data['position'].iloc[-1] == 1: # go long
if self.position == 0: # if current position is flat
# Opens a long position via market order
self.create_order( self.instrument, self.units )
elif self.position == -1: # if current position is short
# Goes long after closing the short position via market order
self.create_order( self.instrument, self.units * 2)
# The market position self.position is set to +1 (long)
self.position = 1
# Checks whether the latest positioning (“signal”) is -1 (short)
elif self.data['position'].iloc[-1] == -1: # go short
if self.position == 0: # if current position is flat
# Opens a short position via market order
self.create_order( self.instrument, -self.units )
elif self.position == 1: # if current position is long
# Goes short after closing the long position via market order
self.create_order( self.instrument, -self.units * 2)
# The market position self.position is set to -1 (short)
self.position = -1
Based on this class, getting started with automated, algorithmic trading is just four lines of code. The Python code that follows initiates an automated trading session:
instrument = 'EUR_USD'
mt = MomentumTrader( instrument = instrument, # The instrument parameter is specified.
bar_length = '10s', # The bar_length parameter for the resampling is provided
momentum=6, # The momentum parameter is defined, which is applied to the resampled data intervals
units=10000 # The units parameter is set, which specifies the position size for long and short positions
)
mt.stream_data( mt.instrument, stop=500 )
https://www.oanda.com/demo-account/transaction-history/
https://www.oanda.com/demo-account/funding/
With regard to account information, transaction history, and the like, the Oanda RESTful API is also convenient to work with. For example, after the execution of the momentum strategy in the previous section, the algorithmic trader might want to inspect the current balance of the trading account. This is possible via the .get_account_summary() method:
def get_account_summary( detailed=False ):
''' Returns summary data for Oanda account.'''
if detailed is True:
response = ctx.account.get( account_id )
else:
response = ctx.account.summary( account_id )
raw = response.get('account')
return raw.dict()
get_account_summary()
{'id': '101-001-23023947-001', 'alias': 'Primary',
'currency': 'USD',
'balance': '99989.81',
'createdByUserID': 23023947,
'createdTime': '2022-08-17T10:25:25.251262166Z',
'guaranteedStopLossOrderMode': 'DISABLED',
'pl': '-10.1892', # Realized P&L
'resettablePL': '-10.1892',
'resettablePLTime': '0',
'financing': '-0.0008',
'commission': '0.0',
'guaranteedExecutionFees': '0.0',
'marginRate': '0.02',
'openTradeCount': 1,
'openPositionCount': 1,
'pendingOrderCount': 0,
'hedgingEnabled': False,
'unrealizedPL': '-6.6', # Unrealized P&L
'NAV': '99983.21',
'marginUsed': '198.424',
'marginAvailable': '99784.786',
'positionValue': '9921.2',
'marginCloseoutUnrealizedPL': '-5.8',
'marginCloseoutNAV': '99984.01',
'marginCloseoutMarginUsed': '198.424',
'marginCloseoutPercent': '0.00099',
'marginCloseoutPositionValue': '9921.2',
'withdrawalLimit': '99784.786',
'marginCallMarginUsed': '198.424',
'marginCallPercent': '0.00198',
'lastTransactionID': '150'}
oo = mt.create_order(instrument, units=-mt.position * mt.units,
ret=True, suppress=True
)
oo
Information about the last few trades is received with the .get_transactions() method: https://developer.oanda.com/rest-live-v20/transaction-ep/
def get_transactions(tid=0):
''' Retrieves and returns transactions data. '''
# id: The ID of the last Transaction fetched. [required]
# This query will return all Transactions newer than the TransactionID
response = ctx.transaction.since( account_id, id=tid )
transactions = response.get('transactions' )
transactions = [ t.dict()
for t in transactions
]
return transactions
get_transactions( tid=int( oo['id'] ) - 2 )
https://www.oanda.com/demo-account/transaction-history/
For a concise overview, there is also the .print_transactions() method available:
def print_transactions(tid=0):
''' Prints basic transactions data. '''
transactions = get_transactions(tid)
for trans in transactions:
try:
templ = '%4s | %s | %7s | %8s | %8s'
print(templ % ( trans['id'],
trans['time'][:-8],
trans['instrument'],
trans['units'],
trans['pl']
)
)
except Exception:
pass
print_transactions( tid=int( oo['id' ]) - 18 )
The Oanda platform allows for an easy and straightforward entry into the world of automated, algorithmic trading. Oanda specializes in so-called contracts for difference (CFDs). Depending on the country of residence of the trader, there is a great variety of instruments that can be traded.
A major advantage of Oanda from a technological point of view is the modern, powerful APIs that can be easily accessed via a dedicated Python wrapper package ( v20 ). This chapter shows how to set up an account, how to connect to the APIs with Python, how to retrieve historical data (one minute bars) for backtesting purposes, how to retrieve streaming data in real time, how to automatically trade a CFD based on a momentum strategy, and how to retrieve account information and the detailed transaction history.
tpqoa.py
# 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. The Python Quants GmbH 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.
#
# The tpqoa package is intended as a technological illustration only.
# It comes with no warranties or representations,
# to the extent permitted by applicable law.
#
import _thread
import configparser
import json
import signal
import threading
from time import sleep
import pandas as pd
import v20
from v20.transaction import StopLossDetails, ClientExtensions
from v20.transaction import TrailingStopLossDetails, TakeProfitDetails
MAX_REQUEST_COUNT = float(5000)
class Job(threading.Thread):
def __init__(self, job_callable, args=None):
threading.Thread.__init__(self)
self.callable = job_callable
self.args = args
# The shutdown_flag is a threading.Event object that
# indicates whether the thread should be terminated.
self.shutdown_flag = threading.Event()
self.job = None
self.exception = None
def run(self):
print('Thread #%s started' % self.ident)
try:
self.job = self.callable
while not self.shutdown_flag.is_set():
print("Starting job loop...")
if self.args is None:
self.job()
else:
self.job(self.args)
except Exception as e:
import sys
import traceback
print(traceback.format_exc())
self.exception = e
_thread.interrupt_main()
class ServiceExit(Exception):
"""
Custom exception which is used to trigger the clean exit
of all running threads and the main program.
"""
def __init__(self, message=None):
self.message = message
def __repr__(self):
return repr(self.message)
def service_shutdown(signum, frame):
print('exiting ...')
raise ServiceExit
class tpqoa(object):
''' tpqoa is a Python wrapper class for the Oanda v20 API. '''
def __init__(self, conf_file):
''' Init function is expecting a configuration file with
the following content:
[oanda]
account_id = XYZ-ABC-...
access_token = ZYXCAB...
account_type = practice (default) or live
Parameters
==========
conf_file: string
path to and filename of the configuration file,
e.g. '/home/me/oanda.cfg'
'''
self.config = configparser.ConfigParser()
self.config.read(conf_file)
self.access_token = self.config['oanda']['access_token']
self.account_id = self.config['oanda']['account_id']
self.account_type = self.config['oanda']['account_type']
if self.account_type == 'live':
self.hostname = 'api-fxtrade.oanda.com'
self.stream_hostname = 'stream-fxtrade.oanda.com'
else:
self.hostname = 'api-fxpractice.oanda.com'
self.stream_hostname = 'stream-fxpractice.oanda.com'
self.ctx = v20.Context(
hostname=self.hostname,
port=443,
token=self.access_token,
poll_timeout=10
)
self.ctx_stream = v20.Context(
hostname=self.stream_hostname,
port=443,
token=self.access_token,
)
self.suffix = '.000000000Z'
self.stop_stream = False
def get_instruments(self):
''' Retrieves and returns all instruments for the given account. '''
resp = self.ctx.account.instruments(self.account_id)
instruments = resp.get('instruments')
instruments = [ins.dict() for ins in instruments]
instruments = [(ins['displayName'], ins['name'])
for ins in instruments]
return sorted(instruments)
def get_prices(self, instrument):
''' Returns the current BID/ASK prices for instrument. '''
r = self.ctx.pricing.get(self.account_id, instruments=instrument)
r = json.loads(r.raw_body)
bid = float(r['prices'][0]['closeoutBid'])
ask = float(r['prices'][0]['closeoutAsk'])
return r['time'], bid, ask
def transform_datetime(self, dati):
''' Transforms Python datetime object to string. '''
if isinstance(dati, str):
dati = pd.Timestamp(dati).to_pydatetime()
return dati.isoformat('T') + self.suffix
def retrieve_data(self, instrument, start, end, granularity, price):
raw = self.ctx.instrument.candles(
instrument=instrument,
fromTime=start, toTime=end,
granularity=granularity, price=price)
raw = raw.get('candles')
raw = [cs.dict() for cs in raw]
if price == 'A':
for cs in raw:
cs.update(cs['ask'])
del cs['ask']
elif price == 'B':
for cs in raw:
cs.update(cs['bid'])
del cs['bid']
elif price == 'M':
for cs in raw:
cs.update(cs['mid'])
del cs['mid']
else:
raise ValueError("price must be either 'B', 'A' or 'M'.")
if len(raw) == 0:
return pd.DataFrame() # return empty DataFrame if no data
data = pd.DataFrame(raw)
data['time'] = pd.to_datetime(data['time'])
data = data.set_index('time')
data.index = pd.DatetimeIndex(data.index)
for col in list('ohlc'):
data[col] = data[col].astype(float)
return data
def get_history(self, instrument, start, end,
granularity, price, localize=True):
''' Retrieves historical data for instrument.
Parameters
==========
instrument: string
valid instrument name
start, end: datetime, str
Python datetime or string objects for start and end
granularity: string
a string like 'S5', 'M1' or 'D'
price: string
one of 'A' (ask), 'B' (bid) or 'M' (middle)
Returns
=======
data: pd.DataFrame
pandas DataFrame object with data
'''
if granularity.startswith('S') or granularity.startswith('M') \
or granularity.startswith('H'):
multiplier = float("".join(filter(str.isdigit, granularity)))
if granularity.startswith('S'):
# freq = '1h'
freq = f"{int(MAX_REQUEST_COUNT * multiplier / float(3600))}H"
else:
# freq = 'D'
freq = f"{int(MAX_REQUEST_COUNT * multiplier / float(1440))}D"
data = pd.DataFrame()
dr = pd.date_range(start, end, freq=freq)
for t in range(len(dr)):
batch_start = self.transform_datetime(dr[t])
if t != len(dr) - 1:
batch_end = self.transform_datetime(dr[t + 1])
else:
batch_end = self.transform_datetime(end)
batch = self.retrieve_data(instrument, batch_start, batch_end,
granularity, price)
data = pd.concat([data, batch])
else:
start = self.transform_datetime(start)
end = self.transform_datetime(end)
data = self.retrieve_data(instrument, start, end,
granularity, price)
if localize:
data.index = data.index.tz_localize(None)
return data[['o', 'h', 'l', 'c', 'volume', 'complete']]
def create_order(self, instrument, units, price=None, sl_distance=None,
tsl_distance=None, tp_price=None, comment=None,
touch=False, suppress=False, ret=False):
''' Places order with Oanda.
Parameters
==========
instrument: string
valid instrument name
units: int
number of units of instrument to be bought
(positive int, eg 'units=50')
or to be sold (negative int, eg 'units=-100')
price: float
limit order price, touch order price
sl_distance: float
stop loss distance price, mandatory eg in Germany
tsl_distance: float
trailing stop loss distance
tp_price: float
take profit price to be used for the trade
comment: str
string
touch: boolean
market_if_touched order (requires price to be set)
suppress: boolean
whether to suppress print out
ret: boolean
whether to return the order object
'''
client_ext = ClientExtensions(
comment=comment) if comment is not None else None
sl_details = (StopLossDetails(distance=sl_distance,
clientExtensions=client_ext)
if sl_distance is not None else None)
tsl_details = (TrailingStopLossDetails(distance=tsl_distance,
clientExtensions=client_ext)
if tsl_distance is not None else None)
tp_details = (TakeProfitDetails(
price=tp_price, clientExtensions=client_ext)
if tp_price is not None else None)
if price is None:
request = self.ctx.order.market(
self.account_id,
instrument=instrument,
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details,
)
elif touch:
request = self.ctx.order.market_if_touched(
self.account_id,
instrument=instrument,
price=price,
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details
)
else:
request = self.ctx.order.limit(
self.account_id,
instrument=instrument,
price=price,
units=units,
stopLossOnFill=sl_details,
trailingStopLossOnFill=tsl_details,
takeProfitOnFill=tp_details
)
# First checking if the order is rejected
if 'orderRejectTransaction' in request.body:
order = request.get('orderRejectTransaction')
elif 'orderFillTransaction' in request.body:
order = request.get('orderFillTransaction')
elif 'orderCreateTransaction' in request.body:
order = request.get('orderCreateTransaction')
else:
# This case does not happen. But keeping this for completeness.
order = None
if not suppress and order is not None:
print('\n\n', order.dict(), '\n')
if ret is True:
return order.dict() if order is not None else None
def stream_data(self, instrument, stop=None, ret=False, callback=None):
''' Starts a real-time data stream.
Parameters
==========
instrument: string
valid instrument name
'''
self.stream_instrument = instrument
self.ticks = 0
response = self.ctx_stream.pricing.stream(
self.account_id, snapshot=True,
instruments=instrument)
msgs = []
for msg_type, msg in response.parts():
msgs.append(msg)
# print(msg_type, msg)
if msg_type == 'pricing.ClientPrice':
self.ticks += 1
self.time = msg.time
if callback is not None:
callback(msg.instrument, msg.time,
float(msg.bids[0].dict()['price']),
float(msg.asks[0].dict()['price']))
else:
self.on_success(msg.time,
float(msg.bids[0].dict()['price']),
float(msg.asks[0].dict()['price']))
if stop is not None:
if self.ticks >= stop:
if ret:
return msgs
break
if self.stop_stream:
if ret:
return msgs
break
def _stream_data_failsafe_thread(self, args):
try:
print("Starting price streaming")
self.stream_data(args[0], callback=args[1])
except Exception as e:
import sys
import traceback
print(traceback.format_exc())
sleep(3)
return
def stream_data_failsafe(self, instrument, callback=None):
signal.signal(signal.SIGTERM, service_shutdown)
signal.signal(signal.SIGINT, service_shutdown)
signal.signal(signal.SIGSEGV, service_shutdown)
try:
price_stream_thread = Job(self._stream_data_failsafe_thread,
[instrument, callback])
price_stream_thread.start()
return price_stream_thread
except ServiceExit as e:
print('Handling exception')
import sys
import traceback
print(traceback)
price_stream_thread.shutdown_flag.set()
price_stream_thread.join()
def on_success(self, time, bid, ask):
''' Method called when new data is retrieved. '''
print(time, bid, ask)
def get_account_summary(self, detailed=False):
''' Returns summary data for Oanda account.'''
if detailed is True:
response = self.ctx.account.get(self.account_id)
else:
response = self.ctx.account.summary(self.account_id)
raw = response.get('account')
return raw.dict()
def get_transaction(self, tid=0):
''' Retrieves and returns transaction data. '''
response = self.ctx.transaction.get(self.account_id, tid)
transaction = response.get('transaction')
return transaction.dict()
def get_transactions(self, tid=0):
''' Retrieves and returns transactions data. '''
response = self.ctx.transaction.since(self.account_id, id=tid)
transactions = response.get('transactions')
transactions = [t.dict() for t in transactions]
return transactions
def print_transactions(self, tid=0):
''' Prints basic transactions data. '''
transactions = self.get_transactions(tid)
for trans in transactions:
try:
templ = '%4s | %s | %7s | %8s | %8s'
print(templ % (trans['id'],
trans['time'][:-8],
trans['instrument'],
trans['units'],
trans['pl']))
except Exception:
pass
def get_positions(self):
''' Retrieves and returns positions data. '''
response = self.ctx.position.list_open(self.account_id).body
positions = [p.dict() for p in response.get('positions')]
return positions