├── .github ├── FUNDING.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── config ├── config.ini └── write_config.py ├── data ├── order_response.jsonc ├── order_response_sub_classes.jsonc ├── orders.json └── sample_oco.jsonc ├── docs ├── algo_trading.md ├── portfolio_object.md ├── strategies │ └── Strategy 1 - Golden Crossover.md └── trade_object.md ├── pyrobot ├── indicators.py ├── order_status.py ├── portfolio.py ├── robot.py ├── stock_frame.py └── trades.py ├── samples ├── responses │ ├── orders.jsonc │ ├── quotes-small.jsonc │ └── quotes.jsonc ├── trading_robot.py ├── trading_robot_indicators.py └── trading_robot_indicators_compare.py ├── setup.py └── tests ├── test_indicators.py ├── test_portfolio.py ├── test_robot.py ├── test_stock_frame.py └── test_trades.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [areed1192] 4 | patreon: sigmacoding 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 16 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | /configs 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | pyrobot/config.py 132 | tests/config.py 133 | .vscode/settings.json 134 | indicators/ 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Reed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Trading Robot 2 | 3 | ## Table of Contents 4 | 5 | - [Overview](#overview) 6 | - [Setup](#setup) 7 | - [Usage](#usage) 8 | - [Support These Projects](#support-these-projects) 9 | 10 | ## Overview 11 | 12 | Current Version: **0.1.1** 13 | 14 | A trading robot written in Python that can run automated strategies using a technical analysis. 15 | The robot is designed to mimic a few common scenarios: 16 | 17 | 1. Maintaining a portfolio of multiple instruments. The `Portfolio` object will be able 18 | to calculate common risk metrics related to a portfolio and give real-time feedback 19 | as you trade. 20 | 21 | 2. Define an order that can be used to trade a financial instrument. With the `Trade` object, 22 | you can define simple or even complex orders using Python. These orders will also help similify 23 | common scenarios like defining both a take profit and stop loss at the same time. 24 | 25 | 3. A real-time data table that includes both historical and real-time prices as they change. The 26 | `StockFrame` will make the process of storing your data easy and quick. Additionally, it will be 27 | setup so that way you can easily select your financial data as it comes in and do further analysis 28 | if needed. 29 | 30 | 4. Define and calculate indicators using both historical and real-time prices. The `Indicator` object 31 | will help you easily define the input of your indicators, calculate them, and then update their values 32 | as new prices come. 33 | 34 | ## Setup 35 | 36 | **Setup - Local Install:** 37 | 38 | If you are planning to make modifications to this project or you would like to access it 39 | before it has been indexed on `PyPi`. I would recommend you either install this project 40 | in `editable` mode or do a `local install`. For those of you, who want to make modifications 41 | to this project. I would recommend you install the library in `editable` mode. 42 | 43 | If you want to install the library in `editable` mode, make sure to run the `setup.py` 44 | file, so you can install any dependencies you may need. To run the `setup.py` file, 45 | run the following command in your terminal. 46 | 47 | ```console 48 | pip install -e . 49 | ``` 50 | 51 | If you don't plan to make any modifications to the project but still want to use it across 52 | your different projects, then do a local install. 53 | 54 | ```console 55 | pip install . 56 | ``` 57 | 58 | This will install all the dependencies listed in the `setup.py` file. Once done 59 | you can use the library wherever you want. 60 | 61 | **Setup - PyPi Install:** 62 | 63 | The project can be found at PyPI, if you'd like to view the project please use this 64 | [link](https://pypi.org/project/python-trading-robot/). To **install** the library, 65 | run the following command from the terminal. 66 | 67 | ```bash 68 | pip install python-trading-robot 69 | ``` 70 | 71 | **Setup - PyPi Upgrade:** 72 | 73 | To **upgrade** the library, run the following command from the terminal. 74 | 75 | ```bash 76 | pip install --upgrade python-trading-robot 77 | ``` 78 | 79 | ## Usage 80 | 81 | To run the robot, you will need to provide a few pieces of information from your TD Ameritrade Developer account. 82 | The following items are need for authentication: 83 | 84 | - Client ID: Also, called your consumer key, this was provided when you registered an app with the TD Ameritrade 85 | Developer platform. An example of a client ID could look like the following `MMMMYYYYYA6444VXXXXBBJC3DOOOO`. 86 | 87 | - Redirect URI: Also called the callbakc URL or redirect URL, this was specified by you when you regiestered your app with 88 | the TD Ameritrade Developer platform. Here is an example of a redirect URI 89 | 90 | - Credentials Path: This is a file path that will point to a JSON file where your state info will be saved. Keep in mind 91 | that it is okay if it points to a non-existing file as once you run the script the file will be auto generated. For example, 92 | if I want my state info to be saved to my desktop, then it would look like the following: `C:\Users\Desktop\ts_state.json` 93 | 94 | Once you've identfied those pieces of info, you can run the robot. Here is a simple example that will create a new instance 95 | of it: 96 | 97 | ```python 98 | from pyrobot.robot import PyRobot 99 | 100 | # Initialize the robot 101 | trading_robot = PyRobot( 102 | client_id='XXXXXX111111YYYY22', 103 | redirect_uri='https://localhost/mycallback', 104 | credentials_path='path/to/td_state.json' 105 | ) 106 | ``` 107 | 108 | For more detailed examples, go to the `trading_robot.py` file to see an example of how to use the library along with all 109 | the different objects inside. 110 | 111 | ## Support these Projects 112 | 113 | **Patreon:** 114 | Help support this project and future projects by donating to my [Patreon Page](https://www.patreon.com/sigmacoding). I'm always 115 | looking to add more content for individuals like yourself, unfortuantely some of the APIs I would require me to pay monthly fees. 116 | 117 | **YouTube:** 118 | If you'd like to watch more of my content, feel free to visit my YouTube channel [Sigma Coding](https://www.youtube.com/c/SigmaCoding). 119 | 120 | 122 | -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | client_id = FAKE_CLIENT_ID 3 | redirect_uri = FAKE_REDIRECT_URL 4 | json_path = FAKE_PATH 5 | account_number = FAKE_ACCT 6 | -------------------------------------------------------------------------------- /config/write_config.py: -------------------------------------------------------------------------------- 1 | # python 3.x 2 | from configparser import ConfigParser 3 | 4 | config = ConfigParser() 5 | 6 | config.add_section('main') 7 | config.set('main', 'CLIENT_ID', 'FAKE_CLIENT_ID') 8 | config.set('main', 'REDIRECT_URI', 'FAKE_REDIRECT_URL') 9 | config.set('main', 'JSON_PATH', 'FAKE_PATH') 10 | config.set('main', 'ACCOUNT_NUMBER', 'FAKE_ACCT') 11 | 12 | with open(file='config/config.ini', mode='w') as f: 13 | config.write(f) -------------------------------------------------------------------------------- /data/order_response.jsonc: -------------------------------------------------------------------------------- 1 | //OrderGet: 2 | { 3 | "session": "'NORMAL' or 'AM' or 'PM' or 'SEAMLESS'", 4 | "duration": "'DAY' or 'GOOD_TILL_CANCEL' or 'FILL_OR_KILL'", 5 | "orderType": "'MARKET' or 'LIMIT' or 'STOP' or 'STOP_LIMIT' or 'TRAILING_STOP' or 'MARKET_ON_CLOSE' or 'EXERCISE' or 'TRAILING_STOP_LIMIT' or 'NET_DEBIT' or 'NET_CREDIT' or 'NET_ZERO'", 6 | "cancelTime": { 7 | "date": "string", 8 | "shortFormat": false 9 | }, 10 | "complexOrderStrategyType": "'NONE' or 'COVERED' or 'VERTICAL' or 'BACK_RATIO' or 'CALENDAR' or 'DIAGONAL' or 'STRADDLE' or 'STRANGLE' or 'COLLAR_SYNTHETIC' or 'BUTTERFLY' or 'CONDOR' or 'IRON_CONDOR' or 'VERTICAL_ROLL' or 'COLLAR_WITH_STOCK' or 'DOUBLE_DIAGONAL' or 'UNBALANCED_BUTTERFLY' or 'UNBALANCED_CONDOR' or 'UNBALANCED_IRON_CONDOR' or 'UNBALANCED_VERTICAL_ROLL' or 'CUSTOM'", 11 | "quantity": 0, 12 | "filledQuantity": 0, 13 | "remainingQuantity": 0, 14 | "requestedDestination": "'INET' or 'ECN_ARCA' or 'CBOE' or 'AMEX' or 'PHLX' or 'ISE' or 'BOX' or 'NYSE' or 'NASDAQ' or 'BATS' or 'C2' or 'AUTO'", 15 | "destinationLinkName": "string", 16 | "releaseTime": "string", 17 | "stopPrice": 0, 18 | "stopPriceLinkBasis": "'MANUAL' or 'BASE' or 'TRIGGER' or 'LAST' or 'BID' or 'ASK' or 'ASK_BID' or 'MARK' or 'AVERAGE'", 19 | "stopPriceLinkType": "'VALUE' or 'PERCENT' or 'TICK'", 20 | "stopPriceOffset": 0, 21 | "stopType": "'STANDARD' or 'BID' or 'ASK' or 'LAST' or 'MARK'", 22 | "priceLinkBasis": "'MANUAL' or 'BASE' or 'TRIGGER' or 'LAST' or 'BID' or 'ASK' or 'ASK_BID' or 'MARK' or 'AVERAGE'", 23 | "priceLinkType": "'VALUE' or 'PERCENT' or 'TICK'", 24 | "price": 0, 25 | "taxLotMethod": "'FIFO' or 'LIFO' or 'HIGH_COST' or 'LOW_COST' or 'AVERAGE_COST' or 'SPECIFIC_LOT'", 26 | "orderLegCollection": [ 27 | { 28 | "orderLegType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 29 | "legId": 0, 30 | "instrument": "The type has the following subclasses [Equity, FixedIncome, MutualFund, CashEquivalent, Option] descriptions are listed below\"", 31 | "instruction": "'BUY' or 'SELL' or 'BUY_TO_COVER' or 'SELL_SHORT' or 'BUY_TO_OPEN' or 'BUY_TO_CLOSE' or 'SELL_TO_OPEN' or 'SELL_TO_CLOSE' or 'EXCHANGE'", 32 | "positionEffect": "'OPENING' or 'CLOSING' or 'AUTOMATIC'", 33 | "quantity": 0, 34 | "quantityType": "'ALL_SHARES' or 'DOLLARS' or 'SHARES'" 35 | } 36 | ], 37 | "activationPrice": 0, 38 | "specialInstruction": "'ALL_OR_NONE' or 'DO_NOT_REDUCE' or 'ALL_OR_NONE_DO_NOT_REDUCE'", 39 | "orderStrategyType": "'SINGLE' or 'OCO' or 'TRIGGER'", 40 | "orderId": 0, 41 | "cancelable": false, 42 | "editable": false, 43 | "status": "'AWAITING_PARENT_ORDER' or 'AWAITING_CONDITION' or 'AWAITING_MANUAL_REVIEW' or 'ACCEPTED' or 'AWAITING_UR_OUT' or 'PENDING_ACTIVATION' or 'QUEUED' or 'WORKING' or 'REJECTED' or 'PENDING_CANCEL' or 'CANCELED' or 'PENDING_REPLACE' or 'REPLACED' or 'FILLED' or 'EXPIRED'", 44 | "enteredTime": "string", 45 | "closeTime": "string", 46 | "tag": "string", 47 | "accountId": 0, 48 | "orderActivityCollection": [ 49 | "The type has the following subclasses [Execution] descriptions are listed below" 50 | ], 51 | "replacingOrderCollection": [ 52 | {} 53 | ], 54 | "childOrderStrategies": [ 55 | {} 56 | ], 57 | "statusDescription": "string" 58 | } 59 | -------------------------------------------------------------------------------- /data/order_response_sub_classes.jsonc: -------------------------------------------------------------------------------- 1 | //The class has the 2 | //following subclasses: 3 | //-Equity 4 | //-FixedIncome 5 | //-MutualFund 6 | //-CashEquivalent 7 | //-Option 8 | //-OrderActivity 9 | 10 | { 11 | "equity": { 12 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 13 | "cusip": "string", 14 | "symbol": "string", 15 | "description": "string" 16 | }, 17 | "fixed_income": { 18 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 19 | "cusip": "string", 20 | "symbol": "string", 21 | "description": "string", 22 | "maturityDate": "string", 23 | "variableRate": 0, 24 | "factor": 0 25 | }, 26 | "mutual_fund": { 27 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 28 | "cusip": "string", 29 | "symbol": "string", 30 | "description": "string", 31 | "type": "'NOT_APPLICABLE' or 'OPEN_END_NON_TAXABLE' or 'OPEN_END_TAXABLE' or 'NO_LOAD_NON_TAXABLE' or 'NO_LOAD_TAXABLE'" 32 | }, 33 | "cash_equivalent": { 34 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 35 | "cusip": "string", 36 | "symbol": "string", 37 | "description": "string", 38 | "type": "'SAVINGS' or 'MONEY_MARKET_FUND'" 39 | }, 40 | "option": { 41 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'", 42 | "cusip": "string", 43 | "symbol": "string", 44 | "description": "string", 45 | "type": "'VANILLA' or 'BINARY' or 'BARRIER'", 46 | "putCall": "'PUT' or 'CALL'", 47 | "underlyingSymbol": "string", 48 | "optionMultiplier": 0, 49 | "optionDeliverables": [ 50 | { 51 | "symbol": "string", 52 | "deliverableUnits": 0, 53 | "currencyType": "'USD' or 'CAD' or 'EUR' or 'JPY'", 54 | "assetType": "'EQUITY' or 'OPTION' or 'INDEX' or 'MUTUAL_FUND' or 'CASH_EQUIVALENT' or 'FIXED_INCOME' or 'CURRENCY'" 55 | } 56 | ] 57 | }, 58 | "order_acitivity": { 59 | "activityType": "'EXECUTION' or 'ORDER_ACTION'", 60 | "executionType": "'FILL'", 61 | "quantity": 0, 62 | "orderRemainingQuantity": 0, 63 | "executionLegs": [ 64 | { 65 | "legId": 0, 66 | "quantity": 0, 67 | "mismarkedQuantity": 0, 68 | "price": 0, 69 | "time": "string" 70 | } 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /data/sample_oco.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "childOrderStrategies": [ 3 | { 4 | "childOrderStrategies": [ 5 | { 6 | "duration": "DAY", 7 | "orderLegCollection": [ 8 | { 9 | "instruction": "SELL", 10 | "instrument": { "assetType": "EQUITY", "symbol": "MSFT" }, 11 | "quantity": 1 12 | } 13 | ], 14 | "orderStrategyType": "SINGLE", 15 | "orderType": "LIMIT", 16 | "price": 218.4, 17 | "session": "NORMAL" 18 | }, 19 | { 20 | "duration": "DAY", 21 | "orderLegCollection": [ 22 | { 23 | "instruction": "SELL", 24 | "instrument": { "assetType": "EQUITY", "symbol": "MSFT" }, 25 | "quantity": 1 26 | } 27 | ], 28 | "orderStrategyType": "SINGLE", 29 | "orderType": "STOP", 30 | "session": "NORMAL", 31 | "stopPrice": 217.6 32 | } 33 | ], 34 | "orderStrategyType": "OCO" 35 | } 36 | ], 37 | "duration": "DAY", 38 | "orderLegCollection": [ 39 | { 40 | "instruction": "BUY", 41 | "instrument": { "assetType": "EQUITY", "symbol": "MSFT" }, 42 | "quantity": 1 43 | } 44 | ], 45 | "orderStrategyType": "TRIGGER", 46 | "orderType": "MARKET", 47 | "session": "NORMAL" 48 | } 49 | -------------------------------------------------------------------------------- /docs/algo_trading.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areed1192/python-trading-robot/abe7ac9189873f376e6fd968f57f403a8ffd45d2/docs/algo_trading.md -------------------------------------------------------------------------------- /docs/portfolio_object.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areed1192/python-trading-robot/abe7ac9189873f376e6fd968f57f403a8ffd45d2/docs/portfolio_object.md -------------------------------------------------------------------------------- /docs/strategies/Strategy 1 - Golden Crossover.md: -------------------------------------------------------------------------------- 1 | # Trading Strategies - Crossover 2 | 3 | ## Overview 4 | 5 | The Golden Cross is a bullish phenomenon when the 50-day moving average crosses 6 | above the 200-day moving average. When the market is in a long-term downtrend, 7 | the 50-day moving average is below the 200-day moving average.However, no downtrend 8 | lasts forever. So, when a new uptrend begins, the 50-day moving average must cross 9 | above the 200-day moving average — and that’s known as the Golden Cross. 10 | 11 | Because the concept is what matters (which is the short-term trend showing signs of 12 | strength against the long-term downtrend). The moving average is only a tool to 13 | define the trend. 14 | 15 | ## Indicators 16 | 17 | 1. 50-Day SMA (Simple Moving Average), represents the short-term trend. 18 | 2. 200-Day SMA (Simple Moving Average), represents the long-term trend. 19 | 20 | ## Buy Signal & Sell Signal 21 | 22 | 1. Buy Signal, the 50-Day Average Crosses **ABOVE** the 200-Day Moving Average, 23 | indicating that there may be a bull-market on the horizon. 24 | 2. Sell Signal, the 50-Day Average Crosses **BELOW** the 200-Day Moving Average, 25 | indicating that there may be a bear-market on the horizon. 26 | 27 | When the 50-Day SMA crosses **ABOVE** the 200-Day SMA we call it a **Golden Crossover**, 28 | and when the 50-Day SMA crosses **BELOW** the 200-Day SMA we call it a **Death Cross**. 29 | 30 | ## How to use the Golden Crossover 31 | 32 | There are three stages to a golden cross. 33 | 34 | - The **first stage** requires that a downtrend eventually bottoms out as selling is depleted. 35 | - In the **second stage**, the shorter moving average forms a crossover up through the larger 36 | moving average to trigger a breakout and confirmation of trend reversal. 37 | - The **last stage** is the continuing uptrend for the follow through to higher prices. The 38 | moving averages act as support levels on pullbacks, until they crossover back down at which 39 | point a death cross may form. The death cross is the opposite of the golden cross as the shorter 40 | moving average forms a crossover down through the longer moving average. 41 | 42 | The most commonly used moving averages are the 50-period and the 200-period moving average. The period 43 | represents a specific time increment. **Generally, larger time periods tend to form stronger lasting breakouts.** 44 | For example, the daily 50-day moving average crossover up through the 200-day moving average on an index like 45 | the S&P 500 is one of the most popular bullish market signals. With a bellwether index, the motto "A rising 46 | tide lifts all boats" applies when a golden cross forms as the buying resonates throughout the index components 47 | and sectors. 48 | 49 | ## Python Code 50 | 51 | ```python 52 | # Create an indicator Object. 53 | indicator_client = Indicators(price_data_frame=stock_frame) 54 | 55 | # Add the 200-Day simple moving average. 56 | indicator_client.sma(period=200) 57 | 58 | # Add the 50-Day simple moving average. 59 | indicator_client.sma(period=200) 60 | 61 | # Add a signal to check for. 62 | indicator_client.set_indicator_signal_compare( 63 | indicator_1='sma', 64 | indicator_2='sma', 65 | condition_buy=operator.ge, 66 | condition_sell=operator.le, 67 | ) 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/trade_object.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areed1192/python-trading-robot/abe7ac9189873f376e6fd968f57f403a8ffd45d2/docs/trade_object.md -------------------------------------------------------------------------------- /pyrobot/indicators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | from typing import Any 5 | from typing import Dict 6 | from typing import Union 7 | 8 | from pyrobot.stock_frame import StockFrame 9 | 10 | class Indicators(): 11 | 12 | """ 13 | Represents an Indicator Object which can be used 14 | to easily add technical indicators to a StockFrame. 15 | """ 16 | 17 | def __init__(self, price_data_frame: StockFrame) -> None: 18 | """Initalizes the Indicator Client. 19 | 20 | Arguments: 21 | ---- 22 | price_data_frame {pyrobot.StockFrame} -- The price data frame which is used to add indicators to. 23 | At a minimum this data frame must have the following columns: `['timestamp','close','open','high','low']`. 24 | 25 | Usage: 26 | ---- 27 | >>> historical_prices_df = trading_robot.grab_historical_prices( 28 | start=start_date, 29 | end=end_date, 30 | bar_size=1, 31 | bar_type='minute' 32 | ) 33 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 34 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 35 | >>> indicator_client.price_data_frame 36 | """ 37 | 38 | self._stock_frame: StockFrame = price_data_frame 39 | self._price_groups = price_data_frame.symbol_groups 40 | self._current_indicators = {} 41 | self._indicator_signals = {} 42 | self._frame = self._stock_frame.frame 43 | 44 | self._indicators_comp_key = [] 45 | self._indicators_key = [] 46 | 47 | if self.is_multi_index: 48 | True 49 | 50 | def get_indicator_signal(self, indicator: str= None) -> Dict: 51 | """Return the raw Pandas Dataframe Object. 52 | 53 | Arguments: 54 | ---- 55 | indicator {Optional[str]} -- The indicator key, for example `ema` or `sma`. 56 | 57 | Returns: 58 | ---- 59 | {dict} -- Either all of the indicators or the specified indicator. 60 | """ 61 | 62 | if indicator and indicator in self._indicator_signals: 63 | return self._indicator_signals[indicator] 64 | else: 65 | return self._indicator_signals 66 | 67 | def set_indicator_signal(self, indicator: str, buy: float, sell: float, condition_buy: Any, condition_sell: Any, 68 | buy_max: float = None, sell_max: float = None, condition_buy_max: Any = None, condition_sell_max: Any = None) -> None: 69 | """Used to set an indicator where one indicator crosses above or below a certain numerical threshold. 70 | 71 | Arguments: 72 | ---- 73 | indicator {str} -- The indicator key, for example `ema` or `sma`. 74 | 75 | buy {float} -- The buy signal threshold for the indicator. 76 | 77 | sell {float} -- The sell signal threshold for the indicator. 78 | 79 | condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would 80 | represent greater than or from the `operator` module it would represent `operator.gt`. 81 | 82 | condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would 83 | represent greater than or from the `operator` module it would represent `operator.gt`. 84 | 85 | buy_max {float} -- If the buy threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 86 | This means if the signal exceeds this amount it WILL NOT PURCHASE THE INSTRUMENT. (defaults to None). 87 | 88 | sell_max {float} -- If the sell threshold has a maximum value that needs to be set, then set the `buy_max` threshold. 89 | This means if the signal exceeds this amount it WILL NOT SELL THE INSTRUMENT. (defaults to None). 90 | 91 | condition_buy_max {str} -- The operator which is used to evaluate the `buy_max` condition. For example, `">"` would 92 | represent greater than or from the `operator` module it would represent `operator.gt`. (defaults to None). 93 | 94 | condition_sell_max {str} -- The operator which is used to evaluate the `sell_max` condition. For example, `">"` would 95 | represent greater than or from the `operator` module it would represent `operator.gt`. (defaults to None). 96 | """ 97 | 98 | # Add the key if it doesn't exist. 99 | if indicator not in self._indicator_signals: 100 | self._indicator_signals[indicator] = {} 101 | self._indicators_key.append(indicator) 102 | 103 | # Add the signals. 104 | self._indicator_signals[indicator]['buy'] = buy 105 | self._indicator_signals[indicator]['sell'] = sell 106 | self._indicator_signals[indicator]['buy_operator'] = condition_buy 107 | self._indicator_signals[indicator]['sell_operator'] = condition_sell 108 | 109 | # Add the max signals 110 | self._indicator_signals[indicator]['buy_max'] = buy_max 111 | self._indicator_signals[indicator]['sell_max'] = sell_max 112 | self._indicator_signals[indicator]['buy_operator_max'] = condition_buy_max 113 | self._indicator_signals[indicator]['sell_operator_max'] = condition_sell_max 114 | 115 | def set_indicator_signal_compare(self, indicator_1: str, indicator_2: str, condition_buy: Any, condition_sell: Any) -> None: 116 | """Used to set an indicator where one indicator is compared to another indicator. 117 | 118 | Overview: 119 | ---- 120 | Some trading strategies depend on comparing one indicator to another indicator. 121 | For example, the Simple Moving Average crossing above or below the Exponential 122 | Moving Average. This will be used to help build those strategies that depend 123 | on this type of structure. 124 | 125 | Arguments: 126 | ---- 127 | indicator_1 {str} -- The first indicator key, for example `ema` or `sma`. 128 | 129 | indicator_2 {str} -- The second indicator key, this is the indicator we will compare to. For example, 130 | is the `sma` greater than the `ema`. 131 | 132 | condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would 133 | represent greater than or from the `operator` module it would represent `operator.gt`. 134 | 135 | condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would 136 | represent greater than or from the `operator` module it would represent `operator.gt`. 137 | """ 138 | 139 | # Define the key. 140 | key = "{ind_1}_comp_{ind_2}".format( 141 | ind_1=indicator_1, 142 | ind_2=indicator_2 143 | ) 144 | 145 | # Add the key if it doesn't exist. 146 | if key not in self._indicator_signals: 147 | self._indicator_signals[key] = {} 148 | self._indicators_comp_key.append(key) 149 | 150 | # Grab the dictionary. 151 | indicator_dict = self._indicator_signals[key] 152 | 153 | # Add the signals. 154 | indicator_dict['type'] = 'comparison' 155 | indicator_dict['indicator_1'] = indicator_1 156 | indicator_dict['indicator_2'] = indicator_2 157 | indicator_dict['buy_operator'] = condition_buy 158 | indicator_dict['sell_operator'] = condition_sell 159 | 160 | @property 161 | def price_data_frame(self) -> pd.DataFrame: 162 | """Return the raw Pandas Dataframe Object. 163 | 164 | Returns: 165 | ---- 166 | {pd.DataFrame} -- A multi-index data frame. 167 | """ 168 | 169 | return self._frame 170 | 171 | @price_data_frame.setter 172 | def price_data_frame(self, price_data_frame: pd.DataFrame) -> None: 173 | """Sets the price data frame. 174 | 175 | Arguments: 176 | ---- 177 | price_data_frame {pd.DataFrame} -- A multi-index data frame. 178 | """ 179 | 180 | self._frame = price_data_frame 181 | 182 | @property 183 | def is_multi_index(self) -> bool: 184 | """Specifies whether the data frame is a multi-index dataframe. 185 | 186 | Returns: 187 | ---- 188 | {bool} -- `True` if the data frame is a `pd.MultiIndex` object. `False` otherwise. 189 | """ 190 | 191 | if isinstance(self._frame.index, pd.MultiIndex): 192 | return True 193 | else: 194 | return False 195 | 196 | def change_in_price(self, column_name: str = 'change_in_price') -> pd.DataFrame: 197 | """Calculates the Change in Price. 198 | 199 | Returns: 200 | ---- 201 | {pd.DataFrame} -- A data frame with the Change in Price included. 202 | """ 203 | 204 | locals_data = locals() 205 | del locals_data['self'] 206 | 207 | self._current_indicators[column_name] = {} 208 | self._current_indicators[column_name]['args'] = locals_data 209 | self._current_indicators[column_name]['func'] = self.change_in_price 210 | 211 | self._frame[column_name] = self._price_groups['close'].transform( 212 | lambda x: x.diff() 213 | ) 214 | 215 | return self._frame 216 | 217 | def rsi(self, period: int, method: str = 'wilders', column_name: str = 'rsi') -> pd.DataFrame: 218 | """Calculates the Relative Strength Index (RSI). 219 | 220 | Arguments: 221 | ---- 222 | period {int} -- The number of periods to use to calculate the RSI. 223 | 224 | Keyword Arguments: 225 | ---- 226 | method {str} -- The calculation methodology. (default: {'wilders'}) 227 | 228 | Returns: 229 | ---- 230 | {pd.DataFrame} -- A Pandas data frame with the RSI indicator included. 231 | 232 | Usage: 233 | ---- 234 | >>> historical_prices_df = trading_robot.grab_historical_prices( 235 | start=start_date, 236 | end=end_date, 237 | bar_size=1, 238 | bar_type='minute' 239 | ) 240 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 241 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 242 | >>> indicator_client.rsi(period=14) 243 | >>> price_data_frame = inidcator_client.price_data_frame 244 | """ 245 | 246 | locals_data = locals() 247 | del locals_data['self'] 248 | 249 | self._current_indicators[column_name] = {} 250 | self._current_indicators[column_name]['args'] = locals_data 251 | self._current_indicators[column_name]['func'] = self.rsi 252 | 253 | # First calculate the Change in Price. 254 | if 'change_in_price' not in self._frame.columns: 255 | self.change_in_price() 256 | 257 | # Define the up days. 258 | self._frame['up_day'] = self._price_groups['change_in_price'].transform( 259 | lambda x : np.where(x >= 0, x, 0) 260 | ) 261 | 262 | # Define the down days. 263 | self._frame['down_day'] = self._price_groups['change_in_price'].transform( 264 | lambda x : np.where(x < 0, x.abs(), 0) 265 | ) 266 | 267 | # Calculate the EWMA for the Up days. 268 | self._frame['ewma_up'] = self._price_groups['up_day'].transform( 269 | lambda x: x.ewm(span = period).mean() 270 | ) 271 | 272 | # Calculate the EWMA for the Down days. 273 | self._frame['ewma_down'] = self._price_groups['down_day'].transform( 274 | lambda x: x.ewm(span = period).mean() 275 | ) 276 | 277 | # Calculate the Relative Strength 278 | relative_strength = self._frame['ewma_up'] / self._frame['ewma_down'] 279 | 280 | # Calculate the Relative Strength Index 281 | relative_strength_index = 100.0 - (100.0 / (1.0 + relative_strength)) 282 | 283 | # Add the info to the data frame. 284 | self._frame['rsi'] = np.where(relative_strength_index == 0, 100, 100 - (100 / (1 + relative_strength_index))) 285 | 286 | # Clean up before sending back. 287 | self._frame.drop( 288 | labels=['ewma_up', 'ewma_down', 'down_day', 'up_day', 'change_in_price'], 289 | axis=1, 290 | inplace=True 291 | ) 292 | 293 | return self._frame 294 | 295 | def sma(self, period: int, column_name: str = 'sma') -> pd.DataFrame: 296 | """Calculates the Simple Moving Average (SMA). 297 | 298 | Arguments: 299 | ---- 300 | period {int} -- The number of periods to use when calculating the SMA. 301 | 302 | Returns: 303 | ---- 304 | {pd.DataFrame} -- A Pandas data frame with the SMA indicator included. 305 | 306 | Usage: 307 | ---- 308 | >>> historical_prices_df = trading_robot.grab_historical_prices( 309 | start=start_date, 310 | end=end_date, 311 | bar_size=1, 312 | bar_type='minute' 313 | ) 314 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 315 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 316 | >>> indicator_client.sma(period=100) 317 | """ 318 | 319 | locals_data = locals() 320 | del locals_data['self'] 321 | 322 | self._current_indicators[column_name] = {} 323 | self._current_indicators[column_name]['args'] = locals_data 324 | self._current_indicators[column_name]['func'] = self.sma 325 | 326 | # Add the SMA 327 | self._frame[column_name] = self._price_groups['close'].transform( 328 | lambda x: x.rolling(window=period).mean() 329 | ) 330 | 331 | return self._frame 332 | 333 | def ema(self, period: int, alpha: float = 0.0, column_name = 'ema') -> pd.DataFrame: 334 | """Calculates the Exponential Moving Average (EMA). 335 | 336 | Arguments: 337 | ---- 338 | period {int} -- The number of periods to use when calculating the EMA. 339 | 340 | alpha {float} -- The alpha weight used in the calculation. (default: {0.0}) 341 | 342 | Returns: 343 | ---- 344 | {pd.DataFrame} -- A Pandas data frame with the EMA indicator included. 345 | 346 | Usage: 347 | ---- 348 | >>> historical_prices_df = trading_robot.grab_historical_prices( 349 | start=start_date, 350 | end=end_date, 351 | bar_size=1, 352 | bar_type='minute' 353 | ) 354 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 355 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 356 | >>> indicator_client.ema(period=50, alpha=1/50) 357 | """ 358 | 359 | locals_data = locals() 360 | del locals_data['self'] 361 | 362 | self._current_indicators[column_name] = {} 363 | self._current_indicators[column_name]['args'] = locals_data 364 | self._current_indicators[column_name]['func'] = self.ema 365 | 366 | # Add the EMA 367 | self._frame[column_name] = self._price_groups['close'].transform( 368 | lambda x: x.ewm(span=period).mean() 369 | ) 370 | 371 | return self._frame 372 | 373 | def rate_of_change(self, period: int = 1, column_name: str = 'rate_of_change') -> pd.DataFrame: 374 | """Calculates the Rate of Change (ROC). 375 | 376 | Arguments: 377 | ---- 378 | period {int} -- The number of periods to use when calculating 379 | the ROC. (default: {1}) 380 | 381 | Returns: 382 | ---- 383 | {pd.DataFrame} -- A Pandas data frame with the ROC indicator included. 384 | 385 | Usage: 386 | ---- 387 | >>> historical_prices_df = trading_robot.grab_historical_prices( 388 | start=start_date, 389 | end=end_date, 390 | bar_size=1, 391 | bar_type='minute' 392 | ) 393 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 394 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 395 | >>> indicator_client.rate_of_change() 396 | """ 397 | locals_data = locals() 398 | del locals_data['self'] 399 | 400 | self._current_indicators[column_name] = {} 401 | self._current_indicators[column_name]['args'] = locals_data 402 | self._current_indicators[column_name]['func'] = self.rate_of_change 403 | 404 | # Add the Momentum indicator. 405 | self._frame[column_name] = self._price_groups['close'].transform( 406 | lambda x: x.pct_change(periods=period) 407 | ) 408 | 409 | return self._frame 410 | 411 | def bollinger_bands(self, period: int = 20, column_name: str = 'bollinger_bands') -> pd.DataFrame: 412 | """Calculates the Bollinger Bands. 413 | 414 | Arguments: 415 | ---- 416 | period {int} -- The number of periods to use when calculating 417 | the Bollinger Bands. (default: {20}) 418 | 419 | Returns: 420 | ---- 421 | {pd.DataFrame} -- A Pandas data frame with the Lower and Upper band 422 | indicator included. 423 | 424 | Usage: 425 | ---- 426 | >>> historical_prices_df = trading_robot.grab_historical_prices( 427 | start=start_date, 428 | end=end_date, 429 | bar_size=1, 430 | bar_type='minute' 431 | ) 432 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 433 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 434 | >>> indicator_client.bollinger_bands() 435 | """ 436 | locals_data = locals() 437 | del locals_data['self'] 438 | 439 | self._current_indicators[column_name] = {} 440 | self._current_indicators[column_name]['args'] = locals_data 441 | self._current_indicators[column_name]['func'] = self.bollinger_bands 442 | 443 | # Define the Moving Avg. 444 | self._frame['moving_avg'] = self._price_groups['close'].transform( 445 | lambda x : x.rolling(window=period).mean() 446 | ) 447 | 448 | # Define Moving Std. 449 | self._frame['moving_std'] = self._price_groups['close'].transform( 450 | lambda x : x.rolling(window=period).std() 451 | ) 452 | 453 | # Define the Upper Band. 454 | self._frame['band_upper'] = 4 * (self._frame['moving_std'] / self._frame['moving_avg']) 455 | 456 | # Define the lower band 457 | self._frame['band_lower'] = ( 458 | (self._frame['close'] - self._frame['moving_avg']) + 459 | (2 * self._frame['moving_std']) / 460 | (4 * self._frame['moving_std']) 461 | ) 462 | 463 | # Clean up before sending back. 464 | self._frame.drop( 465 | labels=['moving_avg', 'moving_std'], 466 | axis=1, 467 | inplace=True 468 | ) 469 | 470 | return self._frame 471 | 472 | def average_true_range(self, period: int = 14, column_name: str ='average_true_range') -> pd.DataFrame: 473 | """Calculates the Average True Range (ATR). 474 | 475 | Arguments: 476 | ---- 477 | period {int} -- The number of periods to use when calculating 478 | the ATR. (default: {14}) 479 | 480 | Returns: 481 | ---- 482 | {pd.DataFrame} -- A Pandas data frame with the ATR included. 483 | 484 | Usage: 485 | ---- 486 | >>> historical_prices_df = trading_robot.grab_historical_prices( 487 | start=start_date, 488 | end=end_date, 489 | bar_size=1, 490 | bar_type='minute' 491 | ) 492 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 493 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 494 | >>> indicator_client.average_true_range() 495 | """ 496 | 497 | locals_data = locals() 498 | del locals_data['self'] 499 | 500 | self._current_indicators[column_name] = {} 501 | self._current_indicators[column_name]['args'] = locals_data 502 | self._current_indicators[column_name]['func'] = self.average_true_range 503 | 504 | 505 | # Calculate the different parts of True Range. 506 | self._frame['true_range_0'] = abs(self._frame['high'] - self._frame['low']) 507 | self._frame['true_range_1'] = abs(self._frame['high'] - self._frame['close'].shift()) 508 | self._frame['true_range_2'] = abs(self._frame['low'] - self._frame['close'].shift()) 509 | 510 | # Grab the Max. 511 | self._frame['true_range'] = self._frame[['true_range_0', 'true_range_1', 'true_range_2']].max(axis=1) 512 | 513 | # Calculate the Average True Range. 514 | self._frame['average_true_range'] = self._frame['true_range'].transform( 515 | lambda x: x.ewm(span = period, min_periods = period).mean() 516 | ) 517 | 518 | # Clean up before sending back. 519 | self._frame.drop( 520 | labels=['true_range_0', 'true_range_1', 'true_range_2', 'true_range'], 521 | axis=1, 522 | inplace=True 523 | ) 524 | 525 | return self._frame 526 | 527 | def stochastic_oscillator(self, column_name: str = 'stochastic_oscillator') -> pd.DataFrame: 528 | """Calculates the Stochastic Oscillator. 529 | 530 | Returns: 531 | ---- 532 | {pd.DataFrame} -- A Pandas data frame with the Stochastic Oscillator included. 533 | 534 | Usage: 535 | ---- 536 | >>> historical_prices_df = trading_robot.grab_historical_prices( 537 | start=start_date, 538 | end=end_date, 539 | bar_size=1, 540 | bar_type='minute' 541 | ) 542 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 543 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 544 | >>> indicator_client.stochastic_oscillator() 545 | """ 546 | 547 | locals_data = locals() 548 | del locals_data['self'] 549 | 550 | self._current_indicators[column_name] = {} 551 | self._current_indicators[column_name]['args'] = locals_data 552 | self._current_indicators[column_name]['func'] = self.stochastic_oscillator 553 | 554 | # Calculate the stochastic_oscillator. 555 | self._frame['stochastic_oscillator'] = ( 556 | self._frame['close'] - self._frame['low'] / 557 | self._frame['high'] - self._frame['low'] 558 | ) 559 | 560 | return self._frame 561 | 562 | def macd(self, fast_period: int = 12, slow_period: int = 26, column_name: str = 'macd') -> pd.DataFrame: 563 | """Calculates the Moving Average Convergence Divergence (MACD). 564 | 565 | Arguments: 566 | ---- 567 | fast_period {int} -- The number of periods to use when calculating 568 | the fast moving MACD. (default: {12}) 569 | 570 | slow_period {int} -- The number of periods to use when calculating 571 | the slow moving MACD. (default: {26}) 572 | 573 | Returns: 574 | ---- 575 | {pd.DataFrame} -- A Pandas data frame with the MACD included. 576 | 577 | Usage: 578 | ---- 579 | >>> historical_prices_df = trading_robot.grab_historical_prices( 580 | start=start_date, 581 | end=end_date, 582 | bar_size=1, 583 | bar_type='minute' 584 | ) 585 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 586 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 587 | >>> indicator_client.macd(fast_period=12, slow_period=26) 588 | """ 589 | 590 | locals_data = locals() 591 | del locals_data['self'] 592 | 593 | self._current_indicators[column_name] = {} 594 | self._current_indicators[column_name]['args'] = locals_data 595 | self._current_indicators[column_name]['func'] = self.macd 596 | 597 | # Calculate the Fast Moving MACD. 598 | self._frame['macd_fast'] = self._frame['close'].transform( 599 | lambda x: x.ewm(span = fast_period, min_periods = fast_period).mean() 600 | ) 601 | 602 | # Calculate the Slow Moving MACD. 603 | self._frame['macd_slow'] = self._frame['close'].transform( 604 | lambda x: x.ewm(span = slow_period, min_periods = slow_period).mean() 605 | ) 606 | 607 | # Calculate the difference between the fast and the slow. 608 | self._frame['macd_diff'] = self._frame['macd_fast'] - self._frame['macd_slow'] 609 | 610 | # Calculate the Exponential moving average of the fast. 611 | self._frame['macd'] = self._frame['macd_diff'].transform( 612 | lambda x: x.ewm(span = 9, min_periods = 8).mean() 613 | ) 614 | 615 | return self._frame 616 | 617 | def mass_index(self, period: int = 9, column_name: str = 'mass_index') -> pd.DataFrame: 618 | """Calculates the Mass Index indicator. 619 | 620 | Arguments: 621 | ---- 622 | period {int} -- The number of periods to use when calculating 623 | the mass index. (default: {9}) 624 | 625 | Returns: 626 | ---- 627 | {pd.DataFrame} -- A Pandas data frame with the Mass Index included. 628 | 629 | Usage: 630 | ---- 631 | >>> historical_prices_df = trading_robot.grab_historical_prices( 632 | start=start_date, 633 | end=end_date, 634 | bar_size=1, 635 | bar_type='minute' 636 | ) 637 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 638 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 639 | >>> indicator_client.mass_index(period=9) 640 | """ 641 | 642 | locals_data = locals() 643 | del locals_data['self'] 644 | 645 | self._current_indicators[column_name] = {} 646 | self._current_indicators[column_name]['args'] = locals_data 647 | self._current_indicators[column_name]['func'] = self.mass_index 648 | 649 | # Calculate the Diff. 650 | self._frame['diff'] = self._frame['high'] - self._frame['low'] 651 | 652 | # Calculate Mass Index 1 653 | self._frame['mass_index_1'] = self._frame['diff'].transform( 654 | lambda x: x.ewm(span = period, min_periods = period - 1).mean() 655 | ) 656 | 657 | # Calculate Mass Index 2 658 | self._frame['mass_index_2'] = self._frame['mass_index_1'].transform( 659 | lambda x: x.ewm(span = period, min_periods = period - 1).mean() 660 | ) 661 | 662 | # Grab the raw index. 663 | self._frame['mass_index_raw'] = self._frame['mass_index_1'] / self._frame['mass_index_2'] 664 | 665 | # Calculate the Mass Index. 666 | self._frame['mass_index'] = self._frame['mass_index_raw'].transform( 667 | lambda x: x.rolling(window=25).sum() 668 | ) 669 | 670 | # Clean up before sending back. 671 | self._frame.drop( 672 | labels=['diff', 'mass_index_1', 'mass_index_2', 'mass_index_raw'], 673 | axis=1, 674 | inplace=True 675 | ) 676 | 677 | return self._frame 678 | 679 | def force_index(self, period: int, column_name: str = 'force_index') -> pd.DataFrame: 680 | """Calculates the Force Index. 681 | 682 | Arguments: 683 | ---- 684 | period {int} -- The number of periods to use when calculating 685 | the force index. 686 | 687 | Returns: 688 | ---- 689 | {pd.DataFrame} -- A Pandas data frame with the force index included. 690 | 691 | Usage: 692 | ---- 693 | >>> historical_prices_df = trading_robot.grab_historical_prices( 694 | start=start_date, 695 | end=end_date, 696 | bar_size=1, 697 | bar_type='minute' 698 | ) 699 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 700 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 701 | >>> indicator_client.force_index(period=9) 702 | """ 703 | 704 | locals_data = locals() 705 | del locals_data['self'] 706 | 707 | self._current_indicators[column_name] = {} 708 | self._current_indicators[column_name]['args'] = locals_data 709 | self._current_indicators[column_name]['func'] = self.force_index 710 | 711 | # Calculate the Force Index. 712 | self._frame[column_name] = self._frame['close'].diff(period) * self._frame['volume'].diff(period) 713 | 714 | return self._frame 715 | 716 | def ease_of_movement(self, period: int, column_name: str = 'ease_of_movement') -> pd.DataFrame: 717 | """Calculates the Ease of Movement. 718 | 719 | Arguments: 720 | ---- 721 | period {int} -- The number of periods to use when calculating 722 | the Ease of Movement. 723 | 724 | Returns: 725 | ---- 726 | {pd.DataFrame} -- A Pandas data frame with the Ease of Movement included. 727 | 728 | Usage: 729 | ---- 730 | >>> historical_prices_df = trading_robot.grab_historical_prices( 731 | start=start_date, 732 | end=end_date, 733 | bar_size=1, 734 | bar_type='minute' 735 | ) 736 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 737 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 738 | >>> indicator_client.ease_of_movement(period=9) 739 | """ 740 | 741 | locals_data = locals() 742 | del locals_data['self'] 743 | 744 | self._current_indicators[column_name] = {} 745 | self._current_indicators[column_name]['args'] = locals_data 746 | self._current_indicators[column_name]['func'] = self.ease_of_movement 747 | 748 | # Calculate the ease of movement. 749 | high_plus_low = (self._frame['high'].diff(1) + self._frame['low'].diff(1)) 750 | diff_divi_vol = (self._frame['high'] - self._frame['low']) / (2 * self._frame['volume']) 751 | self._frame['ease_of_movement_raw'] = high_plus_low * diff_divi_vol 752 | 753 | # Calculate the Rolling Average of the Ease of Movement. 754 | self._frame['ease_of_movement'] = self._frame['ease_of_movement_raw'].transform( 755 | lambda x: x.rolling(window=period).mean() 756 | ) 757 | 758 | # Clean up before sending back. 759 | self._frame.drop( 760 | labels=['ease_of_movement_raw'], 761 | axis=1, 762 | inplace=True 763 | ) 764 | 765 | return self._frame 766 | 767 | def commodity_channel_index(self, period: int, column_name: str = 'commodity_channel_index') -> pd.DataFrame: 768 | """Calculates the Commodity Channel Index. 769 | 770 | Arguments: 771 | ---- 772 | period {int} -- The number of periods to use when calculating 773 | the Commodity Channel Index. 774 | 775 | Returns: 776 | ---- 777 | {pd.DataFrame} -- A Pandas data frame with the Commodity Channel Index included. 778 | 779 | Usage: 780 | ---- 781 | >>> historical_prices_df = trading_robot.grab_historical_prices( 782 | start=start_date, 783 | end=end_date, 784 | bar_size=1, 785 | bar_type='minute' 786 | ) 787 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 788 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 789 | >>> indicator_client.commodity_channel_index(period=9) 790 | """ 791 | 792 | locals_data = locals() 793 | del locals_data['self'] 794 | 795 | self._current_indicators[column_name] = {} 796 | self._current_indicators[column_name]['args'] = locals_data 797 | self._current_indicators[column_name]['func'] = self.commodity_channel_index 798 | 799 | # Calculate the Typical Price. 800 | self._frame['typical_price'] = (self._frame['high'] + self._frame['low'] + self._frame['close']) / 3 801 | 802 | # Calculate the Rolling Average of the Typical Price. 803 | self._frame['typical_price_mean'] = self._frame['pp'].transform( 804 | lambda x: x.rolling(window=period).mean() 805 | ) 806 | 807 | # Calculate the Rolling Standard Deviation of the Typical Price. 808 | self._frame['typical_price_std'] = self._frame['pp'].transform( 809 | lambda x: x.rolling(window=period).std() 810 | ) 811 | 812 | # Calculate the Commodity Channel Index. 813 | self._frame[column_name] = self._frame['typical_price_mean'] / self._frame['typical_price_std'] 814 | 815 | # Clean up before sending back. 816 | self._frame.drop( 817 | labels=['typical_price', 'typical_price_mean', 'typical_price_std'], 818 | axis=1, 819 | inplace=True 820 | ) 821 | 822 | return self._frame 823 | 824 | def standard_deviation(self, period: int, column_name: str = 'standard_deviation') -> pd.DataFrame: 825 | """Calculates the Standard Deviation. 826 | 827 | Arguments: 828 | ---- 829 | period {int} -- The number of periods to use when calculating 830 | the standard deviation. 831 | 832 | Returns: 833 | ---- 834 | {pd.DataFrame} -- A Pandas data frame with the Standard Deviation included. 835 | 836 | Usage: 837 | ---- 838 | >>> historical_prices_df = trading_robot.grab_historical_prices( 839 | start=start_date, 840 | end=end_date, 841 | bar_size=1, 842 | bar_type='minute' 843 | ) 844 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 845 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 846 | >>> indicator_client.standard_deviation(period=9) 847 | """ 848 | 849 | locals_data = locals() 850 | del locals_data['self'] 851 | 852 | self._current_indicators[column_name] = {} 853 | self._current_indicators[column_name]['args'] = locals_data 854 | self._current_indicators[column_name]['func'] = self.standard_deviation 855 | 856 | # Calculate the Standard Deviation. 857 | self._frame[column_name] = self._frame['close'].transform( 858 | lambda x: x.ewm(span=period).std() 859 | ) 860 | 861 | return self._frame 862 | 863 | def chaikin_oscillator(self, period: int, column_name: str = 'chaikin_oscillator') -> pd.DataFrame: 864 | """Calculates the Chaikin Oscillator. 865 | 866 | Arguments: 867 | ---- 868 | period {int} -- The number of periods to use when calculating 869 | the Chaikin Oscillator. 870 | 871 | Returns: 872 | ---- 873 | {pd.DataFrame} -- A Pandas data frame with the Chaikin Oscillator included. 874 | 875 | Usage: 876 | ---- 877 | >>> historical_prices_df = trading_robot.grab_historical_prices( 878 | start=start_date, 879 | end=end_date, 880 | bar_size=1, 881 | bar_type='minute' 882 | ) 883 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 884 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 885 | >>> indicator_client.chaikin_oscillator(period=9) 886 | """ 887 | 888 | locals_data = locals() 889 | del locals_data['self'] 890 | 891 | self._current_indicators[column_name] = {} 892 | self._current_indicators[column_name]['args'] = locals_data 893 | self._current_indicators[column_name]['func'] = self.chaikin_oscillator 894 | 895 | # Calculate the Money Flow Multiplier. 896 | money_flow_multiplier_top = 2 * (self._frame['close'] - self._frame['high'] - self._frame['low']) 897 | money_flow_multiplier_bot = (self._frame['high'] - self._frame['low']) 898 | 899 | # Calculate Money Flow Volume 900 | self._frame['money_flow_volume'] = (money_flow_multiplier_top / money_flow_multiplier_bot) * self._frame['volume'] 901 | 902 | # Calculate the 3-Day moving average of the Money Flow Volume. 903 | self._frame['money_flow_volume_3'] = self._frame['money_flow_volume'].transform( 904 | lambda x: x.ewm(span=3, min_periods=2).mean() 905 | ) 906 | 907 | # Calculate the 10-Day moving average of the Money Flow Volume. 908 | self._frame['money_flow_volume_10'] = self._frame['money_flow_volume'].transform( 909 | lambda x: x.ewm(span=10, min_periods=9).mean() 910 | ) 911 | 912 | # Calculate the Chaikin Oscillator. 913 | self._frame[column_name] = self._frame['money_flow_volume_3'] - self._frame['money_flow_volume_10'] 914 | 915 | # Clean up before sending back. 916 | self._frame.drop( 917 | labels=['money_flow_volume_3', 'money_flow_volume_10', 'money_flow_volume'], 918 | axis=1, 919 | inplace=True 920 | ) 921 | 922 | return self._frame 923 | 924 | def kst_oscillator(self, r1: int, r2: int, r3: int, r4: int, n1: int, n2: int, n3: int, n4: int, column_name: str = 'kst_oscillator') -> pd.DataFrame: 925 | """Calculates the Mass Index indicator. 926 | 927 | Arguments: 928 | ---- 929 | period {int} -- The number of periods to use when calculating 930 | the mass index. (default: {9}) 931 | 932 | Returns: 933 | ---- 934 | {pd.DataFrame} -- A Pandas data frame with the Mass Index included. 935 | 936 | Usage: 937 | ---- 938 | >>> historical_prices_df = trading_robot.grab_historical_prices( 939 | start=start_date, 940 | end=end_date, 941 | bar_size=1, 942 | bar_type='minute' 943 | ) 944 | >>> price_data_frame = pd.DataFrame(data=historical_prices) 945 | >>> indicator_client = Indicators(price_data_frame=price_data_frame) 946 | >>> indicator_client.mass_index(period=9) 947 | """ 948 | 949 | locals_data = locals() 950 | del locals_data['self'] 951 | 952 | self._current_indicators[column_name] = {} 953 | self._current_indicators[column_name]['args'] = locals_data 954 | self._current_indicators[column_name]['func'] = self.kst_oscillator 955 | 956 | # Calculate the ROC 1. 957 | self._frame['roc_1'] = self._frame['close'].diff(r1 - 1) / self._frame['close'].shift(r1 - 1) 958 | 959 | # Calculate the ROC 2. 960 | self._frame['roc_2'] = self._frame['close'].diff(r2 - 1) / self._frame['close'].shift(r2 - 1) 961 | 962 | # Calculate the ROC 3. 963 | self._frame['roc_3'] = self._frame['close'].diff(r3 - 1) / self._frame['close'].shift(r3 - 1) 964 | 965 | # Calculate the ROC 4. 966 | self._frame['roc_4'] = self._frame['close'].diff(r4 - 1) / self._frame['close'].shift(r4 - 1) 967 | 968 | 969 | # Calculate the Mass Index. 970 | self._frame['roc_1_n'] = self._frame['roc_1'].transform( 971 | lambda x: x.rolling(window=n1).sum() 972 | ) 973 | 974 | # Calculate the Mass Index. 975 | self._frame['roc_2_n'] = self._frame['roc_2'].transform( 976 | lambda x: x.rolling(window=n2).sum() 977 | ) 978 | 979 | # Calculate the Mass Index. 980 | self._frame['roc_3_n'] = self._frame['roc_3'].transform( 981 | lambda x: x.rolling(window=n3).sum() 982 | ) 983 | 984 | # Calculate the Mass Index. 985 | self._frame['roc_4_n'] = self._frame['roc_4'].transform( 986 | lambda x: x.rolling(window=n4).sum() 987 | ) 988 | 989 | self._frame[column_name] = 100 * (self._frame['roc_1_n'] + 2 * self._frame['roc_2_n'] + 3 * self._frame['roc_3_n'] + 4 * self._frame['roc_4_n']) 990 | self._frame[column_name + "_signal"] = self._frame['column_name'].transform( 991 | lambda x: x.rolling().mean() 992 | ) 993 | 994 | # Clean up before sending back. 995 | self._frame.drop( 996 | labels=['roc_1', 'roc_2', 'roc_3', 'roc_4', 'roc_1_n', 'roc_2_n', 'roc_3_n', 'roc_4_n'], 997 | axis=1, 998 | inplace=True 999 | ) 1000 | 1001 | return self._frame 1002 | 1003 | def refresh(self): 1004 | """Updates the Indicator columns after adding the new rows.""" 1005 | 1006 | # First update the groups since, we have new rows. 1007 | self._price_groups = self._stock_frame.symbol_groups 1008 | 1009 | # Grab all the details of the indicators so far. 1010 | for indicator in self._current_indicators: 1011 | 1012 | # Grab the function. 1013 | indicator_argument = self._current_indicators[indicator]['args'] 1014 | 1015 | # Grab the arguments. 1016 | indicator_function = self._current_indicators[indicator]['func'] 1017 | 1018 | # Update the function. 1019 | indicator_function(**indicator_argument) 1020 | 1021 | def check_signals(self) -> Union[pd.DataFrame, None]: 1022 | """Checks to see if any signals have been generated. 1023 | 1024 | Returns: 1025 | ---- 1026 | {Union[pd.DataFrame, None]} -- If signals are generated then a pandas.DataFrame 1027 | is returned otherwise nothing is returned. 1028 | """ 1029 | 1030 | signals_df = self._stock_frame._check_signals( 1031 | indicators=self._indicator_signals, 1032 | indciators_comp_key=self._indicators_comp_key, 1033 | indicators_key=self._indicators_key 1034 | ) 1035 | 1036 | return signals_df 1037 | 1038 | 1039 | # #KST Oscillator 1040 | # def KST(df, r1, r2, r3, r4, n1, n2, n3, n4): 1041 | # M = df['Close'].diff(r1 - 1) 1042 | # N = df['Close'].shift(r1 - 1) 1043 | # ROC1 = M / N 1044 | # M = df['Close'].diff(r2 - 1) 1045 | # N = df['Close'].shift(r2 - 1) 1046 | # ROC2 = M / N 1047 | # M = df['Close'].diff(r3 - 1) 1048 | # N = df['Close'].shift(r3 - 1) 1049 | # ROC3 = M / N 1050 | # M = df['Close'].diff(r4 - 1) 1051 | # N = df['Close'].shift(r4 - 1) 1052 | # ROC4 = M / N 1053 | # KST = pd.Series(pd.rolling_sum(ROC1, n1) + pd.rolling_sum(ROC2, n2) * 2 + pd.rolling_sum(ROC3, n3) * 3 + 1054 | # pd.rolling_sum(ROC4, n4) * 4, name = 'KST_' + str(r1) + '_' + str(r2) + '_' + str(r3) + '_' + str(r4) + '_' + 1055 | # str(n1) + '_' + str(n2) + '_' + str(n3) + '_' + str(n4)) 1056 | # df = df.join(KST) 1057 | # return df 1058 | -------------------------------------------------------------------------------- /pyrobot/order_status.py: -------------------------------------------------------------------------------- 1 | from pyrobot.trades import Trade 2 | from td.client import TDClient 3 | 4 | class OrderStatus(): 5 | 6 | def __init__(self, trade_obj: Trade) -> None: 7 | 8 | self.trade_obj = trade_obj 9 | self.order_status = self.trade_obj.order_status 10 | 11 | @property 12 | def is_cancelled(self, refresh_order_info: bool = True) -> bool: 13 | """Specifies whether the order was filled or not. 14 | 15 | Arguments: 16 | ---- 17 | refresh_order_info {bool} -- Specifies whether you want 18 | to refresh the order data from the TD API before 19 | checking. If `True` a request will be made to the 20 | TD API to grab the latest Order Info. 21 | 22 | Returns 23 | ------- 24 | bool 25 | `True` if the order status is `FILLED`, `False` 26 | otherwise. 27 | """ 28 | 29 | if refresh_order_info: 30 | self.trade_obj._update_order_status() 31 | 32 | if self.order_status == 'FILLED': 33 | return True 34 | else: 35 | return False 36 | 37 | @property 38 | def is_rejected(self, refresh_order_info: bool = True) -> bool: 39 | """Specifies whether the order was rejected or not. 40 | 41 | Arguments: 42 | ---- 43 | refresh_order_info {bool} -- Specifies whether you want 44 | to refresh the order data from the TD API before 45 | checking. If `True` a request will be made to the 46 | TD API to grab the latest Order Info. 47 | 48 | Returns 49 | ------- 50 | bool 51 | `True` if the order status is `REJECTED`, `False` 52 | otherwise. 53 | """ 54 | 55 | if refresh_order_info: 56 | self.trade_obj._update_order_status() 57 | 58 | if self.order_status == 'REJECTED': 59 | return True 60 | else: 61 | return False 62 | 63 | @property 64 | def is_expired(self, refresh_order_info: bool = True) -> bool: 65 | """Specifies whether the order has expired or not. 66 | 67 | Arguments: 68 | ---- 69 | refresh_order_info {bool} -- Specifies whether you want 70 | to refresh the order data from the TD API before 71 | checking. If `True` a request will be made to the 72 | TD API to grab the latest Order Info. 73 | 74 | Returns 75 | ------- 76 | bool 77 | `True` if the order status is `EXPIRED`, `False` 78 | otherwise. 79 | """ 80 | 81 | if refresh_order_info: 82 | self.trade_obj._update_order_status() 83 | 84 | if self.order_status == 'EXPIRED': 85 | return True 86 | else: 87 | return False 88 | 89 | @property 90 | def is_replaced(self, refresh_order_info: bool = True) -> bool: 91 | """Specifies whether the order has been replaced or not. 92 | 93 | Arguments: 94 | ---- 95 | refresh_order_info {bool} -- Specifies whether you want 96 | to refresh the order data from the TD API before 97 | checking. If `True` a request will be made to the 98 | TD API to grab the latest Order Info. 99 | 100 | Returns 101 | ------- 102 | bool 103 | `True` if the order status is `REPLACED`, `False` 104 | otherwise. 105 | """ 106 | 107 | if refresh_order_info: 108 | self.trade_obj._update_order_status() 109 | 110 | if self.order_status == 'REPLACED': 111 | return True 112 | else: 113 | return False 114 | 115 | @property 116 | def is_working(self, refresh_order_info: bool = True) -> bool: 117 | """Specifies whether the order is working or not. 118 | 119 | Arguments: 120 | ---- 121 | refresh_order_info {bool} -- Specifies whether you want 122 | to refresh the order data from the TD API before 123 | checking. If `True` a request will be made to the 124 | TD API to grab the latest Order Info. 125 | 126 | Returns 127 | ------- 128 | bool 129 | `True` if the order status is `WORKING`, `False` 130 | otherwise. 131 | """ 132 | 133 | if refresh_order_info: 134 | self.trade_obj._update_order_status() 135 | 136 | if self.order_status == 'WORKING': 137 | return True 138 | else: 139 | return False 140 | 141 | @property 142 | def is_pending_activation(self, refresh_order_info: bool = True) -> bool: 143 | """Specifies whether the order is pending activation or not. 144 | 145 | Arguments: 146 | ---- 147 | refresh_order_info {bool} -- Specifies whether you want 148 | to refresh the order data from the TD API before 149 | checking. If `True` a request will be made to the 150 | TD API to grab the latest Order Info. 151 | 152 | Returns 153 | ------- 154 | bool 155 | `True` if the order status is `PENDING_ACTIVATION`, 156 | `False` otherwise. 157 | """ 158 | 159 | if refresh_order_info: 160 | self.trade_obj._update_order_status() 161 | 162 | if self.order_status == 'PENDING_ACTIVATION': 163 | return True 164 | else: 165 | return False 166 | 167 | @property 168 | def is_pending_cancel(self, refresh_order_info: bool = True) -> bool: 169 | """Specifies whether the order is pending cancellation or not. 170 | 171 | Arguments: 172 | ---- 173 | refresh_order_info {bool} -- Specifies whether you want 174 | to refresh the order data from the TD API before 175 | checking. If `True` a request will be made to the 176 | TD API to grab the latest Order Info. 177 | 178 | Returns 179 | ------- 180 | bool 181 | `True` if the order status is `PENDING_CANCEL`, 182 | `False` otherwise. 183 | """ 184 | 185 | if refresh_order_info: 186 | self.trade_obj._update_order_status() 187 | 188 | if self.order_status == 'PENDING_CANCEL': 189 | return True 190 | else: 191 | return False 192 | 193 | @property 194 | def is_pending_replace(self, refresh_order_info: bool = True) -> bool: 195 | """Specifies whether the order is pending replacement or not. 196 | 197 | Arguments: 198 | ---- 199 | refresh_order_info {bool} -- Specifies whether you want 200 | to refresh the order data from the TD API before 201 | checking. If `True` a request will be made to the 202 | TD API to grab the latest Order Info. 203 | 204 | Returns 205 | ------- 206 | bool 207 | `True` if the order status is `PENDING_REPLACE`, 208 | `False` otherwise. 209 | """ 210 | 211 | if refresh_order_info: 212 | self.trade_obj._update_order_status() 213 | 214 | if self.order_status == 'PENDING_REPLACE': 215 | return True 216 | else: 217 | return False 218 | 219 | @property 220 | def is_queued(self, refresh_order_info: bool = True) -> bool: 221 | """Specifies whether the order is in the queue or not. 222 | 223 | Arguments: 224 | ---- 225 | refresh_order_info {bool} -- Specifies whether you want 226 | to refresh the order data from the TD API before 227 | checking. If `True` a request will be made to the 228 | TD API to grab the latest Order Info. 229 | 230 | Returns 231 | ------- 232 | bool 233 | `True` if the order status is `QUEUED`, `False` 234 | otherwise. 235 | """ 236 | 237 | if refresh_order_info: 238 | self.trade_obj._update_order_status() 239 | 240 | if self.order_status == 'QUEUED': 241 | return True 242 | else: 243 | return False 244 | 245 | @property 246 | def is_accepted(self, refresh_order_info: bool = True) -> bool: 247 | """Specifies whether the order was accepted or not. 248 | 249 | Arguments: 250 | ---- 251 | refresh_order_info {bool} -- Specifies whether you want 252 | to refresh the order data from the TD API before 253 | checking. If `True` a request will be made to the 254 | TD API to grab the latest Order Info. 255 | 256 | Returns 257 | ------- 258 | bool 259 | `True` if the order status is `ACCEPTED`, `False` 260 | otherwise. 261 | """ 262 | 263 | if refresh_order_info: 264 | self.trade_obj._update_order_status() 265 | 266 | if self.order_status == 'ACCEPTED': 267 | return True 268 | else: 269 | return False 270 | 271 | @property 272 | def is_awaiting_parent_order(self, refresh_order_info: bool = True) -> bool: 273 | """Specifies whether the order is waiting for the parent order 274 | to execute or not. 275 | 276 | Arguments: 277 | ---- 278 | refresh_order_info {bool} -- Specifies whether you want 279 | to refresh the order data from the TD API before 280 | checking. If `True` a request will be made to the 281 | TD API to grab the latest Order Info. 282 | 283 | Returns 284 | ------- 285 | bool 286 | `True` if the order status is `AWAITING_PARENT_ORDER`, 287 | `False` otherwise. 288 | """ 289 | 290 | if refresh_order_info: 291 | self.trade_obj._update_order_status() 292 | 293 | if self.order_status == 'AWAITING_PARENT_ORDER': 294 | return True 295 | else: 296 | return False 297 | 298 | @property 299 | def is_awaiting_condition(self, refresh_order_info: bool = True) -> bool: 300 | """Specifies whether the order is waiting for the condition 301 | to execute or not. 302 | 303 | Arguments: 304 | ---- 305 | refresh_order_info {bool} -- Specifies whether you want 306 | to refresh the order data from the TD API before 307 | checking. If `True` a request will be made to the 308 | TD API to grab the latest Order Info. 309 | 310 | Returns 311 | ------- 312 | bool 313 | `True` if the order status is `AWAITING_CONDITION`, 314 | `False` otherwise. 315 | """ 316 | 317 | if refresh_order_info: 318 | self.trade_obj._update_order_status() 319 | 320 | if self.order_status == 'AWAITING_CONDITION': 321 | return True 322 | else: 323 | return False -------------------------------------------------------------------------------- /pyrobot/portfolio.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pandas import DataFrame 4 | from typing import Tuple 5 | from typing import List 6 | from typing import Optional 7 | 8 | 9 | from pyrobot.stock_frame import StockFrame 10 | from td.client import TDClient 11 | 12 | 13 | class Portfolio(): 14 | 15 | def __init__(self, account_number: Optional[str] = None) -> None: 16 | """Initalizes a new instance of the Portfolio object. 17 | 18 | Keyword Arguments: 19 | ---- 20 | account_number {str} -- An accout number to associate with the Portfolio. (default: {None}) 21 | """ 22 | 23 | self.positions = {} 24 | self.positions_count = 0 25 | 26 | self.profit_loss = 0.00 27 | self.market_value = 0.00 28 | self.risk_tolerance = 0.00 29 | self.account_number = account_number 30 | 31 | self._historical_prices = [] 32 | 33 | self._td_client: TDClient = None 34 | self._stock_frame: StockFrame = None 35 | self._stock_frame_daily: StockFrame = None 36 | 37 | def add_positions(self, positions: List[dict]) -> dict: 38 | """Add Multiple positions to the portfolio at once. 39 | 40 | This method will take an iterable containing the values 41 | normally passed through in the `add_position` endpoint and 42 | then adds each position to the portfolio. 43 | 44 | Arguments: 45 | ---- 46 | positions {list[dict]} -- Multiple positions with the required arguments to be added. 47 | 48 | Returns: 49 | ---- 50 | {dict} -- The current positions in the portfolio. 51 | 52 | Usage: 53 | ---- 54 | >>> # Define mutliple positions to add. 55 | >>> multi_position = [ 56 | { 57 | 'asset_type': 'equity', 58 | 'quantity': 2, 59 | 'purchase_price': 4.00, 60 | 'symbol': 'TSLA', 61 | 'purchase_date': '2020-01-31' 62 | }, 63 | { 64 | 'asset_type': 'equity', 65 | 'quantity': 2,0 66 | 'purchase_price': 4.00, 67 | 'symbol': 'SQ', 68 | 'purchase_date': '2020-01-31' 69 | } 70 | ] 71 | >>> new_positions = trading_robot.portfolio.add_positions(positions=multi_position) 72 | { 73 | 'SQ': { 74 | 'asset_type': 'equity', 75 | 'purchase_date': '2020-01-31', 76 | 'purchase_price': 4.00, 77 | 'quantity': 2, 78 | 'symbol': 'SQ' 79 | }, 80 | 'TSLA': { 81 | 'asset_type': 'equity', 82 | 'purchase_date': '2020-01-31', 83 | 'purchase_price': 4.00, 84 | 'quantity': 2, 85 | 'symbol': 'TSLA' 86 | } 87 | } 88 | """ 89 | 90 | if isinstance(positions, list): 91 | 92 | # Loop through each position. 93 | for position in positions: 94 | 95 | # Add the position. 96 | self.add_position( 97 | symbol=position['symbol'], 98 | asset_type=position['asset_type'], 99 | quantity=position.get('quantity', 0), 100 | purchase_price=position.get('purchase_price', 0.0), 101 | purchase_date=position.get('purchase_date', None) 102 | ) 103 | 104 | return self.positions 105 | 106 | else: 107 | raise TypeError('Positions must be a list of dictionaries.') 108 | 109 | def add_position(self, symbol: str, asset_type: str, purchase_date: Optional[str] = None, quantity: int = 0, purchase_price: float = 0.0) -> dict: 110 | """Adds a single new position to the the portfolio. 111 | 112 | Arguments: 113 | ---- 114 | symbol {str} -- The Symbol of the Financial Instrument. Example: 'AAPL' or '/ES' 115 | 116 | asset_type {str} -- The type of the financial instrument to be added. For example, 117 | 'equity', 'forex', 'option', 'futures' 118 | 119 | Keyword Arguments: 120 | ---- 121 | quantity {int} -- The number of shares or contracts you own. (default: {0}) 122 | 123 | purchase_price {float} -- The price at which the position was purchased. (default: {0.00}) 124 | 125 | purchase_date {str} -- The date which the asset was purchased. Must be ISO Format "YYYY-MM-DD" 126 | For example, "2020-04-01" (default: {None}) 127 | 128 | Returns: 129 | ---- 130 | {dict} -- A dictionary object that represents a position in the portfolio. 131 | 132 | Usage: 133 | ---- 134 | >>> portfolio = Portfolio() 135 | >>> new_position = Portfolio.add_position(symbol='MSFT', 136 | asset_type='equity', 137 | quantity=2, 138 | purchase_price=4.00, 139 | purchase_date="2020-01-31" 140 | ) 141 | >>> new_position 142 | { 143 | 'asset_type': 'equity', 144 | 'quantity': 2, 145 | 'purchase_price': 4.00, 146 | 'symbol': 'MSFT', 147 | 'purchase_date': '2020-01-31' 148 | } 149 | """ 150 | 151 | self.positions[symbol] = {} 152 | self.positions[symbol]['symbol'] = symbol 153 | self.positions[symbol]['quantity'] = quantity 154 | self.positions[symbol]['purchase_price'] = purchase_price 155 | self.positions[symbol]['purchase_date'] = purchase_date 156 | self.positions[symbol]['asset_type'] = asset_type 157 | 158 | if purchase_date: 159 | self.positions[symbol]['ownership_status'] = True 160 | else: 161 | self.positions[symbol]['ownership_status'] = False 162 | 163 | return self.positions[symbol] 164 | 165 | def remove_position(self, symbol: str) -> Tuple[bool, str]: 166 | """Deletes a single position from the portfolio. 167 | 168 | Arguments: 169 | ---- 170 | symbol {str} -- The symbol of the instrument to be deleted. Example: 'AAPL' or '/ES' 171 | 172 | Returns: 173 | ---- 174 | {Tuple[bool, str]} -- Returns `True` if successfully deleted, `False` otherwise 175 | along with a message. 176 | 177 | Usage: 178 | ---- 179 | >>> portfolio = Portfolio() 180 | 181 | >>> new_position = Portfolio.add_position( 182 | symbol='MSFT', 183 | asset_type='equity', 184 | quantity=2, 185 | purchase_price=4.00, 186 | purchase_date="2020-01-31" 187 | ) 188 | >>> delete_status = Portfolio.delete_position(symbol='MSFT') 189 | >>> delete_status 190 | (True, 'MSFT was successfully removed.') 191 | 192 | >>> delete_status = Portfolio.delete_position(symbol='AAPL') 193 | >>> delete_status 194 | (False, 'AAPL did not exist in the porfolio.') 195 | """ 196 | 197 | if symbol in self.positions: 198 | del self.positions[symbol] 199 | return (True, "{symbol} was successfully removed.".format(symbol=symbol)) 200 | else: 201 | return (False, "{symbol} did not exist in the porfolio.".format(symbol=symbol)) 202 | 203 | def total_allocation(self) -> dict: 204 | """Returns a summary of the portfolio by asset allocation.""" 205 | 206 | total_allocation = { 207 | 'stocks': [], 208 | 'fixed_income': [], 209 | 'options': [], 210 | 'futures': [], 211 | 'furex': [] 212 | } 213 | 214 | if len(self.positions.keys()) > 0: 215 | for symbol in self.positions: 216 | total_allocation[self.positions[symbol]['asset_type']].append(self.positions[symbol]) 217 | 218 | def portfolio_variance(self, weights: dict, covariance_matrix: DataFrame) -> dict: 219 | 220 | sorted_keys = list(weights.keys()) 221 | sorted_keys.sort() 222 | 223 | sorted_weights = np.array([weights[symbol] for symbol in sorted_keys]) 224 | portfolio_variance = np.dot( 225 | sorted_weights.T, 226 | np.dot(covariance_matrix, sorted_weights) 227 | ) 228 | 229 | return portfolio_variance 230 | 231 | def portfolio_metrics(self) -> dict: 232 | """Calculates different portfolio risk metrics using daily data. 233 | 234 | Overview: 235 | ---- 236 | To build an effective summary of our portfolio we will need to 237 | calculate different metrics that help represent the risk of our 238 | portfolio and it's performance. The following metrics will be calculated 239 | in this method: 240 | 241 | 1. Standard Deviation of Percent Returns. 242 | 2. Covariance of Percent Returns. 243 | 2. Variance of Percent Returns. 244 | 3. Average Percent Return 245 | 4. Weighted Average Percent Return. 246 | 5. Portfolio Variance. 247 | 248 | Returns: 249 | ---- 250 | dict -- [description] 251 | """ 252 | 253 | if not self._stock_frame_daily: 254 | self._grab_daily_historical_prices() 255 | 256 | # Calculate the weights. 257 | porftolio_weights = self.portfolio_weights() 258 | 259 | # Calculate the Daily Returns (%) 260 | self._stock_frame_daily.frame['daily_returns_pct'] = self._stock_frame_daily.symbol_groups['close'].transform( 261 | lambda x: x.pct_change() 262 | ) 263 | 264 | # Calculate the Daily Returns (Mean) 265 | self._stock_frame_daily.frame['daily_returns_avg'] = self._stock_frame_daily.symbol_groups['daily_returns_pct'].transform( 266 | lambda x: x.mean() 267 | ) 268 | 269 | # Calculate the Daily Returns (Standard Deviation) 270 | self._stock_frame_daily.frame['daily_returns_std'] = self._stock_frame_daily.symbol_groups['daily_returns_pct'].transform( 271 | lambda x: x.std() 272 | ) 273 | 274 | # Calculate the Covariance. 275 | returns_cov = self._stock_frame_daily.frame.unstack( 276 | level=0)['daily_returns_pct'].cov() 277 | 278 | # Take the other columns and get ready to add them to our dictionary. 279 | returns_avg = self._stock_frame_daily.symbol_groups['daily_returns_avg'].tail( 280 | n=1 281 | ).to_dict() 282 | 283 | returns_std = self._stock_frame_daily.symbol_groups['daily_returns_std'].tail( 284 | n=1 285 | ).to_dict() 286 | 287 | metrics_dict = {} 288 | 289 | portfolio_variance = self.portfolio_variance( 290 | weights=porftolio_weights, 291 | covariance_matrix=returns_cov 292 | ) 293 | 294 | for index_tuple in returns_std: 295 | 296 | symbol = index_tuple[0] 297 | metrics_dict[symbol] = {} 298 | metrics_dict[symbol]['weight'] = porftolio_weights[symbol] 299 | metrics_dict[symbol]['average_returns'] = returns_avg[index_tuple] 300 | metrics_dict[symbol]['weighted_returns'] = returns_avg[index_tuple] * \ 301 | metrics_dict[symbol]['weight'] 302 | metrics_dict[symbol]['standard_deviation_of_returns'] = returns_std[index_tuple] 303 | metrics_dict[symbol]['variance_of_returns'] = returns_std[index_tuple] ** 2 304 | metrics_dict[symbol]['covariance_of_returns'] = returns_cov.loc[[ 305 | symbol]].to_dict() 306 | 307 | metrics_dict['portfolio'] = {} 308 | metrics_dict['portfolio']['variance'] = portfolio_variance 309 | 310 | return metrics_dict 311 | 312 | def portfolio_weights(self) -> dict: 313 | """Calculate the weights for each position in the portfolio 314 | 315 | Returns: 316 | ---- 317 | {dict} -- Each symbol with their designated weights. 318 | """ 319 | 320 | weights = {} 321 | 322 | # First grab all the symbols. 323 | symbols = self.positions.keys() 324 | 325 | # Grab the quotes. 326 | quotes = self.td_client.get_quotes(instruments=list(symbols)) 327 | 328 | # Grab the projected market value. 329 | projected_market_value_dict = self.projected_market_value( 330 | current_prices=quotes 331 | ) 332 | 333 | # Loop through each symbol. 334 | for symbol in projected_market_value_dict: 335 | 336 | # Calculate the weights. 337 | if symbol != 'total': 338 | weights[symbol] = projected_market_value_dict[symbol]['total_market_value'] / \ 339 | projected_market_value_dict['total']['total_market_value'] 340 | 341 | return weights 342 | 343 | def portfolio_summary(self): 344 | """Generates a summary of our portfolio.""" 345 | 346 | # First grab all the symbols. 347 | symbols = self.positions.keys() 348 | 349 | # Grab the quotes. 350 | quotes = self.td_client.get_quotes(instruments=list(symbols)) 351 | 352 | portfolio_summary_dict = {} 353 | portfolio_summary_dict['projected_market_value'] = self.projected_market_value( 354 | current_prices=quotes 355 | ) 356 | portfolio_summary_dict['portfolio_weights'] = self.portfolio_weights() 357 | portfolio_summary_dict['portfolio_risk'] = "" 358 | 359 | return portfolio_summary_dict 360 | 361 | def in_portfolio(self, symbol: str) -> bool: 362 | """checks if the symbol is in the portfolio. 363 | 364 | Arguments: 365 | ---- 366 | symbol {str} -- The symbol of the instrument to be deleted. Example: 'AAPL' or '/ES' 367 | 368 | Returns: 369 | ---- 370 | bool -- `True` if the position is in the portfolio, `False` otherwise. 371 | 372 | Usage: 373 | ---- 374 | >>> portfolio = Portfolio() 375 | >>> new_position = Portfolio.add_position( 376 | symbol='MSFT', 377 | asset_type='equity' 378 | ) 379 | >>> in_position_flag = Portfolio.in_portfolio(symbol='MSFT') 380 | >>> in_position_flag 381 | True 382 | """ 383 | 384 | if symbol in self.positions: 385 | return True 386 | else: 387 | return False 388 | 389 | def get_ownership_status(self, symbol: str) -> bool: 390 | """Gets the ownership status for a position in the portfolio. 391 | 392 | Arguments: 393 | ---- 394 | symbol {str} -- The symbol you want to grab the ownership status for. 395 | 396 | Returns: 397 | ---- 398 | {bool} -- `True` if the we own the position, `False` if we do not own it. 399 | """ 400 | 401 | if self.in_portfolio(symbol=symbol) and self.positions[symbol]['ownership_status']: 402 | return self.positions[symbol]['ownership_status'] 403 | else: 404 | return False 405 | 406 | def set_ownership_status(self, symbol: str, ownership: bool) -> None: 407 | """Sets the ownership status for a position in the portfolio. 408 | 409 | Arguments: 410 | ---- 411 | symbol {str} -- The symbol you want to change the ownership status for. 412 | 413 | ownership {bool} -- The ownership status you want the symbol to have. Can either 414 | be `True` or `False`. 415 | 416 | Raises: 417 | ---- 418 | KeyError: If the symbol does not exist in the portfolio it will return an error. 419 | """ 420 | 421 | if self.in_portfolio(symbol=symbol): 422 | self.positions[symbol]['ownership_status'] = ownership 423 | else: 424 | raise KeyError( 425 | "Can't set ownership status, as you do not have the symbol in your portfolio." 426 | ) 427 | 428 | def is_profitable(self, symbol: str, current_price: float) -> bool: 429 | """Specifies whether a position is profitable. 430 | 431 | Arguments: 432 | ---- 433 | symbol {str} -- The symbol of the instrument, to check profitability. 434 | 435 | current_price {float} -- The current trading price of the instrument. 436 | 437 | Returns: 438 | ---- 439 | {bool} -- Specifies whether the position is profitable or flat `True` or not 440 | profitable `False`. 441 | 442 | Raises: 443 | ---- 444 | KeyError: If the Symbol does not exist it will return a key error. 445 | 446 | Usage: 447 | ---- 448 | >>> portfolio = Portfolio() 449 | >>> new_position = Portfolio.add_position( 450 | symbol='MSFT', 451 | asset_type='equity', 452 | purchase_price=4.00, 453 | purchase_date="2020-01-31" 454 | ) 455 | >>> is_profitable_flag = Portfolio.is_profitable( 456 | symbol='MSFT', 457 | current_price=7.00 458 | ) 459 | >>> is_profitable_flag 460 | True 461 | """ 462 | 463 | # Grab the purchase price, if it exists. 464 | if self.in_portfolio(symbol=symbol): 465 | purchase_price = self.positions[symbol]['purchase_price'] 466 | else: 467 | raise KeyError("The Symbol you tried to request does not exist.") 468 | 469 | if (purchase_price <= current_price): 470 | return True 471 | elif (purchase_price > current_price): 472 | return False 473 | 474 | def projected_market_value(self, current_prices: dict) -> dict: 475 | """Returns the Projected market value for all the positions in the portfolio. 476 | 477 | Arguments: 478 | ---- 479 | current_prices {dict} -- A dictionary of current quotes for each of the symbols 480 | in the portfolio. 481 | 482 | Returns: 483 | ---- 484 | dict -- A summarized version of the portfolio with each position, purchase price, current price, 485 | and projected values. 486 | 487 | Usage: 488 | ---- 489 | >>> portfolio = Portfolio() 490 | >>> new_position = portfolio.add_position( 491 | symbol='MSFT', 492 | asset_type='equity', 493 | purchase_price=4.00, 494 | purchase_date="2020-01-31" 495 | ) 496 | >>> portfolio_summary = portfolio.projected_market_value(current_prices={'MSFT':{'lastPrice': 8.00, 'openPrice': 7.50}}) 497 | """ 498 | 499 | projected_value = {} 500 | total_value = 0.0 501 | total_invested_capital = 0.0 502 | total_profit_or_loss = 0.0 503 | 504 | position_count_profitable = 0 505 | position_count_not_profitable = 0 506 | position_count_break_even = 0 507 | 508 | for symbol in current_prices: 509 | 510 | if self.in_portfolio(symbol=symbol): 511 | 512 | projected_value[symbol] = {} 513 | current_quantity = self.positions[symbol]['quantity'] 514 | purchase_price = self.positions[symbol]['purchase_price'] 515 | current_price = current_prices[symbol]['lastPrice'] 516 | is_profitable = self.is_profitable( 517 | symbol=symbol, current_price=current_price) 518 | 519 | projected_value[symbol]['purchase_price'] = purchase_price 520 | projected_value[symbol]['current_price'] = current_prices[symbol]['lastPrice'] 521 | projected_value[symbol]['quantity'] = current_quantity 522 | projected_value[symbol]['is_profitable'] = is_profitable 523 | 524 | # Calculate total market value. 525 | projected_value[symbol]['total_market_value'] = ( 526 | current_price * current_quantity 527 | ) 528 | 529 | # Calculate total invested capital. 530 | projected_value[symbol]['total_invested_capital'] = ( 531 | current_quantity * purchase_price 532 | ) 533 | 534 | projected_value[symbol]['total_loss_or_gain_$'] = ((current_price - purchase_price) * current_quantity) 535 | projected_value[symbol]['total_loss_or_gain_%'] = round(((current_price - purchase_price) / purchase_price), 4) 536 | 537 | total_value += projected_value[symbol]['total_market_value'] 538 | total_profit_or_loss += projected_value[symbol]['total_loss_or_gain_$'] 539 | total_invested_capital += projected_value[symbol]['total_invested_capital'] 540 | 541 | if projected_value[symbol]['total_loss_or_gain_$'] > 0: 542 | position_count_profitable += 1 543 | elif projected_value[symbol]['total_loss_or_gain_$'] < 0: 544 | position_count_not_profitable += 1 545 | else: 546 | position_count_break_even += 1 547 | 548 | projected_value['total'] = {} 549 | projected_value['total']['total_positions'] = len(self.positions) 550 | projected_value['total']['total_market_value'] = total_value 551 | projected_value['total']['total_invested_capital'] = total_invested_capital 552 | projected_value['total']['total_profit_or_loss'] = total_profit_or_loss 553 | projected_value['total']['number_of_profitable_positions'] = position_count_profitable 554 | projected_value['total']['number_of_non_profitable_positions'] = position_count_not_profitable 555 | projected_value['total']['number_of_breakeven_positions'] = position_count_break_even 556 | 557 | return projected_value 558 | 559 | @property 560 | def historical_prices(self) -> List[dict]: 561 | """Gets the historical prices for the Portfolio 562 | 563 | Returns: 564 | ---- 565 | List[dict] -- A list of historical candle prices. 566 | """ 567 | 568 | return self._historical_prices 569 | 570 | @historical_prices.setter 571 | def historical_prices(self, historical_prices: List[dict]) -> None: 572 | """Sets the historical prices for the Portfolio 573 | 574 | Arguments: 575 | ---- 576 | historical_prices {List[dict]} -- A list of historical candle prices. 577 | """ 578 | 579 | self._historical_prices = historical_prices 580 | 581 | @property 582 | def stock_frame(self) -> StockFrame: 583 | """Gets the StockFrame object for the Portfolio 584 | 585 | Returns: 586 | ---- 587 | {StockFrame} -- A StockFrame object with symbol groups, and rolling windows. 588 | """ 589 | 590 | return self._stock_frame 591 | 592 | @stock_frame.setter 593 | def stock_frame(self, stock_frame: StockFrame) -> None: 594 | """Sets the StockFrame object for the Portfolio 595 | 596 | Arguments: 597 | ---- 598 | stock_frame {StockFrame} -- A StockFrame object with symbol groups, and rolling windows. 599 | """ 600 | 601 | self._stock_frame = stock_frame 602 | 603 | @property 604 | def td_client(self) -> TDClient: 605 | """Gets the TDClient object for the Portfolio 606 | 607 | Returns: 608 | ---- 609 | {TDClient} -- An authenticated session with the TD API. 610 | """ 611 | 612 | return self._td_client 613 | 614 | @td_client.setter 615 | def td_client(self, td_client: TDClient) -> None: 616 | """Sets the TDClient object for the Portfolio 617 | 618 | Arguments: 619 | ---- 620 | td_client {TDClient} -- An authenticated session with the TD API. 621 | """ 622 | 623 | self._td_client: TDClient = td_client 624 | 625 | def _grab_daily_historical_prices(self) -> StockFrame: 626 | """Grabs the daily historical prices for each position. 627 | 628 | Returns: 629 | ---- 630 | {StockFrame} -- A StockFrame object with data organized, grouped, and sorted. 631 | """ 632 | 633 | new_prices = [] 634 | 635 | # Loop through each position. 636 | for symbol in self.positions: 637 | 638 | # Grab the historical prices. 639 | historical_prices_response = self.td_client.get_price_history( 640 | symbol=symbol, 641 | period_type='year', 642 | period=1, 643 | frequency_type='daily', 644 | frequency=1, 645 | extended_hours=True 646 | ) 647 | 648 | # Loop through the chandles. 649 | for candle in historical_prices_response['candles']: 650 | 651 | new_price_mini_dict = {} 652 | new_price_mini_dict['symbol'] = symbol 653 | new_price_mini_dict['open'] = candle['open'] 654 | new_price_mini_dict['close'] = candle['close'] 655 | new_price_mini_dict['high'] = candle['high'] 656 | new_price_mini_dict['low'] = candle['low'] 657 | new_price_mini_dict['volume'] = candle['volume'] 658 | new_price_mini_dict['datetime'] = candle['datetime'] 659 | new_prices.append(new_price_mini_dict) 660 | 661 | # Create and set the StockFrame 662 | self._stock_frame_daily = StockFrame(data=new_prices) 663 | self._stock_frame_daily.create_frame() 664 | 665 | return self._stock_frame_daily 666 | -------------------------------------------------------------------------------- /pyrobot/stock_frame.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | from typing import List 4 | from typing import Dict 5 | from typing import Union 6 | 7 | from pandas.core.groupby import DataFrameGroupBy 8 | from pandas.core.window import RollingGroupby 9 | from pandas.core.window import Window 10 | 11 | 12 | class StockFrame(): 13 | 14 | def __init__(self, data: List[Dict]) -> None: 15 | """Initalizes the Stock Data Frame Object. 16 | 17 | Arguments: 18 | ---- 19 | data {List[Dict]} -- The data to convert to a frame. Normally, this is 20 | returned from the historical prices endpoint. 21 | """ 22 | 23 | self._data = data 24 | self._frame: pd.DataFrame = self.create_frame() 25 | self._symbol_groups = None 26 | self._symbol_rolling_groups = None 27 | 28 | @property 29 | def frame(self) -> pd.DataFrame: 30 | """The frame object. 31 | 32 | Returns: 33 | ---- 34 | pd.DataFrame -- A pandas data frame with the price data. 35 | """ 36 | return self._frame 37 | 38 | @property 39 | def symbol_groups(self) -> DataFrameGroupBy: 40 | """Returns the Groups in the StockFrame. 41 | 42 | Overview: 43 | ---- 44 | Often we will want to apply operations to a each symbol. The 45 | `symbols_groups` property will return the dataframe grouped by 46 | each symbol. 47 | 48 | Returns: 49 | ---- 50 | {DataFrameGroupBy} -- A `pandas.core.groupby.GroupBy` object with each symbol. 51 | """ 52 | 53 | # Group by Symbol. 54 | self._symbol_groups: DataFrameGroupBy = self._frame.groupby( 55 | by='symbol', 56 | as_index=False, 57 | sort=True 58 | ) 59 | 60 | return self._symbol_groups 61 | 62 | def symbol_rolling_groups(self, size: int) -> RollingGroupby: 63 | """Grabs the windows for each group. 64 | 65 | Arguments: 66 | ---- 67 | size {int} -- The size of the window. 68 | 69 | Returns: 70 | ---- 71 | {RollingGroupby} -- A `pandas.core.window.RollingGroupby` object. 72 | """ 73 | 74 | # If we don't a symbols group, then create it. 75 | if not self._symbol_groups: 76 | self.symbol_groups 77 | 78 | self._symbol_rolling_groups: RollingGroupby = self._symbol_groups.rolling( 79 | size 80 | ) 81 | 82 | return self._symbol_rolling_groups 83 | 84 | def create_frame(self) -> pd.DataFrame: 85 | """Creates a new data frame with the data passed through. 86 | 87 | Returns: 88 | ---- 89 | {pd.DataFrame} -- A pandas dataframe. 90 | """ 91 | 92 | # Make a data frame. 93 | price_df = pd.DataFrame(data=self._data) 94 | price_df = self._parse_datetime_column(price_df=price_df) 95 | price_df = self._set_multi_index(price_df=price_df) 96 | 97 | return price_df 98 | 99 | def _parse_datetime_column(self, price_df: pd.DataFrame) -> pd.DataFrame: 100 | """Parses the datetime column passed through. 101 | 102 | Arguments: 103 | ---- 104 | price_df {pd.DataFrame} -- The price data frame with a 105 | datetime column. 106 | 107 | Returns: 108 | ---- 109 | {pd.DataFrame} -- A pandas dataframe. 110 | """ 111 | 112 | price_df['datetime'] = pd.to_datetime( 113 | price_df['datetime'], 114 | unit='ms', 115 | origin='unix' 116 | ) 117 | 118 | return price_df 119 | 120 | def _set_multi_index(self, price_df: pd.DataFrame) -> pd.DataFrame: 121 | """Converts the dataframe to a multi-index data frame. 122 | 123 | Arguments: 124 | ---- 125 | price_df {pd.DataFrame} -- The price data frame. 126 | 127 | Returns: 128 | ---- 129 | pd.DataFrame -- A pandas dataframe. 130 | """ 131 | 132 | price_df = price_df.set_index(keys=['symbol', 'datetime']) 133 | 134 | return price_df 135 | 136 | def add_rows(self, data: Dict) -> None: 137 | """Adds a new row to our StockFrame. 138 | 139 | Arguments: 140 | ---- 141 | data {Dict} -- A list of quotes. 142 | 143 | Usage: 144 | ---- 145 | >>> # Create a StockFrame object. 146 | >>> stock_frame = trading_robot.create_stock_frame( 147 | data=historical_prices['aggregated'] 148 | ) 149 | >>> fake_data = { 150 | "datetime": 1586390396750, 151 | "symbol": "MSFT", 152 | "close": 165.7, 153 | "open": 165.67, 154 | "high": 166.67, 155 | "low": 163.5, 156 | "volume": 48318234 157 | } 158 | >>> # Add to the Stock Frame. 159 | >>> stock_frame.add_rows(data=fake_data) 160 | """ 161 | 162 | column_names = ['open', 'close', 'high', 'low', 'volume'] 163 | 164 | for quote in data: 165 | 166 | # Parse the Timestamp. 167 | time_stamp = pd.to_datetime( 168 | quote['datetime'], 169 | unit='ms', 170 | origin='unix' 171 | ) 172 | 173 | # Define the Index Tuple. 174 | row_id = (quote['symbol'], time_stamp) 175 | 176 | # Define the values. 177 | row_values = [ 178 | quote['open'], 179 | quote['close'], 180 | quote['high'], 181 | quote['low'], 182 | quote['volume'] 183 | ] 184 | 185 | # Create a new row. 186 | new_row = pd.Series(data=row_values) 187 | 188 | # Add the row. 189 | self.frame.loc[row_id, column_names] = new_row.values 190 | 191 | self.frame.sort_index(inplace=True) 192 | 193 | def do_indicator_exist(self, column_names: List[str]) -> bool: 194 | """Checks to see if the indicator columns specified exist. 195 | 196 | Overview: 197 | ---- 198 | The user can add multiple indicator columns to their StockFrame object 199 | and in some cases we will need to modify those columns before making trades. 200 | In those situations, this method, will help us check if those columns exist 201 | before proceeding on in the code. 202 | 203 | Arguments: 204 | ---- 205 | column_names {List[str]} -- A list of column names that will be checked. 206 | 207 | Raises: 208 | ---- 209 | KeyError: If a column is not found in the StockFrame, a KeyError will be raised. 210 | 211 | Returns: 212 | ---- 213 | bool -- `True` if all the columns exist. 214 | """ 215 | 216 | if set(column_names).issubset(self._frame.columns): 217 | return True 218 | else: 219 | raise KeyError("The following indicator columns are missing from the StockFrame: {missing_columns}".format( 220 | missing_columns=set(column_names).difference( 221 | self._frame.columns) 222 | )) 223 | 224 | def _check_signals(self, indicators: dict, indciators_comp_key: List[str], indicators_key: List[str]) -> Union[pd.DataFrame, None]: 225 | """Returns the last row of the StockFrame if conditions are met. 226 | 227 | Overview: 228 | ---- 229 | Before a trade is executed, we must check to make sure if the 230 | conditions that warrant a `buy` or `sell` signal are met. This 231 | method will take last row for each symbol in the StockFrame and 232 | compare the indicator column values with the conditions specified 233 | by the user. 234 | 235 | If the conditions are met the row will be returned back to the user. 236 | 237 | Arguments: 238 | ---- 239 | indicators {dict} -- A dictionary containing all the indicators to be checked 240 | along with their buy and sell criteria. 241 | 242 | indicators_comp_key List[str] -- A list of the indicators where we are comparing 243 | one indicator to another indicator. 244 | 245 | indicators_key List[str] -- A list of the indicators where we are comparing 246 | one indicator to a numerical value. 247 | 248 | Returns: 249 | ---- 250 | {Union[pd.DataFrame, None]} -- If signals are generated then, a pandas.DataFrame object 251 | will be returned. If no signals are found then nothing will be returned. 252 | """ 253 | 254 | # Grab the last rows. 255 | last_rows = self._symbol_groups.tail(1) 256 | 257 | # Define a list of conditions. 258 | conditions = {} 259 | 260 | # Check to see if all the columns exist. 261 | if self.do_indicator_exist(column_names=indicators_key): 262 | 263 | for indicator in indicators_key: 264 | 265 | column = last_rows[indicator] 266 | 267 | # Grab the Buy & Sell Condition. 268 | buy_condition_target = indicators[indicator]['buy'] 269 | sell_condition_target = indicators[indicator]['sell'] 270 | 271 | buy_condition_operator = indicators[indicator]['buy_operator'] 272 | sell_condition_operator = indicators[indicator]['sell_operator'] 273 | 274 | condition_1: pd.Series = buy_condition_operator( 275 | column, buy_condition_target 276 | ) 277 | condition_2: pd.Series = sell_condition_operator( 278 | column, sell_condition_target 279 | ) 280 | 281 | condition_1 = condition_1.where(lambda x: x == True).dropna() 282 | condition_2 = condition_2.where(lambda x: x == True).dropna() 283 | 284 | conditions['buys'] = condition_1 285 | conditions['sells'] = condition_2 286 | 287 | # Store the indicators in a list. 288 | check_indicators = [] 289 | 290 | # Split the name so we can check if the indicator exist. 291 | for indicator in indciators_comp_key: 292 | parts = indicator.split('_comp_') 293 | check_indicators += parts 294 | 295 | if self.do_indicator_exist(column_names=check_indicators): 296 | 297 | for indicator in indciators_comp_key: 298 | 299 | # Split the indicators. 300 | parts = indicator.split('_comp_') 301 | 302 | # Grab the indicators that need to be compared. 303 | indicator_1 = last_rows[parts[0]] 304 | indicator_2 = last_rows[parts[1]] 305 | 306 | # If we have a buy operator, grab it. 307 | if indicators[indicator]['buy_operator']: 308 | 309 | # Grab the Buy Operator. 310 | buy_condition_operator = indicators[indicator]['buy_operator'] 311 | 312 | # Grab the Condition. 313 | condition_1: pd.Series = buy_condition_operator( 314 | indicator_1, indicator_2 315 | ) 316 | 317 | # Keep the one's that aren't null. 318 | condition_1 = condition_1.where(lambda x: x == True).dropna() 319 | 320 | # Add it as a buy signal. 321 | conditions['buys'] = condition_1 322 | 323 | # If we have a sell operator, grab it. 324 | if indicators[indicator]['sell_operator']: 325 | 326 | # Grab the Sell Operator. 327 | sell_condition_operator = indicators[indicator]['sell_operator'] 328 | 329 | # Store it in a Pd.Series. 330 | condition_2: pd.Series = sell_condition_operator( 331 | indicator_1, indicator_2 332 | ) 333 | 334 | # keep the one's that aren't null. 335 | condition_2 = condition_2.where(lambda x: x == True).dropna() 336 | 337 | # Add it as a sell signal. 338 | conditions['sells'] = condition_2 339 | 340 | return conditions 341 | 342 | def grab_current_bar(self, symbol: str) -> pd.Series: 343 | """Grabs the current trading bar. 344 | 345 | ### Parameters 346 | ---------- 347 | symbol : str 348 | The symbol to grab the latest 349 | bar for. 350 | 351 | ### Returns 352 | ------- 353 | pd.Series 354 | A candle bar, represented as a 355 | pandas series object. 356 | """ 357 | 358 | # Filter the Stock Frame. 359 | bars_filtered = self._frame.filter(like=symbol, axis=0) 360 | bars = bars_filtered.tail(1) 361 | 362 | return bars 363 | 364 | def grab_n_bars_ago(self, symbol: str, n: int) -> pd.Series: 365 | """Grabs the current trading bar. 366 | 367 | ### Parameters 368 | ---------- 369 | symbol : str 370 | The symbol to grab the latest 371 | bar for. 372 | 373 | n : str 374 | The number of bars to look back. 375 | 376 | ### Returns 377 | ------- 378 | pd.Series 379 | A candle bar, represented as a 380 | pandas series object. 381 | """ 382 | 383 | # Filter the Stock Frame. 384 | bars_filtered = self._frame.filter(like=symbol, axis=0) 385 | bars = bars_filtered.iloc[-n] 386 | 387 | return bars -------------------------------------------------------------------------------- /pyrobot/trades.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | from typing import List 5 | from typing import Dict 6 | 7 | from td.client import TDClient 8 | 9 | 10 | class Trade(): 11 | 12 | """ 13 | Object Type: 14 | ---- 15 | `pyrobot.Trade` 16 | 17 | Overview: 18 | ---- 19 | Reprsents the Trade Object which is used to create new trades, 20 | add customizations to them, and easily modify existing content. 21 | """ 22 | 23 | def __init__(self): 24 | """Initalizes a new order.""" 25 | 26 | self.order = {} 27 | self.trade_id = "" 28 | self.order_id = "" 29 | self.account = "" 30 | self.order_status = "NOT_PLACED" 31 | 32 | self.side = "" 33 | self.side_opposite = "" 34 | self.enter_or_exit = "" 35 | self.enter_or_exit_opposite = "" 36 | 37 | self._order_response = {} 38 | self._triggered_added = False 39 | self._multi_leg = False 40 | self._one_cancels_other = False 41 | self._td_client: TDClient = None 42 | 43 | def to_dict(self) -> dict: 44 | 45 | # Initialize the Dict. 46 | obj_dict = { 47 | "__class___": self.__class__.__name__, 48 | "__module___": self.__module__ 49 | } 50 | 51 | # Add the Object. 52 | obj_dict.update(self.__dict__) 53 | 54 | return obj_dict 55 | 56 | def new_trade(self, trade_id: str, order_type: str, side: str, enter_or_exit: str, price: float = 0.00, stop_limit_price: float = 0.00) -> dict: 57 | """Creates a new Trade object template. 58 | 59 | A trade object is a template that can be used to help build complex trades 60 | that normally are prone to errors when writing the JSON. Additionally, it 61 | will help the process of storing trades easier. 62 | 63 | Arguments: 64 | ---- 65 | order_type {str} -- The type of order you would like to create. Can be 66 | one of the following: ['mkt', 'lmt', 'stop', 'stop_lmt', 'trailing_stop'] 67 | 68 | side {str} -- The side the trade will take, can be one of the 69 | following: ['long', 'short'] 70 | 71 | enter_or_exit {str} -- Specifices whether this trade will enter a new position 72 | or exit an existing position. If used to enter then specify, 'enter'. If 73 | used to exit a trade specify, 'exit'. 74 | 75 | Returns: 76 | ---- 77 | {dict} -- [description] 78 | """ 79 | 80 | self.trade_id = trade_id 81 | 82 | self.order_types = { 83 | 'mkt': 'MARKET', 84 | 'lmt': 'LIMIT', 85 | 'stop': 'STOP', 86 | 'stop_lmt': 'STOP_LIMIT', 87 | 'trailing_stop': 'TRAILING_STOP' 88 | } 89 | 90 | self.order_instructions = { 91 | 'enter': { 92 | 'long': 'BUY', 93 | 'short': 'SELL_SHORT' 94 | }, 95 | 'exit': { 96 | 'long': 'SELL', 97 | 'short': 'BUY_TO_COVER' 98 | } 99 | } 100 | 101 | self.order = { 102 | "orderStrategyType": "SINGLE", 103 | "orderType": self.order_types[order_type], 104 | "session": "NORMAL", 105 | "duration": "DAY", 106 | "orderLegCollection": [ 107 | { 108 | "instruction": self.order_instructions[enter_or_exit][side], 109 | "quantity": 0, 110 | "instrument": { 111 | "symbol": None, 112 | "assetType": None 113 | } 114 | } 115 | ] 116 | } 117 | 118 | if self.order['orderType'] == 'STOP': 119 | self.order['stopPrice'] = price 120 | 121 | elif self.order['orderType'] == 'LIMIT': 122 | self.order['price'] = price 123 | 124 | elif self.order['orderType'] == 'STOP_LIMIT': 125 | self.order['price'] = stop_limit_price 126 | self.order['stopPrice'] = price 127 | 128 | elif self.order['orderType'] == 'TRAILING_STOP': 129 | self.order['stopPriceLinkBasis'] = "" 130 | self.order['stopPriceLinkType'] = "" 131 | self.order['stopPriceOffset'] = 0.00 132 | self.order['stopType'] = 'STANDARD' 133 | 134 | # Make a refrence to the side we take, useful when adding other components. 135 | self.enter_or_exit = enter_or_exit 136 | self.side = side 137 | self.order_type = order_type 138 | self.price = price 139 | 140 | # If it's a stop limit order or stop order, set the stop price. 141 | if self.is_stop_order or self.is_stop_limit_order: 142 | self.stop_price = price 143 | else: 144 | self.stop_price = 0.0 145 | 146 | # If it's a stop limit order set the stop limit price. 147 | if self.is_stop_limit_order: 148 | self.stop_limit_price = stop_limit_price 149 | else: 150 | self.stop_limit_price = 0.0 151 | 152 | # If it's a limit price set the limit price. 153 | if self.is_limit_order: 154 | self.limit_price = price 155 | else: 156 | self.limit_price = 0.0 157 | 158 | # Set the enter or exit state. 159 | if self.enter_or_exit == 'enter': 160 | self.enter_or_exit_opposite = 'exit' 161 | if self.enter_or_exit == 'exit': 162 | self.enter_or_exit_opposite = 'enter' 163 | 164 | # Set the side state. 165 | if self.side == 'long': 166 | self.side_opposite = 'short' 167 | if self.side == 'short': 168 | self.side_opposite = 'long' 169 | 170 | return self.order 171 | 172 | def instrument(self, symbol: str, quantity: int, asset_type: str, sub_asset_type: str = None, order_leg_id: int = 0) -> dict: 173 | """Adds an instrument to a trade. 174 | 175 | Arguments: 176 | ---- 177 | symbol {str} -- The instrument ticker symbol. 178 | 179 | quantity {int} -- The quantity of shares to be purchased. 180 | 181 | asset_type {str} -- The instrument asset type. For example, `EQUITY`. 182 | 183 | Keyword Arguments: 184 | ---- 185 | sub_asset_type {str} -- The instrument sub-asset type, not always needed. For example, `ETF`. (default: {None}) 186 | 187 | Returns: 188 | ---- 189 | {dict} -- A dictionary with the instrument. 190 | """ 191 | 192 | leg = self.order['orderLegCollection'][order_leg_id] 193 | 194 | leg['instrument']['symbol'] = symbol 195 | leg['instrument']['assetType'] = asset_type 196 | leg['quantity'] = quantity 197 | 198 | self.order_size = quantity 199 | self.symbol = symbol 200 | self.asset_type = asset_type 201 | 202 | return leg 203 | 204 | def add_option_instrument(self, symbol: str, quantity: int, order_leg_id: int = 0) -> dict: 205 | """Adds an Option instrument to the Trade object. 206 | 207 | Args: 208 | ---- 209 | symbol (str): The option symbol to be added. 210 | 211 | quantity (int): The number of option contracts to purchase or sell. 212 | 213 | order_leg_id (int, optional): The position of the instrument within the 214 | the Order Leg Collection.. Defaults to 0. 215 | 216 | Returns: 217 | ---- 218 | dict: The order leg containing the option contract. 219 | """ 220 | 221 | self.instrument( 222 | symbol=symbol, 223 | quantity=quantity, 224 | asset_type='OPTION', 225 | order_leg_id=order_leg_id 226 | ) 227 | 228 | leg = self.order['orderLegCollection'][order_leg_id] 229 | 230 | return leg 231 | 232 | def good_till_cancel(self, cancel_time: datetime) -> None: 233 | """Converts an order to a `Good Till Cancel` order. 234 | 235 | Arguments: 236 | ---- 237 | cancel_time {datetime.datetime} -- A datetime object representing the 238 | cancel time of the order. 239 | """ 240 | 241 | self.order['duration'] = 'GOOD_TILL_CANCEL' 242 | self.order['cancelTime'] = cancel_time.isoformat() 243 | 244 | def modify_side(self, side: str, leg_id: int = 0) -> None: 245 | """Modifies the Side the order takes. 246 | 247 | Arguments: 248 | ---- 249 | side {str} -- The side to be set. Can be one of the following: 250 | `['buy', 'sell', 'sell_short', 'buy_to_cover']`. 251 | 252 | Keyword Arguments: 253 | ---- 254 | leg_id {int} -- The leg you want to adjust. (default: {0}) 255 | 256 | Raises: 257 | ---- 258 | ValueError -- If the `side` argument does not match one of the valid sides, 259 | then a ValueError will be raised. 260 | """ 261 | 262 | # Validate the Side. 263 | if side and side not in ['buy', 'sell', 'sell_short', 'buy_to_cover', 'sell_to_close', 'buy_to_open']: 264 | raise ValueError( 265 | "The side you have specified is not valid. Please choose a valid side: ['buy', 'sell', 'sell_short', 'buy_to_cover','sell_to_close', 'buy_to_open']" 266 | ) 267 | 268 | # Set the Order. 269 | if side: 270 | self.order['orderLegCollection'][leg_id]['instruction'] = side.upper() 271 | else: 272 | self.order['orderLegCollection'][leg_id]['instruction'] = self.order_instructions[self.enter_or_exit][self.side_opposite] 273 | 274 | def add_box_range(self, profit_size: float = 0.00, stop_size: float = 0.00, 275 | stop_percentage: bool = False, profit_percentage: bool = False, 276 | stop_limit: bool = False, make_one_cancels_other: bool = True, 277 | limit_size: float = 0.00, limit_percentage: bool = False): 278 | """Adds a Stop Loss(or Stop-Limit order), and a limit Order 279 | 280 | Arguments: 281 | ---- 282 | profit_size {float} -- The size of desired profit. For example, `0.10`. 283 | 284 | profit_percentage {float} -- Specifies whether the `profit_size` is in absolute dollars `False` or 285 | in percentage terms `True`. 286 | 287 | stop_size {float} -- The size of desired stop loss. For example, `0.10`. 288 | 289 | stop_percentage {float} -- Specifies whether the `stop_size` is in absolute dollars `False` or 290 | in percentage terms `True`. 291 | 292 | Keyword Arguments: 293 | ---- 294 | stop_limit {bool} -- If `True` makes the stop-loss a stop-limit. (default: {False}) 295 | """ 296 | 297 | if not self._triggered_added: 298 | self._convert_to_trigger() 299 | 300 | # Add a take profit Limit order. 301 | self.add_take_profit( 302 | profit_size=profit_size, 303 | percentage=profit_percentage 304 | ) 305 | 306 | # Add a stop Loss Order. 307 | if not stop_limit: 308 | self.add_stop_loss( 309 | stop_size=profit_size, 310 | percentage=stop_percentage 311 | ) 312 | else: 313 | self.add_stop_limit( 314 | stop_size=profit_size, 315 | limit_size=limit_size, 316 | stop_percentage=stop_percentage, 317 | limit_percentage=limit_percentage 318 | ) 319 | 320 | if make_one_cancels_other: 321 | self.add_one_cancels_other() 322 | 323 | self.is_box_range = True 324 | 325 | def add_stop_loss(self, stop_size: float, percentage: bool = False) -> bool: 326 | """Add's a stop loss order to exit the position when a certain loss is reached. 327 | 328 | Arguments: 329 | ---- 330 | stop_size {float} -- The size of the stop from the current trading price. For example, `0.10`. 331 | 332 | Keyword Arguments: 333 | ---- 334 | percentage {bool} -- Specifies whether the `stop_size` adjustment is a 335 | `percentage` or an `absolute dollar amount`. If `True` will calculate the 336 | stop size as a percentage of the current price. (default: {False}) 337 | 338 | Returns: 339 | ---- 340 | {bool} -- `True` if the order was added. 341 | """ 342 | 343 | if not self._triggered_added: 344 | self._convert_to_trigger() 345 | 346 | price = self.grab_price() 347 | 348 | if percentage: 349 | adjustment = 1.0 - stop_size 350 | new_price = self._calculate_new_price( 351 | price=price, 352 | adjustment=adjustment, 353 | percentage=True 354 | ) 355 | else: 356 | adjustment = -stop_size 357 | new_price = self._calculate_new_price( 358 | price=price, 359 | adjustment=adjustment, 360 | percentage=False 361 | ) 362 | 363 | stop_loss_order = { 364 | "orderType": "STOP", 365 | "session": "NORMAL", 366 | "duration": "DAY", 367 | "stopPrice": new_price, 368 | "orderStrategyType": "SINGLE", 369 | "orderLegCollection": [ 370 | { 371 | "instruction": self.order_instructions[self.enter_or_exit_opposite][self.side], 372 | "quantity": self.order_size, 373 | "instrument": { 374 | "symbol": self.symbol, 375 | "assetType": self.asset_type 376 | } 377 | } 378 | ] 379 | } 380 | 381 | self.stop_loss_order = stop_loss_order 382 | self.order['childOrderStrategies'].append(self.stop_loss_order) 383 | 384 | return True 385 | 386 | def add_stop_limit(self, stop_size: float, limit_size: float, stop_percentage: bool = False, limit_percentage: bool = False): 387 | """Add's a Stop Limit Order to exit a trade when a stop price is reached but does not exceed the limit. 388 | 389 | Arguments: 390 | ---- 391 | stop_size {float} -- The size of the stop from the current trading price. For example, `0.10`. 392 | 393 | limit_size {float} -- The size of the limit from the current stop price. For example, `0.10`. 394 | 395 | Keyword Arguments: 396 | ---- 397 | stop_percentage {bool} -- Specifies whether the `stop_size` adjustment is a 398 | `percentage` or an `absolute dollar amount`. If `True` will calculate the 399 | stop size as a percentage of the current price. (default: {False}) 400 | 401 | limit_percentage {bool} -- Specifies whether the `limit_size` adjustment is a 402 | `percentage` or an `absolute dollar amount`. If `True` will calculate the 403 | limit size as a percentage of the current stop price. (default: {False}) 404 | 405 | Returns: 406 | ---- 407 | {bool} -- `True` if the order was added. 408 | """ 409 | 410 | # Check to see if there is a trigger. 411 | if not self._triggered_added: 412 | self._convert_to_trigger() 413 | 414 | price = self.grab_price() 415 | 416 | # Calculate the Stop Price. 417 | if stop_percentage: 418 | adjustment = 1.0 - stop_size 419 | stop_price = self._calculate_new_price( 420 | price=price, 421 | adjustment=adjustment, 422 | percentage=True 423 | ) 424 | else: 425 | adjustment = -stop_size 426 | stop_price = self._calculate_new_price( 427 | price=price, 428 | adjustment=adjustment, 429 | percentage=False 430 | ) 431 | 432 | # Calculate the Limit Price. 433 | if limit_percentage: 434 | adjustment = 1.0 - limit_size 435 | limit_price = self._calculate_new_price( 436 | price=price, 437 | adjustment=adjustment, 438 | percentage=True 439 | ) 440 | else: 441 | adjustment = -limit_size 442 | limit_price = self._calculate_new_price( 443 | price=price, 444 | adjustment=adjustment, 445 | percentage=False 446 | ) 447 | 448 | # Add the order. 449 | stop_limit_order = { 450 | "orderType": "STOP_LIMIT", 451 | "session": "NORMAL", 452 | "duration": "DAY", 453 | "price": limit_price, 454 | "stopPrice": stop_price, 455 | "orderStrategyType": "SINGLE", 456 | "orderLegCollection": [ 457 | { 458 | "instruction": self.order_instructions[self.enter_or_exit_opposite][self.side], 459 | "quantity": self.order_size, 460 | "instrument": { 461 | "symbol": self.symbol, 462 | "assetType": self.asset_type 463 | } 464 | } 465 | ] 466 | } 467 | 468 | self.stop_limit_order = stop_limit_order 469 | self.order['childOrderStrategies'].append(self.stop_limit_order) 470 | 471 | return True 472 | 473 | def _calculate_new_price(self, price: float, adjustment: float, percentage: bool) -> float: 474 | """Calculates an adjusted price given an old price. 475 | 476 | Arguments: 477 | ---- 478 | price {float} -- The original price. 479 | 480 | adjustment {float} -- The adjustment to be made to the new price. 481 | 482 | percentage {bool} -- Specifies whether the adjustment is a percentage adjustment `True` or 483 | an absolute dollar adjustment `False`. 484 | 485 | Returns: 486 | ---- 487 | {float} -- The new price after the adjustment has been made. 488 | """ 489 | 490 | if percentage: 491 | new_price = price * adjustment 492 | else: 493 | new_price = price + adjustment 494 | 495 | # For orders below $1.00, can only have 4 decimal places. 496 | if new_price < 1: 497 | new_price = round(new_price, 4) 498 | 499 | # For orders above $1.00, can only have 2 decimal places. 500 | else: 501 | new_price = round(new_price, 2) 502 | 503 | return new_price 504 | 505 | def grab_price(self) -> float: 506 | """Grabs the current price of the order. 507 | 508 | Returns 509 | ------- 510 | float 511 | The price rounded to 2 decimal places. 512 | """ 513 | 514 | # We need to basis to calculate off of. Use the price. 515 | if self.order_type == 'mkt': 516 | 517 | quote = self._td_client.get_quotes(instruments=[self.symbol]) 518 | 519 | # Have to make a call to Get Quotes. 520 | price = quote[self.symbol]['lastPrice'] 521 | 522 | elif self.order_type == 'lmt': 523 | price = self.price 524 | 525 | else: 526 | 527 | quote = self._td_client.get_quotes(instruments=[self.symbol]) 528 | 529 | # Have to make a call to Get Quotes. 530 | price = quote[self.symbol]['lastPrice'] 531 | 532 | return round(price, 2) 533 | 534 | def add_take_profit(self, profit_size: float, percentage: bool = False) -> bool: 535 | """Add's a Limit Order to exit a trade when a profit threshold is reached. 536 | 537 | Arguments: 538 | ---- 539 | profit_size {float} -- The size of the profit you want to make. For example, `0.10`. 540 | 541 | Keyword Arguments: 542 | ---- 543 | percentage {bool} -- Specifies whether the `profit_size` passed through is a 544 | `percentage` or an `absolute dollar amount`. If `True` will calculate the 545 | profit as a percentage of the current price. (default: {False}) 546 | 547 | Returns: 548 | ---- 549 | {bool} -- `True` if the order was added. 550 | """ 551 | 552 | # Check to see if we have a trigger order. 553 | if not self._triggered_added: 554 | self._convert_to_trigger() 555 | 556 | price = self.grab_price() 557 | 558 | # Calculate the new price. 559 | if percentage: 560 | adjustment = 1.0 + profit_size 561 | new_price = self._calculate_new_price( 562 | price=price, 563 | adjustment=adjustment, 564 | percentage=True 565 | ) 566 | else: 567 | adjustment = profit_size 568 | new_price = self._calculate_new_price( 569 | price=price, 570 | adjustment=adjustment, 571 | percentage=False 572 | ) 573 | 574 | # Build the order. 575 | take_profit_order = { 576 | "orderType": "LIMIT", 577 | "session": "NORMAL", 578 | "price": new_price, 579 | "duration": "DAY", 580 | "orderStrategyType": "SINGLE", 581 | "orderLegCollection": [ 582 | { 583 | "instruction": self.order_instructions[self.enter_or_exit_opposite][self.side], 584 | "quantity": self.order_size, 585 | "instrument": { 586 | "symbol": self.symbol, 587 | "assetType": self.asset_type 588 | } 589 | } 590 | ] 591 | } 592 | 593 | # Add the order. 594 | self.take_profit_order = take_profit_order 595 | self.order['childOrderStrategies'].append(self.take_profit_order) 596 | 597 | return True 598 | 599 | def add_one_cancels_other(self, orders: List[Dict] = None) -> Dict: 600 | """Add's a One Cancel's Other Order 601 | 602 | Arguments: 603 | ---- 604 | orders {List[Dict]} -- A list of two orders that will cancel each other 605 | if one of those orders are executed. 606 | 607 | Returns: 608 | ---- 609 | {Dict} -- A template that can be added to a Child Order Strategies list. 610 | """ 611 | 612 | # Define the OCO Template 613 | new_temp = [ 614 | { 615 | 'orderStrategyType': "OCO", 616 | 'childOrderStrategies':[] 617 | } 618 | ] 619 | 620 | # If we alread have a trigger than their are orders there. 621 | if self._triggered_added: 622 | 623 | # Grab the old ones. 624 | old_orders = self.order['childOrderStrategies'] 625 | 626 | # Add them to the template. 627 | new_temp[0]['childOrderStrategies'] = old_orders 628 | 629 | # Set the new child order strategy. 630 | self.order['childOrderStrategies'] = new_temp 631 | 632 | # Set it so we know it's a One Cancels Other. 633 | self._one_cancels_other = True 634 | 635 | def _convert_to_trigger(self): 636 | """Converts a regular order to a trigger order. 637 | 638 | Overview: 639 | ---- 640 | Trigger orders can be used to have a stop loss orders, or take profit 641 | orders placed right after the main order has been placed. This helps 642 | protect the order when possible and take profit when thresholds are 643 | reached. 644 | """ 645 | 646 | # Only convert to a trigger order, if it already isn't one. 647 | if self.order and not self._triggered_added: 648 | self.order['orderStrategyType'] = 'TRIGGER' 649 | 650 | # Trigger orders will have child strategies, so initalize that list. 651 | self.order['childOrderStrategies'] = [] 652 | 653 | # Update the state. 654 | self._triggered_added = True 655 | 656 | def modify_session(self, session: str) -> None: 657 | """Changes which session the order is for. 658 | 659 | Description 660 | ---- 661 | Orders are able to be active during different trading sessions. 662 | If you would like the order to be active during a different session, 663 | then choose one of the following: 664 | 665 | 1. 'am' - This is for pre-market hours. 666 | 2. 'pm' - This is for post-market hours. 667 | 3. 'normal' - This is for normal market hours. 668 | 4. 'seamless' - This makes the order active all of the sessions. 669 | 670 | Arguments: 671 | ---- 672 | session {str} -- The session you want the order to be active. Possible values 673 | are ['am', 'pm', 'normal', 'seamless'] 674 | """ 675 | 676 | if session in ['am', 'pm', 'normal', 'seamless']: 677 | self.order['session'] = session.upper() 678 | else: 679 | raise ValueError( 680 | 'Invalid session, choose either am, pm, normal, or seamless') 681 | 682 | @property 683 | def order_response(self) -> dict: 684 | """Returns the order response from submitting an order. 685 | 686 | Returns: 687 | ---- 688 | {dict} -- The order response dictionary. 689 | """ 690 | 691 | return self._order_response 692 | 693 | @order_response.setter 694 | def order_response(self, order_response_dict: dict) -> None: 695 | """Sets the order response from submitting an order. 696 | 697 | Arguments: 698 | ---- 699 | order_response_dict {dict} -- The order response dictionary. 700 | """ 701 | 702 | self._order_response = order_response_dict 703 | 704 | def _generate_order_id(self) -> str: 705 | """Generates an ID that can be used to identify the order. 706 | 707 | Returns: 708 | ---- 709 | {str} -- The order ID that was generated. 710 | """ 711 | 712 | # If we have an order, then generate it. 713 | if self.order: 714 | 715 | order_id = "{symbol}_{side}_{enter_or_exit}_{timestamp}" 716 | 717 | order_id = order_id.format( 718 | symbol=self.symbol, 719 | side=self.side, 720 | enter_or_exit=self.enter_or_exit, 721 | timestamp=datetime.now().timestamp() 722 | ) 723 | 724 | return order_id 725 | 726 | else: 727 | return "" 728 | 729 | def add_leg(self, order_leg_id: int, symbol: str, quantity: int, asset_type: str, sub_asset_type: str = None) -> List[dict]: 730 | """Adds an instrument to a trade. 731 | 732 | Arguments: 733 | ---- 734 | order_leg_id {int} -- The position you want the new leg to be in the leg collection. 735 | 736 | symbol {str} -- The instrument ticker symbol. 737 | 738 | quantity {int} -- The quantity of shares to be purchased. 739 | 740 | asset_type {str} -- The instrument asset type. For example, `EQUITY`. 741 | 742 | Keyword Arguments: 743 | ---- 744 | sub_asset_type {str} -- The instrument sub-asset type, not always needed. For example, `ETF`. (default: {None}) 745 | 746 | Returns: 747 | ---- 748 | {dict} -- The order's order leg collection. 749 | """ 750 | 751 | # Define the leg. 752 | leg = {} 753 | leg['instrument']['symbol'] = symbol 754 | leg['instrument']['assetType'] = asset_type 755 | leg['quantity'] = quantity 756 | 757 | if sub_asset_type: 758 | leg['instrument']['subAssetType'] = sub_asset_type 759 | 760 | # If 0, call instrument. 761 | if order_leg_id == 0: 762 | self.instrument( 763 | symbol=symbol, 764 | asset_type=asset_type, 765 | quantity=quantity, 766 | sub_asset_type=sub_asset_type, 767 | order_leg_id=0 768 | ) 769 | else: 770 | # Insert it. 771 | order_leg_colleciton: list = self.order['orderLegCollection'] 772 | order_leg_colleciton.insert(order_leg_id, leg) 773 | 774 | return self.order['orderLegCollection'] 775 | 776 | @property 777 | def number_of_legs(self) -> int: 778 | """Returns the number of legs in the Order Leg Collection. 779 | 780 | Returns: 781 | ---- 782 | int: The count of legs in the collection. 783 | """ 784 | 785 | return len(self.order['orderLegCollection']) 786 | 787 | def modify_price(self, new_price: float, price_type: str) -> None: 788 | """Used to change the price that is specified. 789 | 790 | Arguments: 791 | ---- 792 | new_price (float): The new price to be set. 793 | 794 | price_type (str): The type of price that should be modified. Can 795 | be one of the following: [ 796 | 'price', 797 | 'stop-price', 798 | 'limit-price', 799 | 'stop-limit-stop-price', 800 | 'stop-limit-limit-price' 801 | ] 802 | """ 803 | 804 | if price_type == 'price': 805 | self.order['price'] = new_price 806 | elif price_type == 'stop-price' and self.is_stop_order: 807 | self.order['stopPrice'] = new_price 808 | self.stop_price = new_price 809 | elif price_type == 'limit-price' and self.is_limit_order: 810 | self.order['price'] = new_price 811 | self.price = new_price 812 | elif price_type == 'stop-limit-limit-price' and self.is_stop_limit_order: 813 | self.order['price'] = new_price 814 | self.stop_limit_price = new_price 815 | elif price_type == 'stop-limit-stop-price' and self.is_stop_limit_order: 816 | self.order['stopPrice'] = new_price 817 | self.stop_price = new_price 818 | 819 | @property 820 | def is_stop_order(self) -> bool: 821 | """Specifies whether the order is a Stop Loss Order. 822 | 823 | Returns: 824 | ---- 825 | bool: `True` if the order is a Stop order, `False` otherwise. 826 | """ 827 | 828 | if self.order_type != 'stop': 829 | return False 830 | else: 831 | return True 832 | 833 | @property 834 | def is_stop_limit_order(self) -> bool: 835 | """Specifies whether the order is a Stop Limit Order. 836 | 837 | Returns: 838 | ---- 839 | bool: `True` if the order is a Stop Limit order, `False` otherwise. 840 | """ 841 | 842 | if self.order_type != 'stop-lmt': 843 | return False 844 | else: 845 | return True 846 | 847 | @property 848 | def is_limit_order(self) -> bool: 849 | """Specifies whether the order is a Limit Order. 850 | 851 | Returns: 852 | ---- 853 | bool: `True` if the order is a Limit order, `False` otherwise. 854 | """ 855 | 856 | if self.order_type != 'lmt': 857 | return False 858 | else: 859 | return True 860 | 861 | @property 862 | def is_trigger_order(self) -> bool: 863 | """Specifies whether the order is a trigger order. 864 | 865 | Returns: 866 | ---- 867 | bool: `True` if the order is a contains a trigger, 868 | `False` otherwise. 869 | """ 870 | 871 | if self._triggered_added: 872 | return True 873 | else: 874 | return False 875 | 876 | def _process_order_response(self) -> None: 877 | """Processes an order response, after is has been submitted.""" 878 | 879 | self.order_id = self._order_response["order_id"] 880 | self.order_status = "QUEUED" 881 | 882 | def _update_order_status(self) -> None: 883 | """Updates the current order status, to reflect what's on TD.""" 884 | 885 | if self.order_id != "": 886 | 887 | order_response = self._td_client.get_orders( 888 | account=self.account, 889 | order_id=self.order_id 890 | ) 891 | 892 | self.order_response = order_response 893 | self.order_status = self.order_response['status'] 894 | 895 | def check_status(self) -> object: 896 | """Used to easily identify the order status. 897 | 898 | Returns 899 | ------- 900 | OrderStatus 901 | An order status object that provides simple 902 | properties to grab the order status. 903 | """ 904 | 905 | from pyrobot.order_status import OrderStatus 906 | 907 | return OrderStatus(trade_obj=self) 908 | 909 | def update_children(self) -> None: 910 | """Updates the Price info of the children info.""" 911 | 912 | # Grab the children. 913 | children = self.order['childOrderStrategies'][0]['childOrderStrategies'] 914 | 915 | # Loop through each child. 916 | for order in children: 917 | 918 | # Get the latest price. 919 | quote = self._td_client.get_quotes(instruments=[self.symbol]) 920 | last_price = quote[self.symbol]['lastPrice'] 921 | 922 | # Update the price. 923 | if order['orderType'] == 'STOP': 924 | order['stopPrice'] = round(order['stopPrice'] + last_price, 2) 925 | elif order['orderType'] == 'LIMIT': 926 | order['price'] = round(order['price'] + last_price, 2) 927 | 928 | -------------------------------------------------------------------------------- /samples/responses/orders.jsonc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/areed1192/python-trading-robot/abe7ac9189873f376e6fd968f57f403a8ffd45d2/samples/responses/orders.jsonc -------------------------------------------------------------------------------- /samples/responses/quotes-small.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "MSFT": { 3 | "bid_price": 165.7, 4 | "ask_price": 165.99, 5 | "last_price": 165.7, 6 | "open_price": 165.67, 7 | "high_price": 166.67, 8 | "low_price": 163.5, 9 | "close_price": 163.49, 10 | "total_volume": 48318234, 11 | "quote_time": 1586390396750, 12 | "trade_time": 1586390396750, 13 | "regular_market_last_price": 165.13, 14 | "regular_market_trade_time": 1586376000354 15 | }, 16 | "AAPL": { 17 | "bid_price": 267.35, 18 | "ask_price": 267.36, 19 | "last_price": 267.33, 20 | "open_price": 262.74, 21 | "high_price": 267.37, 22 | "low_price": 261.23, 23 | "close_price": 259.43, 24 | "total_volume": 42223821, 25 | "quote_time": 1586390399572, 26 | "trade_time": 1586390398976, 27 | "regular_market_last_price": 266.07, 28 | "regular_market_trade_time": 1586376000713 29 | } 30 | } -------------------------------------------------------------------------------- /samples/responses/quotes.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "MSFT": { 3 | "assetType": "EQUITY", 4 | "assetMainType": "EQUITY", 5 | "cusip": "594918104", 6 | "symbol": "MSFT", 7 | "description": "Microsoft Corporation - Common Stock", 8 | "bidPrice": 165.7, 9 | "bidSize": 2200, 10 | "bidId": "P", 11 | "askPrice": 165.99, 12 | "askSize": 200, 13 | "askId": "P", 14 | "lastPrice": 165.7, 15 | "lastSize": 0, 16 | "lastId": "Q", 17 | "openPrice": 165.67, 18 | "highPrice": 166.67, 19 | "lowPrice": 163.5, 20 | "bidTick": " ", 21 | "closePrice": 163.49, 22 | "netChange": 2.21, 23 | "totalVolume": 48318234, 24 | "quoteTimeInLong": 1586390396750, 25 | "tradeTimeInLong": 1586390396750, 26 | "mark": 165.7, 27 | "exchange": "q", 28 | "exchangeName": "NASD", 29 | "marginable": true, 30 | "shortable": true, 31 | "volatility": 0.0124, 32 | "digits": 4, 33 | "52WkHigh": 190.7, 34 | "52WkLow": 118.58, 35 | "nAV": 0.0, 36 | "peRatio": 30.5726, 37 | "divAmount": 2.04, 38 | "divYield": 1.25, 39 | "divDate": "2020-05-20 00: 00: 00.000", 40 | "securityStatus": "Normal", 41 | "regularMarketLastPrice": 165.13, 42 | "regularMarketLastSize": 40679, 43 | "regularMarketNetChange": 1.64, 44 | "regularMarketTradeTimeInLong": 1586376000354, 45 | "netPercentChangeInDouble": 1.3518, 46 | "markChangeInDouble": 2.21, 47 | "markPercentChangeInDouble": 1.3518, 48 | "regularMarketPercentChangeInDouble": 1.0031, 49 | "delayed": false 50 | }, 51 | "AAPL": { 52 | "assetType": "EQUITY", 53 | "assetMainType": "EQUITY", 54 | "cusip": "037833100", 55 | "symbol": "AAPL", 56 | "description": "Apple Inc. - Common Stock", 57 | "bidPrice": 267.35, 58 | "bidSize": 100, 59 | "bidId": "P", 60 | "askPrice": 267.36, 61 | "askSize": 300, 62 | "askId": "P", 63 | "lastPrice": 267.33, 64 | "lastSize": 0, 65 | "lastId": "Q", 66 | "openPrice": 262.74, 67 | "highPrice": 267.37, 68 | "lowPrice": 261.23, 69 | "bidTick": " ", 70 | "closePrice": 259.43, 71 | "netChange": 7.9, 72 | "totalVolume": 42223821, 73 | "quoteTimeInLong": 1586390399572, 74 | "tradeTimeInLong": 1586390398976, 75 | "mark": 267.35, 76 | "exchange": "q", 77 | "exchangeName": "NASD", 78 | "marginable": true, 79 | "shortable": true, 80 | "volatility": 0.0141, 81 | "digits": 4, 82 | "52WkHigh": 327.85, 83 | "52WkLow": 170.27, 84 | "nAV": 0.0, 85 | "peRatio": 20.7296, 86 | "divAmount": 3.08, 87 | "divYield": 1.19, 88 | "divDate": "2020-02-07 00: 00: 00.000", 89 | "securityStatus": "Normal", 90 | "regularMarketLastPrice": 266.07, 91 | "regularMarketLastSize": 27570, 92 | "regularMarketNetChange": 6.64, 93 | "regularMarketTradeTimeInLong": 1586376000713, 94 | "netPercentChangeInDouble": 3.0451, 95 | "markChangeInDouble": 7.92, 96 | "markPercentChangeInDouble": 3.0528, 97 | "regularMarketPercentChangeInDouble": 2.5595, 98 | "delayed": false 99 | } 100 | } -------------------------------------------------------------------------------- /samples/trading_robot.py: -------------------------------------------------------------------------------- 1 | 2 | import time as time_lib 3 | import pprint 4 | import pathlib 5 | import operator 6 | import pandas as pd 7 | 8 | from datetime import datetime 9 | from datetime import timedelta 10 | from configparser import ConfigParser 11 | 12 | from pyrobot.robot import PyRobot 13 | from pyrobot.indicators import Indicators 14 | 15 | # Grab configuration values. 16 | config = ConfigParser() 17 | config.read('configs/config.ini') 18 | 19 | CLIENT_ID = config.get('main', 'CLIENT_ID') 20 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 21 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 22 | ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 23 | 24 | # Initalize the robot. 25 | trading_robot = PyRobot( 26 | client_id=CLIENT_ID, 27 | redirect_uri=REDIRECT_URI, 28 | credentials_path=CREDENTIALS_PATH, 29 | paper_trading=True 30 | ) 31 | 32 | # Create a Portfolio 33 | trading_robot_portfolio = trading_robot.create_portfolio() 34 | 35 | # Define mutliple positions to add. 36 | multi_position = [ 37 | { 38 | 'asset_type': 'equity', 39 | 'quantity': 2, 40 | 'purchase_price': 4.00, 41 | 'symbol': 'TSLA', 42 | 'purchase_date': '2020-01-31' 43 | }, 44 | { 45 | 'asset_type': 'equity', 46 | 'quantity': 2, 47 | 'purchase_price': 4.00, 48 | 'symbol': 'SQ', 49 | 'purchase_date': '2020-01-31' 50 | } 51 | ] 52 | 53 | # Grab the New positions 54 | new_positions = trading_robot.portfolio.add_positions(positions=multi_position) 55 | pprint.pprint(new_positions) 56 | 57 | # Add a single position 58 | trading_robot_portfolio.add_position( 59 | symbol='MSFT', 60 | quantity=10, 61 | purchase_price=10, 62 | asset_type='equity', 63 | purchase_date='2020-04-01' 64 | ) 65 | 66 | # Add another single position 67 | trading_robot_portfolio.add_position( 68 | symbol='AAPL', 69 | quantity=10, 70 | purchase_price=10, 71 | asset_type='equity', 72 | purchase_date='2020-04-01' 73 | ) 74 | 75 | # If the Market is open, print some quotes. 76 | if trading_robot.regular_market_open: 77 | pprint.pprint(trading_robot.grab_current_quotes()) 78 | 79 | # If the Post Market is Open, do something. 80 | elif trading_robot.post_market_open: 81 | pprint.pprint(trading_robot.grab_current_quotes()) 82 | 83 | # If the Pre Market is Open, do something. 84 | elif trading_robot.pre_market_open: 85 | pprint.pprint(trading_robot.grab_current_quotes()) 86 | 87 | # Print the Positions 88 | pprint.pprint(trading_robot_portfolio.positions) 89 | 90 | # Grab the current quotes, for all of our positions. 91 | current_quotes = trading_robot.grab_current_quotes() 92 | 93 | # Print the Quotes. 94 | pprint.pprint(current_quotes) 95 | 96 | # Let's see if our Microsoft Position is profitable. 97 | is_msft_porfitable = trading_robot.portfolio.is_profitable( 98 | symbol="MSFT", 99 | current_price=current_quotes['MSFT']['lastPrice'] 100 | ) 101 | print("Is Microsoft Profitable: {answer}".format(answer=is_msft_porfitable)) 102 | 103 | # Let's get the projected Market Value. 104 | portfolio_summary = trading_robot.portfolio.projected_market_value( 105 | current_prices=current_quotes 106 | ) 107 | pprint.pprint(portfolio_summary) 108 | 109 | # Create a new Trade Object. 110 | new_trade = trading_robot.create_trade( 111 | trade_id='long_msft', 112 | enter_or_exit='enter', 113 | long_or_short='short', 114 | order_type='lmt', 115 | price=150.00 116 | ) 117 | 118 | # Make it Good Till Cancel. 119 | new_trade.good_till_cancel(cancel_time=datetime.now()) 120 | 121 | # Change the session 122 | new_trade.modify_session(session='am') 123 | 124 | # Add an Order Leg. 125 | new_trade.instrument( 126 | symbol='MSFT', 127 | quantity=2, 128 | asset_type='EQUITY' 129 | ) 130 | 131 | # Add a Stop Loss Order with the Main Order. 132 | new_trade.add_stop_loss( 133 | stop_size=.10, 134 | percentage=False 135 | ) 136 | 137 | # Print out the order. 138 | pprint.pprint(new_trade.order) 139 | 140 | # Grab historical prices, first define the start date and end date. 141 | start_date = datetime.today() 142 | end_date = start_date - timedelta(days=30) 143 | 144 | # Grab the historical prices. 145 | historical_prices = trading_robot.grab_historical_prices( 146 | start=end_date, 147 | end=start_date, 148 | bar_size=1, 149 | bar_type='minute' 150 | ) 151 | 152 | # Convert data to a Data Frame. 153 | stock_frame = trading_robot.create_stock_frame( 154 | data=historical_prices['aggregated'] 155 | ) 156 | 157 | # We can also add the stock frame to the Portfolio object. 158 | trading_robot.portfolio.stock_frame = stock_frame 159 | 160 | # Additionally the historical prices can be set as well. 161 | trading_robot.portfolio.historical_prices = historical_prices 162 | 163 | # Portfolio Variance 164 | pprint.pprint(trading_robot.portfolio.portfolio_metrics()) 165 | 166 | # Create an indicator Object. 167 | indicator_client = Indicators(price_data_frame=stock_frame) 168 | 169 | # Add the RSI Indicator. 170 | indicator_client.rsi(period=14) 171 | 172 | # Add the 200 day simple moving average. 173 | indicator_client.sma(period=200) 174 | 175 | # Add the 200 day simple moving average. 176 | indicator_client.sma(period=50) 177 | 178 | # Add the 50 day exponentials moving average. 179 | indicator_client.ema(period=50) 180 | 181 | # Add a signal to check for. 182 | indicator_client.set_indicator_signal( 183 | indicator='rsi', 184 | buy=40.0, 185 | sell=20.0, 186 | condition_buy=operator.ge, 187 | condition_sell=operator.le 188 | ) 189 | 190 | # Define a trading dictionary. 191 | trades_dict = { 192 | 'MSFT': { 193 | 'trade_func': trading_robot.trades['long_msft'], 194 | 'trade_id': trading_robot.trades['long_msft'].trade_id 195 | } 196 | } 197 | 198 | while True: 199 | 200 | # Grab the latest bar. 201 | latest_bars = trading_robot.get_latest_bar() 202 | 203 | # Add to the Stock Frame. 204 | stock_frame.add_rows(data=latest_bars) 205 | 206 | # Refresh the Indicators. 207 | indicator_client.refresh() 208 | 209 | print("="*50) 210 | print("Current StockFrame") 211 | print("-"*50) 212 | print(stock_frame.symbol_groups.tail()) 213 | print("-"*50) 214 | print("") 215 | 216 | # Check for signals. 217 | signals = indicator_client.check_signals() 218 | 219 | # Execute Trades. 220 | trading_robot.execute_signals( 221 | signals=signals, 222 | trades_to_execute=trades_dict 223 | ) 224 | 225 | # Grab the last bar. 226 | last_bar_timestamp = trading_robot.stock_frame.frame.tail( 227 | n=1 228 | ).index.get_level_values(1) 229 | 230 | # Wait till the next bar. 231 | trading_robot.wait_till_next_bar(last_bar_timestamp=last_bar_timestamp) 232 | -------------------------------------------------------------------------------- /samples/trading_robot_indicators.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | from datetime import datetime 4 | from datetime import timedelta 5 | from configparser import ConfigParser 6 | 7 | from pyrobot.robot import PyRobot 8 | from pyrobot.indicators import Indicators 9 | 10 | # Grab configuration values. 11 | config = ConfigParser() 12 | config.read('configs/config.ini') 13 | 14 | CLIENT_ID = config.get('main', 'CLIENT_ID') 15 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 16 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 17 | ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 18 | 19 | # Initalize the robot. 20 | trading_robot = PyRobot( 21 | client_id=CLIENT_ID, 22 | redirect_uri=REDIRECT_URI, 23 | credentials_path=CREDENTIALS_PATH, 24 | paper_trading=True 25 | ) 26 | 27 | # Create a Portfolio 28 | trading_robot_portfolio = trading_robot.create_portfolio() 29 | 30 | # Add a single position 31 | trading_robot_portfolio.add_position( 32 | symbol='MSFT', 33 | quantity=10, 34 | purchase_price=10, 35 | asset_type='equity', 36 | purchase_date='2020-04-01' 37 | ) 38 | 39 | # Grab historical prices, first define the start date and end date. 40 | start_date = datetime.today() 41 | end_date = start_date - timedelta(days=30) 42 | 43 | # Grab the historical prices. 44 | historical_prices = trading_robot.grab_historical_prices( 45 | start=end_date, 46 | end=start_date, 47 | bar_size=1, 48 | bar_type='minute' 49 | ) 50 | 51 | # Convert data to a Data Frame. 52 | stock_frame = trading_robot.create_stock_frame( 53 | data=historical_prices['aggregated'] 54 | ) 55 | 56 | # We can also add the stock frame to the Portfolio object. 57 | trading_robot.portfolio.stock_frame = stock_frame 58 | 59 | # Additionally the historical prices can be set as well. 60 | trading_robot.portfolio.historical_prices = historical_prices 61 | 62 | # Create an indicator Object. 63 | indicator_client = Indicators(price_data_frame=stock_frame) 64 | 65 | # Add the RSI Indicator. 66 | indicator_client.rsi(period=14) 67 | 68 | # Add the 200 day simple moving average. 69 | indicator_client.sma(period=200) 70 | 71 | # Add the 50 day exponentials moving average. 72 | indicator_client.ema(period=50) 73 | 74 | # Add the Bollinger Bands. 75 | indicator_client.bollinger_bands(period=20) 76 | 77 | # Add the Rate of Change. 78 | indicator_client.rate_of_change(period=1) 79 | 80 | # Add the Average True Range. 81 | indicator_client.average_true_range(period=14) 82 | 83 | # Add the Stochastic Oscillator. 84 | indicator_client.stochastic_oscillator() 85 | 86 | # Add the MACD. 87 | indicator_client.macd(fast_period=12, slow_period=26) 88 | 89 | # Add the Mass Index. 90 | indicator_client.mass_index(period=9) 91 | 92 | # # Add the K-Oscillator 93 | # indicator_client.kst_oscillator( 94 | # r1=1, 95 | # r2=2, 96 | # r3=3, 97 | # r4=4, 98 | # n1=1, 99 | # n2=2, 100 | # n3=3, 101 | # n4=4 102 | # ) 103 | 104 | while True: 105 | 106 | # Grab the latest bar. 107 | latest_bars = trading_robot.get_latest_bar() 108 | 109 | # Add to the Stock Frame. 110 | stock_frame.add_rows(data=latest_bars) 111 | 112 | # Refresh the Indicators. 113 | indicator_client.refresh() 114 | 115 | print("="*50) 116 | print("Current StockFrame") 117 | print("-"*50) 118 | print(stock_frame.symbol_groups.tail()) 119 | print("-"*50) 120 | print("") 121 | 122 | # Check for signals. 123 | signals = indicator_client.check_signals() 124 | 125 | # Grab the last bar. 126 | last_bar_timestamp = trading_robot.stock_frame.frame.tail( 127 | n=1 128 | ).index.get_level_values(1) 129 | 130 | # Wait till the next bar. 131 | trading_robot.wait_till_next_bar(last_bar_timestamp=last_bar_timestamp) 132 | -------------------------------------------------------------------------------- /samples/trading_robot_indicators_compare.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import operator 3 | 4 | from datetime import datetime 5 | from datetime import timedelta 6 | from configparser import ConfigParser 7 | 8 | from pyrobot.robot import PyRobot 9 | from pyrobot.indicators import Indicators 10 | 11 | # Grab configuration values. 12 | config = ConfigParser() 13 | config.read('configs/config.ini') 14 | 15 | CLIENT_ID = config.get('main', 'CLIENT_ID') 16 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 17 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 18 | ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 19 | 20 | # Initalize the robot. 21 | trading_robot = PyRobot( 22 | client_id=CLIENT_ID, 23 | redirect_uri=REDIRECT_URI, 24 | credentials_path=CREDENTIALS_PATH, 25 | paper_trading=True 26 | ) 27 | 28 | # Create a Portfolio 29 | trading_robot_portfolio = trading_robot.create_portfolio() 30 | 31 | # Add a single position 32 | trading_robot_portfolio.add_position( 33 | symbol='MSFT', 34 | quantity=10, 35 | purchase_price=10, 36 | asset_type='equity', 37 | purchase_date='2020-04-01' 38 | ) 39 | 40 | # Grab historical prices, first define the start date and end date. 41 | start_date = datetime.today() 42 | end_date = start_date - timedelta(days=30) 43 | 44 | # Grab the historical prices. 45 | historical_prices = trading_robot.grab_historical_prices( 46 | start=end_date, 47 | end=start_date, 48 | bar_size=1, 49 | bar_type='minute' 50 | ) 51 | 52 | # Convert data to a Data Frame. 53 | stock_frame = trading_robot.create_stock_frame( 54 | data=historical_prices['aggregated'] 55 | ) 56 | 57 | # We can also add the stock frame to the Portfolio object. 58 | trading_robot.portfolio.stock_frame = stock_frame 59 | 60 | # Additionally the historical prices can be set as well. 61 | trading_robot.portfolio.historical_prices = historical_prices 62 | 63 | # Create an indicator Object. 64 | indicator_client = Indicators(price_data_frame=stock_frame) 65 | 66 | # Add the RSI Indicator. 67 | indicator_client.rsi(period=14) 68 | 69 | # Add the 200 day simple moving average. 70 | indicator_client.sma(period=200) 71 | 72 | # Add the 50 day simple moving average. 73 | indicator_client.sma(period=50, column_name='sma_50') 74 | 75 | # Add the 50 day exponentials moving average. 76 | indicator_client.ema(period=50) 77 | 78 | # Add the Bollinger Bands. 79 | indicator_client.bollinger_bands(period=20) 80 | 81 | # Add the Rate of Change. 82 | indicator_client.rate_of_change(period=1) 83 | 84 | # Add the Average True Range. 85 | indicator_client.average_true_range(period=14) 86 | 87 | # Add the Stochastic Oscillator. 88 | indicator_client.stochastic_oscillator() 89 | 90 | # Add the MACD. 91 | indicator_client.macd(fast_period=12, slow_period=26) 92 | 93 | # Add the Mass Index. 94 | indicator_client.mass_index(period=9) 95 | 96 | # Add a signal to check for. 97 | indicator_client.set_indicator_signal_compare( 98 | indicator_1='sma', 99 | indicator_2='ema', 100 | condition_buy=operator.ge, 101 | condition_sell=None 102 | ) 103 | 104 | # Add a signal to check for. 105 | indicator_client.set_indicator_signal_compare( 106 | indicator_1='sma', 107 | indicator_2='sma_50', 108 | condition_buy=operator.ge, 109 | condition_sell=None 110 | ) 111 | 112 | # Print the Head. 113 | print(trading_robot.stock_frame.frame.tail()) 114 | 115 | # Check for signals. 116 | signals = indicator_client.check_signals() 117 | 118 | # Print the Head. 119 | print(trading_robot.stock_frame.frame.head()) 120 | 121 | # Print the Signals. 122 | pprint.pprint(signals) 123 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_namespace_packages 3 | 4 | # load the README file. 5 | with open(file="README.md", mode="r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | 10 | name='python-trading-robot', 11 | 12 | author='Alex Reed', 13 | 14 | author_email='coding.sigma@gmail.com', 15 | 16 | version='0.1.1', 17 | 18 | description='A trading robot built for Python that uses the TD Ameritrade API.', 19 | 20 | long_description=long_description, 21 | 22 | long_description_content_type="text/markdown", 23 | 24 | url='https://github.com/areed1192/python-trading-robot', 25 | 26 | install_requires=[ 27 | 'td-ameritrade-python-api>=0.3.0', 28 | 'pandas==1.0.5', 29 | 'numpy==1.19.0' 30 | ], 31 | 32 | keywords='finance, td ameritrade, api, trading robot', 33 | 34 | packages=find_namespace_packages( 35 | include=['pyrobot', 'samples', 'tests'], 36 | exclude=['configs*'] 37 | ), 38 | 39 | include_package_data=True, 40 | 41 | python_requires='>=3.8', 42 | 43 | classifiers=[ 44 | 45 | # I can say what phase of development my library is in. 46 | 'Development Status :: 3 - Alpha', 47 | 48 | # Here I'll add the audience this library is intended for. 49 | 'Intended Audience :: Developers', 50 | 'Intended Audience :: Science/Research', 51 | 'Intended Audience :: Financial and Insurance Industry', 52 | 53 | # Here I'll define the license that guides my library. 54 | 'License :: OSI Approved :: MIT License', 55 | 56 | # Here I'll note that package was written in English. 57 | 'Natural Language :: English', 58 | 59 | # Here I'll note that any operating system can use it. 60 | 'Operating System :: OS Independent', 61 | 62 | # Here I'll specify the version of Python it uses. 63 | 'Programming Language :: Python', 64 | 'Programming Language :: Python :: 3', 65 | 'Programming Language :: Python :: 3.8', 66 | 67 | # Here are the topics that my library covers. 68 | 'Topic :: Database', 69 | 'Topic :: Education', 70 | 'Topic :: Office/Business' 71 | 72 | ] 73 | ) -------------------------------------------------------------------------------- /tests/test_indicators.py: -------------------------------------------------------------------------------- 1 | """Unit test module for the Indicator Object. 2 | 3 | Will perform an instance test to make sure it creates it. Additionally, 4 | it will test different properties and methods of the object. 5 | """ 6 | import unittest 7 | import operator 8 | import pandas as pd 9 | 10 | from unittest import TestCase 11 | from datetime import datetime 12 | from datetime import timedelta 13 | from configparser import ConfigParser 14 | 15 | from pyrobot.robot import PyRobot 16 | from pyrobot.indicators import Indicators 17 | from pyrobot.stock_frame import StockFrame 18 | 19 | 20 | class PyRobotIndicatorTest(TestCase): 21 | 22 | """Will perform a unit test for the Indicator Object.""" 23 | 24 | def setUp(self) -> None: 25 | """Set up the Indicator Client.""" 26 | 27 | # Grab configuration values. 28 | config = ConfigParser() 29 | config.read('configs/config.ini') 30 | 31 | CLIENT_ID = config.get('main', 'CLIENT_ID') 32 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 33 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 34 | 35 | # Create a robot. 36 | self.robot = PyRobot( 37 | client_id = CLIENT_ID, 38 | redirect_uri = REDIRECT_URI, 39 | credentials_path = CREDENTIALS_PATH 40 | ) 41 | 42 | # Grab historical prices, first define the start date and end date. 43 | start_date = datetime.today() 44 | end_date = start_date - timedelta(days=30) 45 | 46 | # Grab the historical prices. 47 | historical_prices = self.robot.grab_historical_prices( 48 | start=end_date, 49 | end=start_date, 50 | bar_size=1, 51 | bar_type='minute', 52 | symbols=['AAPL','MSFT'] 53 | ) 54 | 55 | # Convert data to a Data Frame. 56 | self.stock_frame = self.robot.create_stock_frame(data=historical_prices['aggregated']) 57 | 58 | # Create the indicator client. 59 | self.indicator_client = Indicators(price_data_frame=self.stock_frame) 60 | 61 | def test_creates_instance_of_session(self): 62 | """Create an instance and make sure it's a StockFrame.""" 63 | 64 | self.assertIsInstance(self.stock_frame, StockFrame) 65 | self.assertIsInstance(self.indicator_client, Indicators) 66 | 67 | def test_price_frame_data_property(self): 68 | """Test getting the Price Data Frame.""" 69 | 70 | self.assertIsNotNone(self.indicator_client.price_data_frame) 71 | 72 | def test_is_multi_index_property(self): 73 | """Test getting the Price Data Frame.""" 74 | 75 | self.assertTrue(self.indicator_client.is_multi_index) 76 | 77 | def test_change_in_price(self): 78 | """Test adding the Change in Price.""" 79 | 80 | # Create the Change in Price indicator. 81 | self.indicator_client.change_in_price() 82 | 83 | # Check if we have the column. 84 | self.assertIn('change_in_price', self.stock_frame.frame.columns) 85 | 86 | # And that it's not empty. 87 | self.assertFalse(self.stock_frame.frame['change_in_price'].empty) 88 | 89 | def test_rsi(self): 90 | """Test adding the Relative Strength Index.""" 91 | 92 | # Create the RSI indicator. 93 | self.indicator_client.rsi(period=14) 94 | 95 | # Check if we have the column. 96 | self.assertIn('rsi', self.stock_frame.frame.columns) 97 | 98 | # And that it's not empty. 99 | self.assertFalse(self.stock_frame.frame['rsi'].empty) 100 | 101 | def test_sma(self): 102 | """Test adding the Simple Moving Average.""" 103 | 104 | # Create the SMA indicator. 105 | self.indicator_client.sma(period=200) 106 | 107 | # Check if we have the column. 108 | self.assertIn('sma', self.stock_frame.frame.columns) 109 | 110 | # And that it's not empty. 111 | self.assertFalse(self.stock_frame.frame['sma'].empty) 112 | 113 | def test_ema(self): 114 | """Test adding the Exponential Moving Average.""" 115 | 116 | # Create the EMA indicator. 117 | self.indicator_client.ema(period=50) 118 | 119 | # Check if we have the column. 120 | self.assertIn('ema', self.stock_frame.frame.columns) 121 | 122 | # And that it's not empty. 123 | self.assertFalse(self.stock_frame.frame['ema'].empty) 124 | 125 | def test_indicator_exist(self): 126 | """Test checkinf if an indicator column exist.""" 127 | 128 | # Create the EMA indicator. 129 | self.indicator_client.ema(period=50) 130 | 131 | # Check if we have the column. 132 | self.assertIn('ema', self.stock_frame.frame.columns) 133 | 134 | # And that it's not empty. 135 | self.assertTrue(self.stock_frame.do_indicator_exist(column_names=['ema'])) 136 | 137 | def test_indicator_signal(self): 138 | """Test checkinf if an indicator column exist.""" 139 | 140 | # Create the EMA indicator. 141 | self.indicator_client.ema(period=50) 142 | 143 | self.indicator_client.set_indicator_signal( 144 | indicator='sma', 145 | buy=50.0, 146 | sell=30.0, 147 | condition_buy=operator.ge, 148 | condition_sell=operator.le 149 | ) 150 | 151 | func_1 = operator.ge 152 | func_2 = operator.le 153 | 154 | correct_dict = { 155 | 'buy': 50.0, 156 | 'sell': 30.0, 157 | 'buy_operator': func_1, 158 | 'sell_operator': func_2 159 | } 160 | 161 | correct_dict_all = { 162 | 'sma':{ 163 | 'buy': 50.0, 164 | 'sell': 30.0, 165 | 'buy_operator': func_1, 166 | 'sell_operator': func_2 167 | } 168 | } 169 | 170 | 171 | # And that it's not empty. 172 | self.assertDictEqual( 173 | self.indicator_client.get_indicator_signal(indicator='sma'), 174 | correct_dict 175 | ) 176 | 177 | # And that it's not empty. 178 | self.assertDictEqual( 179 | self.indicator_client.get_indicator_signal(), 180 | correct_dict_all 181 | ) 182 | 183 | def tearDown(self) -> None: 184 | """Teardown the Indicator object.""" 185 | 186 | self.stock_frame = None 187 | self.indicator_client = None 188 | 189 | 190 | if __name__ == '__main__': 191 | unittest.main() -------------------------------------------------------------------------------- /tests/test_portfolio.py: -------------------------------------------------------------------------------- 1 | """Unit test module for the Portfolio Object. 2 | 3 | Will perform an instance test to make sure it creates it. Additionally, 4 | it will test different properties and methods of the object. 5 | """ 6 | 7 | import unittest 8 | from unittest import TestCase 9 | from configparser import ConfigParser 10 | 11 | from pyrobot.portfolio import Portfolio 12 | from td.client import TDClient 13 | 14 | 15 | class PyRobotPortfolioTest(TestCase): 16 | 17 | """Will perform a unit test for the Portfolio object.""" 18 | 19 | def setUp(self) -> None: 20 | """Set up the Portfolio.""" 21 | 22 | self.portfolio = Portfolio() 23 | self.maxDiff = None 24 | 25 | # Grab configuration values. 26 | config = ConfigParser() 27 | config.read('configs/config.ini') 28 | 29 | CLIENT_ID = config.get('main', 'CLIENT_ID') 30 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 31 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 32 | self.ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 33 | 34 | self.td_client = TDClient( 35 | client_id=CLIENT_ID, 36 | redirect_uri=REDIRECT_URI, 37 | credentials_path=CREDENTIALS_PATH 38 | ) 39 | 40 | self.td_client.login() 41 | 42 | def test_create_portofolio(self): 43 | """Make sure it's a Portfolio.""" 44 | 45 | self.assertIsInstance(self.portfolio, Portfolio) 46 | 47 | def test_td_client_property(self): 48 | """Test the TD Client property.""" 49 | 50 | # Should be None if wasn't initalized from the PyRobot. 51 | self.assertIsNone(self.portfolio.td_client) 52 | 53 | def test_stock_frame_property(self): 54 | """Test the Stock Frame property.""" 55 | 56 | # Should be None if wasn't initalized from the PyRobot. 57 | self.assertIsNone(self.portfolio.stock_frame) 58 | 59 | def test_historical_prices_property(self): 60 | """Test the Historical Prices property.""" 61 | 62 | # Should be False if wasn't initalized from the PyRobot. 63 | self.assertFalse(self.portfolio.historical_prices) 64 | 65 | def test_add_position(self): 66 | """Test adding a single position to the portfolio.""" 67 | 68 | new_position = self.portfolio.add_position( 69 | symbol='MSFT', 70 | asset_type='equity', 71 | quantity=10, 72 | purchase_price=3.00, 73 | purchase_date='2020-01-31' 74 | ) 75 | 76 | correct_position = { 77 | 'symbol': 'MSFT', 78 | 'asset_type': 'equity', 79 | 'ownership_status': True, 80 | 'quantity': 10, 81 | 'purchase_price': 3.00, 82 | 'purchase_date': '2020-01-31' 83 | } 84 | 85 | self.assertDictEqual(new_position, correct_position) 86 | 87 | def test_add_position_default_arguments(self): 88 | """Test adding a single position to the portfolio, no date.""" 89 | 90 | new_position = self.portfolio.add_position( 91 | symbol='MSFT', 92 | asset_type='equity' 93 | ) 94 | 95 | correct_position = { 96 | 'symbol': 'MSFT', 97 | 'asset_type': 'equity', 98 | 'ownership_status': False, 99 | 'quantity': 0, 100 | 'purchase_price': 0.00, 101 | 'purchase_date': None 102 | } 103 | 104 | self.assertDictEqual(new_position, correct_position) 105 | 106 | def test_delete_existing_position(self): 107 | """Test deleting an exisiting position.""" 108 | 109 | self.portfolio.add_position( 110 | symbol='MSFT', 111 | asset_type='equity', 112 | quantity=10, 113 | purchase_price=3.00, 114 | purchase_date='2020-01-31' 115 | ) 116 | 117 | delete_status = self.portfolio.remove_position(symbol='MSFT') 118 | correct_status = (True, 'MSFT was successfully removed.') 119 | 120 | self.assertTupleEqual(delete_status, correct_status) 121 | 122 | def test_delete_non_existing_position(self): 123 | """Test deleting a non-exisiting position.""" 124 | 125 | delete_status = self.portfolio.remove_position(symbol='AAPL') 126 | correct_status = (False, 'AAPL did not exist in the porfolio.') 127 | 128 | self.assertTupleEqual(delete_status, correct_status) 129 | 130 | def test_in_portfolio_exisitng(self): 131 | """Checks to see if an exisiting position exists.""" 132 | 133 | self.portfolio.add_position( 134 | symbol='MSFT', 135 | asset_type='equity', 136 | quantity=10, 137 | purchase_price=3.00, 138 | purchase_date='2020-01-31' 139 | ) 140 | 141 | in_portfolio_flag = self.portfolio.in_portfolio(symbol='MSFT') 142 | self.assertTrue(in_portfolio_flag) 143 | 144 | def test_in_portfolio_non_exisitng(self): 145 | """Checks to see if a non exisiting position exists.""" 146 | 147 | in_portfolio_flag = self.portfolio.in_portfolio(symbol='AAPL') 148 | self.assertFalse(in_portfolio_flag) 149 | 150 | def test_is_profitable(self): 151 | """Checks to see if a position is profitable.""" 152 | 153 | # Add a position. 154 | self.portfolio.add_position( 155 | symbol='MSFT', 156 | asset_type='equity', 157 | quantity=10, 158 | purchase_price=3.00, 159 | purchase_date='2020-01-31' 160 | ) 161 | 162 | # Test for being Profitable. 163 | is_profitable = self.portfolio.is_profitable( 164 | symbol='MSFT', 165 | current_price=5.00 166 | 167 | ) 168 | 169 | # Test for not being profitable. 170 | is_not_profitable = self.portfolio.is_profitable( 171 | symbol='MSFT', 172 | current_price=1.00 173 | ) 174 | 175 | self.assertTrue(is_profitable) 176 | self.assertFalse(is_not_profitable) 177 | 178 | def test_projected_market_value(self): 179 | """Tests the generation of a market value summary, for all of the positions.""" 180 | 181 | # Add a position. 182 | self.portfolio.add_position( 183 | symbol='MSFT', 184 | asset_type='equity', 185 | quantity=10, 186 | purchase_price=3.00, 187 | purchase_date='2020-01-31' 188 | ) 189 | 190 | correct_dict = { 191 | 'MSFT': { 192 | 'current_price': 5.0, 193 | 'is_profitable': True, 194 | 'purchase_price': 3.0, 195 | 'quantity': 10, 196 | 'total_invested_capital': 30.0, 197 | 'total_loss_or_gain_$': 20.0, 198 | 'total_loss_or_gain_%': 0.6667, 199 | 'total_market_value': 50.0 200 | }, 201 | 'total': { 202 | 'number_of_breakeven_positions': 0, 203 | 'number_of_non_profitable_positions': 0, 204 | 'number_of_profitable_positions': 1, 205 | 'total_invested_capital': 30.0, 206 | 'total_market_value': 50.0, 207 | 'total_positions': 1, 208 | 'total_profit_or_loss': 20.0 209 | } 210 | } 211 | 212 | portfolio_summary = self.portfolio.projected_market_value(current_prices={'MSFT':{'lastPrice':5.0}}) 213 | self.assertDictEqual(correct_dict, portfolio_summary) 214 | 215 | def test_grab_historical_prices(self): 216 | pass 217 | 218 | def test_portfolio_summary(self): 219 | """Tests the generation of a portfolio summary, for all of the positions.""" 220 | 221 | # Add a position. 222 | self.portfolio.add_position( 223 | symbol='MSFT', 224 | asset_type='equity', 225 | quantity=10, 226 | purchase_price=3.00, 227 | purchase_date='2020-01-31' 228 | ) 229 | 230 | self.portfolio.td_client = self.td_client 231 | 232 | correct_dict = [ 233 | 'projected_market_value', 234 | 'portfolio_weights', 235 | 'portfolio_risk' 236 | ] 237 | 238 | correct_dict = set(correct_dict) 239 | 240 | summary_dict = self.portfolio.portfolio_summary() 241 | 242 | self.assertTrue(correct_dict.issubset(summary_dict)) 243 | 244 | def test_ownership_status(self): 245 | """Tests getting and setting the ownership status.""" 246 | 247 | # Add a position. 248 | self.portfolio.add_position( 249 | symbol='MSFT', 250 | asset_type='equity', 251 | quantity=10, 252 | purchase_price=3.00, 253 | purchase_date='2020-01-31' 254 | ) 255 | 256 | # Should be True, since `purchase_date` was set. 257 | self.assertTrue(self.portfolio.get_ownership_status(symbol='MSFT')) 258 | 259 | # Reassign it. 260 | self.portfolio.set_ownership_status(symbol='MSFT', ownership=False) 261 | 262 | # Should be False. 263 | self.assertFalse(self.portfolio.get_ownership_status(symbol='MSFT')) 264 | 265 | 266 | def tearDown(self) -> None: 267 | """Teardown the Portfolio object.""" 268 | 269 | self.portfolio = None 270 | 271 | 272 | if __name__ == '__main__': 273 | unittest.main() -------------------------------------------------------------------------------- /tests/test_robot.py: -------------------------------------------------------------------------------- 1 | """Unit test module for the PyRobot Object. 2 | 3 | Will perform an instance test to make sure it creates it. Additionally, 4 | it will test different properties and methods of the object. 5 | """ 6 | 7 | import unittest 8 | import pprint 9 | 10 | from unittest import TestCase 11 | from datetime import datetime 12 | from datetime import timezone 13 | from datetime import timedelta 14 | from configparser import ConfigParser 15 | 16 | from pyrobot.trades import Trade 17 | from pyrobot.robot import PyRobot 18 | from pyrobot.portfolio import Portfolio 19 | from pyrobot.stock_frame import StockFrame 20 | 21 | 22 | class PyRobotTest(TestCase): 23 | 24 | """Will perform a unit test for the PyRobot Object.""" 25 | 26 | def setUp(self) -> None: 27 | """Set up the Robot.""" 28 | 29 | # Grab configuration values. 30 | config = ConfigParser() 31 | config.read('configs/config.ini') 32 | 33 | CLIENT_ID = config.get('main', 'CLIENT_ID') 34 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 35 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 36 | self.ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 37 | 38 | self.robot = PyRobot( 39 | client_id = CLIENT_ID, 40 | redirect_uri = REDIRECT_URI, 41 | credentials_path = CREDENTIALS_PATH 42 | ) 43 | 44 | def test_creates_instance_of_session(self): 45 | """Create an instance and make sure it's a robot.""" 46 | 47 | self.assertIsInstance(self.robot, PyRobot) 48 | 49 | def test_create_portofolio(self): 50 | """Call `create_portfolio` and make sure it's a Portfolio.""" 51 | 52 | new_portfolio = self.robot.create_portfolio() 53 | 54 | self.assertIsInstance(new_portfolio, Portfolio) 55 | 56 | 57 | def test_regular_market_open(self): 58 | """Tests whether Market is Open""" 59 | 60 | # Define right now. 61 | right_now = datetime.utcnow().timestamp() 62 | 63 | # Define the start time. 64 | regular_market_start_time = datetime.utcnow().replace( 65 | hour=14, 66 | minute=30, 67 | second=00 68 | ).timestamp() 69 | 70 | # Define the end time. 71 | regular_market_end_time = datetime.utcnow().replace( 72 | hour=21, 73 | minute=00, 74 | second=00 75 | ).timestamp() 76 | 77 | if regular_market_end_time >= right_now >= regular_market_start_time: 78 | open = True 79 | else: 80 | open = False 81 | 82 | self.assertEqual(open, self.robot.regular_market_open) 83 | 84 | 85 | def test_pre_market_open(self): 86 | """Tests whether US Pre-Market is Open""" 87 | 88 | # Define right now. 89 | right_now = datetime.utcnow().timestamp() 90 | 91 | # Define the start time. 92 | pre_market_start_time = datetime.utcnow().replace( 93 | hour=9, 94 | minute=00, 95 | second=00 96 | ).timestamp() 97 | 98 | # Define the end time. 99 | pre_market_end_time = datetime.utcnow().replace( 100 | hour=14, 101 | minute=30, 102 | second=00 103 | ).timestamp() 104 | 105 | if pre_market_end_time >= right_now >= pre_market_start_time: 106 | open = True 107 | else: 108 | open = False 109 | 110 | self.assertEqual(open, self.robot.pre_market_open) 111 | 112 | def test_post_market_open(self): 113 | """Tests whether US Post-Market is Open""" 114 | 115 | # Define right now. 116 | right_now = datetime.utcnow().timestamp() 117 | 118 | # Define the start time. 119 | post_market_start_time = datetime.utcnow().replace( 120 | hour=21, 121 | minute=00, 122 | second=00 123 | ).timestamp() 124 | 125 | # Define the end time. 126 | post_market_end_time = datetime.utcnow().replace( 127 | hour=1, 128 | minute=30, 129 | second=00 130 | ).timestamp() 131 | 132 | if post_market_end_time >= right_now >= post_market_start_time: 133 | open = True 134 | else: 135 | open = False 136 | 137 | self.assertEqual(open, self.robot.post_market_open) 138 | 139 | def test_historical_prices(self): 140 | """Tests Grabbing historical prices.""" 141 | 142 | # Grab historical prices, first define the start date and end date. 143 | start_date = datetime.today() 144 | end_date = start_date - timedelta(days=30) 145 | 146 | # Grab the historical prices. 147 | self.robot.grab_historical_prices( 148 | start=end_date, 149 | end=start_date, 150 | bar_size=1, 151 | bar_type='minute', 152 | symbols=['AAPL'] 153 | ) 154 | 155 | self.assertIn('aggregated', self.robot.historical_prices) 156 | 157 | def test_build_portfolio(self): 158 | """Test building a Portfolio object.""" 159 | 160 | # Create a Portfolio 161 | porfolio_obj = self.robot.create_portfolio() 162 | 163 | self.assertIsInstance(porfolio_obj, Portfolio) 164 | 165 | def test_build_trade(self): 166 | """Test building a Trade object.""" 167 | 168 | # Create a Trade 169 | trade_obj = self.robot.create_trade( 170 | trade_id='long_msft', 171 | enter_or_exit='enter', 172 | long_or_short='short', 173 | order_type='lmt', 174 | price=150.00 175 | ) 176 | 177 | self.assertIsInstance(trade_obj, Trade) 178 | 179 | def test_grab_accounts(self): 180 | """Test grabbing accounts using the robot.""" 181 | 182 | accounts = self.robot.get_accounts(all_accounts=True) 183 | 184 | pprint.pprint(accounts) 185 | 186 | self.assertIsInstance(accounts, list) 187 | 188 | def test_grab_positions(self): 189 | """Test grabbing positions using the robot.""" 190 | 191 | positions = self.robot.get_positions(all_accounts=True) 192 | 193 | pprint.pprint(positions) 194 | 195 | self.assertIsInstance(positions, list) 196 | 197 | def tearDown(self) -> None: 198 | """Teardown the Robot.""" 199 | 200 | self.robot = None 201 | 202 | 203 | if __name__ == '__main__': 204 | unittest.main() -------------------------------------------------------------------------------- /tests/test_stock_frame.py: -------------------------------------------------------------------------------- 1 | """Unit test module for the StockFrame Object. 2 | 3 | Will perform an instance test to make sure it creates it. Additionally, 4 | it will test different properties and methods of the object. 5 | """ 6 | 7 | import unittest 8 | import pandas as pd 9 | from unittest import TestCase 10 | from datetime import datetime 11 | from datetime import timedelta 12 | from configparser import ConfigParser 13 | 14 | from pyrobot.robot import PyRobot 15 | from pyrobot.stock_frame import StockFrame 16 | 17 | 18 | class PyRobotStockFrameTest(TestCase): 19 | 20 | """Will perform a unit test for the StockFrame Object.""" 21 | 22 | def setUp(self) -> None: 23 | """Set up the Stock Frame.""" 24 | 25 | # Grab configuration values. 26 | config = ConfigParser() 27 | config.read('configs/config.ini') 28 | 29 | CLIENT_ID = config.get('main', 'CLIENT_ID') 30 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 31 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 32 | 33 | # Create a robot. 34 | self.robot = PyRobot( 35 | client_id = CLIENT_ID, 36 | redirect_uri = REDIRECT_URI, 37 | credentials_path = CREDENTIALS_PATH 38 | ) 39 | 40 | # Grab historical prices, first define the start date and end date. 41 | start_date = datetime.today() 42 | end_date = start_date - timedelta(days=30) 43 | 44 | # Grab the historical prices. 45 | historical_prices = self.robot.grab_historical_prices( 46 | start=end_date, 47 | end=start_date, 48 | bar_size=1, 49 | bar_type='minute', 50 | symbols=['AAPL','MSFT'] 51 | ) 52 | 53 | # Convert data to a Data Frame. 54 | self.stock_frame = self.robot.create_stock_frame(data=historical_prices['aggregated']) 55 | 56 | def test_creates_instance_of_session(self): 57 | """Create an instance and make sure it's a StockFrame.""" 58 | 59 | self.assertIsInstance(self.stock_frame, StockFrame) 60 | 61 | def test_frame_property(self): 62 | """Test that the `frame` property returns a Pandas DataFrame object.""" 63 | 64 | self.assertIsInstance(self.stock_frame.frame, pd.DataFrame) 65 | self.assertIsInstance(self.stock_frame.frame.index, pd.MultiIndex) 66 | 67 | def test_frame_symbols(self): 68 | """Test that the `frame.index` property contains the specified symbols.""" 69 | 70 | self.assertIn('AAPL', self.stock_frame.frame.index) 71 | self.assertIn('MSFT', self.stock_frame.frame.index) 72 | 73 | def test_symbol_groups_property(self): 74 | """Test that the `symbol_groups` property returns a Pandas DataFrameGroupBy object.""" 75 | 76 | self.assertIsInstance(self.stock_frame.symbol_groups, pd.core.groupby.DataFrameGroupBy) 77 | 78 | def test_symbol_rolling_groups_property(self): 79 | """Test that the `symbol_rolling_groups` property returns a Pandas RollingGroupBy object.""" 80 | 81 | self.assertIsInstance(self.stock_frame.symbol_rolling_groups(size=15), pd.core.window.RollingGroupby) 82 | 83 | def test_add_row(self): 84 | """Test adding a new row to our data frame.""" 85 | 86 | # Define a new row. 87 | new_row_dict = { 88 | 'AAPL':{ 89 | 'openPrice':100.00, 90 | 'closePrice':100.00, 91 | 'highPrice':100.00, 92 | 'lowPrice':100.00, 93 | 'askSize':100, 94 | 'bidSize':100, 95 | 'quoteTimeInLong':1586390399572 96 | } 97 | 98 | } 99 | 100 | # Add the row. 101 | self.stock_frame.add_rows(data=new_row_dict) 102 | 103 | # Create a timestamp. 104 | time_stamp_parsed = pd.to_datetime(1586390399572, unit='ms', origin='unix') 105 | index_tuple = ('AAPL', time_stamp_parsed) 106 | 107 | # Check to see if the Tuple is in the Index. 108 | self.assertIn(index_tuple, self.stock_frame.frame.index) 109 | 110 | def tearDown(self) -> None: 111 | """Teardown the StockFrame.""" 112 | 113 | self.stock_frame = None 114 | 115 | 116 | if __name__ == '__main__': 117 | unittest.main() -------------------------------------------------------------------------------- /tests/test_trades.py: -------------------------------------------------------------------------------- 1 | """Unit test module for the Trade Object. 2 | 3 | Will perform an instance test to make sure it creates it. Additionally, 4 | it will test different properties and methods of the object. 5 | """ 6 | 7 | import unittest 8 | from unittest import TestCase 9 | from configparser import ConfigParser 10 | 11 | from pyrobot.robot import PyRobot 12 | from pyrobot.trades import Trade 13 | 14 | 15 | class PyRobotTradeTest(TestCase): 16 | 17 | """Will perform a unit test for the Trade object.""" 18 | 19 | def setUp(self) -> None: 20 | """Set up the Trade Object.""" 21 | 22 | # Grab configuration values. 23 | config = ConfigParser() 24 | config.read('configs/config.ini') 25 | 26 | CLIENT_ID = config.get('main', 'CLIENT_ID') 27 | REDIRECT_URI = config.get('main', 'REDIRECT_URI') 28 | CREDENTIALS_PATH = config.get('main', 'JSON_PATH') 29 | self.ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') 30 | 31 | # Create a new instance of a robot. 32 | self.robot = PyRobot( 33 | client_id = CLIENT_ID, 34 | redirect_uri = REDIRECT_URI, 35 | credentials_path = CREDENTIALS_PATH 36 | ) 37 | 38 | def test_create_new_market_order(self): 39 | """Create a new market order.""" 40 | 41 | # Create a new Trade Object. 42 | new_trade = self.robot.create_trade( 43 | trade_id='test_1', 44 | enter_or_exit='enter', 45 | long_or_short='short', 46 | order_type='mkt' 47 | ) 48 | 49 | self.assertIsInstance(new_trade, Trade) 50 | self.assertEqual(new_trade.order_type, 'mkt') 51 | 52 | def test_create_new_limit_order(self): 53 | """Create a new limit order.""" 54 | 55 | # Create a new Trade Object. 56 | new_trade = self.robot.create_trade( 57 | trade_id='test_1', 58 | enter_or_exit='enter', 59 | long_or_short='short', 60 | order_type='lmt' 61 | ) 62 | 63 | self.assertIsInstance(new_trade, Trade) 64 | self.assertEqual(new_trade.order_type, 'lmt') 65 | 66 | def test_create_new_stop_order(self): 67 | """Create a new stop order.""" 68 | 69 | # Create a new Trade Object. 70 | new_trade = self.robot.create_trade( 71 | trade_id='test_1', 72 | enter_or_exit='enter', 73 | long_or_short='short', 74 | order_type='stop' 75 | ) 76 | 77 | self.assertIsInstance(new_trade, Trade) 78 | self.assertEqual(new_trade.order_type, 'stop') 79 | 80 | def test_add_instrument(self): 81 | """Tests adding an instrument to an order after creating it.""" 82 | 83 | # Create a new Trade Object. 84 | new_trade = self.robot.create_trade( 85 | trade_id='test_1', 86 | enter_or_exit='enter', 87 | long_or_short='long', 88 | order_type='lmt', 89 | price=12.00 90 | ) 91 | 92 | # Define the Order Leg it should be. 93 | order_leg = { 94 | "instruction": 'BUY', 95 | "quantity": 2, 96 | "instrument": { 97 | "symbol": 'MSFT', 98 | "assetType": 'EQUITY' 99 | } 100 | } 101 | 102 | # Add an instrument to the Trade. 103 | new_trade.instrument(symbol='MSFT', quantity=2, asset_type='EQUITY') 104 | self.assertDictEqual(new_trade.order['orderLegCollection'][0], order_leg) 105 | 106 | def test_add_stop_loss_percentage(self): 107 | """Tests adding a stop Loss Order to an exisiting Limit Order.""" 108 | 109 | # Create a new Trade Object. 110 | new_trade = self.robot.create_trade( 111 | trade_id='test_1', 112 | enter_or_exit='enter', 113 | long_or_short='long', 114 | order_type='lmt', 115 | price=12.00 116 | ) 117 | 118 | # Add a new instrument. 119 | new_trade.instrument(symbol='MSFT',quantity=2, asset_type='EQUITY') 120 | 121 | # Add a new percentage Stop Loss. 122 | new_trade.add_stop_loss(stop_size=.10, percentage=True) 123 | 124 | stop_loss_order = { 125 | "orderType": "STOP", 126 | "session": "NORMAL", 127 | "duration": "DAY", 128 | "stopPrice": 10.8, 129 | "orderStrategyType": "SINGLE", 130 | "orderLegCollection": [ 131 | { 132 | "instruction": 'SELL', 133 | "quantity": 2, 134 | "instrument": { 135 | "symbol": 'MSFT', 136 | "assetType": 'EQUITY' 137 | } 138 | } 139 | ] 140 | } 141 | 142 | self.assertEqual(new_trade.order_type, 'lmt') 143 | self.assertIn('childOrderStrategies', new_trade.order) 144 | self.assertDictEqual(new_trade.stop_loss_order, stop_loss_order) 145 | 146 | def test_add_stop_loss_dollar(self): 147 | """Tests adding a stop Loss Order to an exisiting Limit Order.""" 148 | 149 | # Create a new Trade Object. 150 | new_trade = self.robot.create_trade( 151 | trade_id='test_1', 152 | enter_or_exit='enter', 153 | long_or_short='long', 154 | order_type='lmt', 155 | price=12.00 156 | ) 157 | 158 | # Add a new instrument. 159 | new_trade.instrument(symbol='MSFT', quantity=2, asset_type='EQUITY') 160 | 161 | # Add a new stop Loss. 162 | new_trade.add_stop_loss(stop_size=.10, percentage=False) 163 | 164 | stop_loss_order = { 165 | "orderType": "STOP", 166 | "session": "NORMAL", 167 | "duration": "DAY", 168 | "stopPrice": 11.90, 169 | "orderStrategyType": "SINGLE", 170 | "orderLegCollection": [ 171 | { 172 | "instruction": 'SELL', 173 | "quantity": 2, 174 | "instrument": { 175 | "symbol": 'MSFT', 176 | "assetType": 'EQUITY' 177 | } 178 | } 179 | ] 180 | } 181 | 182 | self.assertEqual(new_trade.order_type, 'lmt') 183 | self.assertIn('childOrderStrategies', new_trade.order) 184 | self.assertDictEqual(new_trade.stop_loss_order, stop_loss_order) 185 | 186 | # def test_price_calculation(self): 187 | # """Tests calculating the new price for Stop Orders.""" 188 | 189 | # new_price = Trade()._calculate_new_price(price=12.00,adjustment=.1, percentage=True) 190 | 191 | def tearDown(self) -> None: 192 | """Teardown the Robot.""" 193 | 194 | self.robot = None 195 | 196 | if __name__ == '__main__': 197 | unittest.main() --------------------------------------------------------------------------------