pa8_Contract for Difference CFD Trading_Oanda_git_log return_Momentum Strategy_Leverage_margin_tpqoa

     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:

  • Instruments
    • The first criterion that comes to mind is the type of instrument one is interested in to trade. For example, one might be interested in trading stocks, exchange traded funds (ETFs), bonds, currencies, commodities, options, or futures.
  • Strategies
    • Some traders are interested in long-only strategies, while others require short selling as well. Some focus on single-instrument strategies, while others focus on those involving multiple instruments at the same time.
  • Costs
    • Fixed and variable transaction costs are an important factor for many traders. They might even decide whether a certain strategy is profitable or not (see, for instance, Chapters 4 and 6).
  • Technology
    • Technology has become an important factor in the selection of trading platforms. First, there are the tools that the platforms offer to traders. Trading tools are available, in general, for desktop/notebook computers, tablets, and smart phones. Second, there are the application programming interfaces (APIs) that can be accessed programmatically by traders.
  • Jurisdiction
    • Financial trading is a heavily regulated field严格监管的领域 with different legal frameworks in place for different countries or regions. This might prohibit certain traders from using certain platforms and/or financial instruments depending on their residence. 

     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: 

  • Instruments
    • Oanda offers a wide range of so-called contracts for diference (CFD) products (see also “Contracts for Difference (CFDs)” on page 225 and “Disclaimer” on page 249). Main characteristics of CFDs are that they are leveraged (for example, 10:1 or 50:1) and traded on margin以保证金交易 such that losses might exceed the initial capital.
  • Strategies
    • Oanda allows both to go long (buy) and to go short (sell) CFDs. Different order types are available, such as market or limit orders, with or without profit targets and/or (trailing) stop losses.
  • Costs
    • There are no fixed transaction costs associated with the trading of CFDs at Oanda. However, there is a bid-ask spread买卖差价 that leads to variable transaction costs when trading CFDs.
  • Technology
    • ​Oanda provides the trading application fxTrade (Practice), which retrieves data in real time and allows the (manual, discretionary/ dɪˈskreʃəneri/全权委托 ) trading of all instruments (see Figure 8-1).

       


      Figure 8-1. Oanda trading application fxTrade Practice

      There is also a browser-based trading application available (see Figure 8-2).

      Figure 8-2. Oanda browser-based trading application
      A major strength of the platform are the RESTful and streaming APIs (see Oanda v20 API) via which traders
      can programmatically access historical and streaming data,
      place buy and sell orders,
      or retrieve account information.
      A Python wrapper package is available (see v20 on PyPi). Oanda offers free paper trading accounts模拟交易账户
      (
           Paper trading is simulated trading that allows investors to practice buying and selling securities. Paper trading can test a new investment strategy before employing it in a live account. Many online brokers offer clients paper trade accounts.
      )
      that provide full access to all technological capabilities, which is really helpful in getting started on the platform. This also simplifies the transitioning from paper to live trading.

  • Jurisdiction
    • Depending on the residence of the account holder, the selection of CFDs that can be traded changes. FX-related CFDs与外汇相关的差价合约 are available basically everywhere Oanda is active. CFDs on stock indices, for instance, might not be available in certain jurisdictions.

#################################### 

Contracts for Diference (CFDs)

     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.

  • “Setting Up an Account” on page 227 briefly discusses how to set up an account.
  • “The Oanda API” on page 229 illustrates the necessary steps to access the API. Based on the API access,
  • “Retrieving Historical Data” on page 230 retrieves and works with historical data for a certain CFD.
  • “Working with Streaming Data” on page 236 introduces the streaming API of Oanda for data retrieval and visualization.
  • “Implementing Trading Strategies in Real Time” on page 239 implements an automated, algorithmic trading strategy in real time.
  • Finally, “Retrieving Account Information” on page 244 deals with retrieving data about the account itself, such as the current balance or recent trades. Throughout, the code makes use of a Python wrapper class called tpqoa (see GitHub repository). 

     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. 

Setting Up an Account in Oanda

     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

The Oanda API

     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

right click Git CMD ==>

 ...

Next, use the following command:https://github.com/yhilpisch/tpqoa

pip install git+https://github.com/yhilpisch/tpqoa.git

Retrieving Historical Data

     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.

Looking Up Instruments Available for Trading

     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]

 

Backtesting a Momentum Strategy on Minute Bars 

     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)


#######################

retrieve the raw data from Oanda

 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()

Why log return

     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()

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()

vectorized backtesting_Momentum

     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.

  • Consecutive periods of positive momentum values indicate an uptrend;
  • conversely, if momentum is consecutively negative, that indicates a downtrend.

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:

  • : Price at time period i
  • : Number of prices added together or the number of time periods

Find a moving average for log_return:

  • Consecutive positive signs( np.sign()= 1 ) indicate uptrend(direction),
  • and successive negative signs( np.sign()= -1 ) indicate downtrend(direction)
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)

Factoring In Leverage and Margin 

     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,

  • one unit means 1 USD, like for currency-related CFDs与货币相关的差价合约.
  • For others, like for index-related CFDs与指数相关的差价合约 (for example, DE30_EUR ), one unit means a currency exposure at the (bid/ask) price of the CFD (for example, 11,750 EUR).)

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%.

  • If the account the algorithmic trader is trading with has less than 200 EUR left in equity/cash, the position needs to be closed out since the (regulatory) margin requirements cannot be met anymore.
  • If losses eat up the margin completely, additional funds need to be allocated as margin to keep the trade alive.(The simplified calculations neglect忽略, for example, financing costs融资成本 that might become due for leveraged trading.)

     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 margin10,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. 

Margin Used Calculation Example:

     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.

Margin Closeout Value Calculation Example

     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 rateopen 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.

ILLUSTRATING TOTAL P&L CALCULATIONS 

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

     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)

Placing Market Orders

     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  


  • ClientExtensions is an application/json object with the following Schema:
    {
        # 
        # 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)
    }

  • StopLossDetails is an application/json object with the following Schema:
    {
        # 
        # 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)
    }

  • TrailingStopLossDetails is an application/json object with the following Schema:
    {
        # 
        # 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)
    }

  • TakeProfitDetails is an application/json object with the following Schema:
    {
        # 
        # 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)
    }
  • https://developer.oanda.com/rest-live-v20/order-df/
  • v20-python/order.py at f28192f4a31bce038cf6dfa302f5878bec192fe5 · oanda/v20-python · GitHub
  • A MarketOrder is an order that is filled immediately upon creation using the current market price
        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")
  • if MarketIfTouchedOrder is an order that is created with a price threshold, and will only be filled by a market price that is touches or crossess the threshold
        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")
  • 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 thant the threshold
        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. 

Implementing Trading Strategies in Real Time

     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/ 

 

 

Retrieving Account Information

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 )  

 ORDER_FILL

Conclusions 

     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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340