Initial checkin
This commit is contained in:
71
README.md
Normal file
71
README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Charls Schwab Quote recording bot
|
||||||
|
|
||||||
|
## Expiry
|
||||||
|
* Access token expires in 30 Min.
|
||||||
|
* Refresh token expires in ?
|
||||||
|
|
||||||
|
## File - Descriptions
|
||||||
|
### **`appCredentials.dat`**
|
||||||
|
* Base64 encoded App Credentials - `"{app key}:{secret}"`
|
||||||
|
* Manually maintained
|
||||||
|
|
||||||
|
### **`appKey.dat`**
|
||||||
|
* only the application's AppKey
|
||||||
|
* Manually maintained
|
||||||
|
|
||||||
|
#### **`allSymbols.json`**
|
||||||
|
* List of all Symbols to get Data for
|
||||||
|
* Manually maintained
|
||||||
|
|
||||||
|
### **`access_token.dat`**
|
||||||
|
* Current Access Token
|
||||||
|
* written by `getNewAccessToken.sh`
|
||||||
|
|
||||||
|
### **`accessTokenReqResp.json`**
|
||||||
|
* Full JSON response of the last Access Token request
|
||||||
|
* written by `getNewAccessToken.sh`
|
||||||
|
|
||||||
|
### **`refresh_token.dat`**
|
||||||
|
* Current refresh token
|
||||||
|
* written by `getNewRefreshToken.sh`
|
||||||
|
|
||||||
|
### **`refreshTokenReqResp.json`**
|
||||||
|
* Full JSON response of the last Refresh Token request
|
||||||
|
* written by `getNewRefreshToken.sh`
|
||||||
|
|
||||||
|
### **`codeReceiveServer.py`**
|
||||||
|
* Python HTTPS web server to receive and output the Authentication code from the Schwab Website callback
|
||||||
|
* This web server only runs temporarily and ends after receiving a request
|
||||||
|
|
||||||
|
### **`cert.pem`**
|
||||||
|
* self signed certificate for the temporary HTTPS server that receives the code.
|
||||||
|
* Manually maintained with:
|
||||||
|
`openssl req -new -x509 -keyout cert.pem -out cert.pem -days 365 -nodes`
|
||||||
|
*Answer all questions hitting enter.*
|
||||||
|
|
||||||
|
## **`data/`**
|
||||||
|
* Folder containing date sub-folders for the output data
|
||||||
|
* Created and maintained by `recordSingleSymbolSingleRun.sh`
|
||||||
|
|
||||||
|
### **`getNewAccessToken.sh`**
|
||||||
|
* Gets a new access Token and saves it to file `access_token.dat`
|
||||||
|
|
||||||
|
### **`getNewRefreshToken.sh`**
|
||||||
|
* Gets a new refresh Token and saves it to file `refresh_token.dat`
|
||||||
|
|
||||||
|
### **`recordAllSymbolsSingleRun.sh`**
|
||||||
|
* Runs `recordSingleSymbolSingleRun.sh` for every Symbol in file `allSymbols.json`
|
||||||
|
|
||||||
|
### **`recordQuoteSingleSymbolSingleRun.sh`**
|
||||||
|
* requests stock quote data for a single symbol and writes output into the `data/*` Folder structure.
|
||||||
|
|
||||||
|
### **`recordOptionChainSingleSymbolSingleRun.sh`**
|
||||||
|
* requests option chain data for a single symbol and writes output into the `data/*` Folder structure.
|
||||||
|
|
||||||
|
|
||||||
|
# NEW CHANGES, NEED TO DOCUMENT
|
||||||
|
jq <<< '{ "numberOfContracts":'$(( $(jq '.numberOfContracts' < ITM.json) + $(jq '.numberOfContracts' < OTM.json) ))' }' > numberOfContracts.tmp
|
||||||
|
jq '. | del(.numberOfContracts) | map_values(select(type!="object"))' < ITM.json > CommonDetail.tmp
|
||||||
|
jq '. | map_values(select(type=="object"))' < ITM.json > ITMo.tmp
|
||||||
|
jq '. | map_values(select(type=="object"))' < OTM.json > OTMo.tmp
|
||||||
|
jq -s '.[0] * .[1] * .[2] * .[3]' numberOfContracts.tmp CommonDetail.tmp ITMo.tmp OTMo.tmp | less
|
||||||
101
allSymbols.json.example
Normal file
101
allSymbols.json.example
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Symbol": "AAPL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "ACN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "AMZN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "BAC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "CMG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "CRM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "CSCO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "DIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "EDV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "EFA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "EWW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "FSLR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "GME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "GOOG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "HD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "INTC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "IWM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "KMB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "KO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "LVS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "MCD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "META"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "NFLX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "PM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "PZZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "QQQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "SLV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "SPY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "TLT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "$VIX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "VXX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "WMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Symbol": "$XSP"
|
||||||
|
}
|
||||||
|
]
|
||||||
30
codeReceiveServer.py
Executable file
30
codeReceiveServer.py
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/python
|
||||||
|
|
||||||
|
import http.server, ssl, socketserver
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
context.load_cert_chain("cert.pem") # PUT YOUR cert.pem HERE
|
||||||
|
server_address = ("0.0.0.0", 443) # CHANGE THIS IP & PORT
|
||||||
|
#handler = http.server.SimpleHTTPRequestHandler
|
||||||
|
|
||||||
|
#class MyHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
class MyHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
parsed_url = urllib.parse.urlparse(self.path)
|
||||||
|
code = urllib.parse.parse_qs(parsed_url.query).get('code', [None])[0]
|
||||||
|
if code:
|
||||||
|
print(code)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write("OK.".encode("utf-8"))
|
||||||
|
self.wfile.flush()
|
||||||
|
self.wfile.close()
|
||||||
|
quit()
|
||||||
|
else:
|
||||||
|
print("Invalid or missing authorization code")
|
||||||
|
|
||||||
|
with socketserver.TCPServer(server_address, MyHandler) as httpd:
|
||||||
|
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
|
||||||
|
httpd.serve_forever()
|
||||||
18
getNewAccessToken.sh
Executable file
18
getNewAccessToken.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
curl -s -X POST https://api.schwabapi.com/v1/oauth/token \
|
||||||
|
-H "Authorization: Basic $(<appCredentials.dat)" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
-d "grant_type=refresh_token&refresh_token=$(<refresh_token.dat)" > temp/accessTokenReqResp.$$.json
|
||||||
|
jq -e -r .access_token temp/accessTokenReqResp.$$.json > access_token.dat
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
errMsg="ERROR: GETTING THE ACCESS TOKEN FAILED! $(<temp/accessTokenReqResp.$$.json)"
|
||||||
|
systemd-cat -t "$0" -p err <<< ${errMsg}
|
||||||
|
>&2 echo "${errMsg}"
|
||||||
|
curl -d "" -X POST "https://deadswitch.kawomi.com/api/v1?topic=trade.schwab.error.getNewAccessToken&secs=1&email=joe@kawomi.com"
|
||||||
|
fi
|
||||||
|
#echo Access Token: $(<access_token.dat)
|
||||||
|
#jq -r .refresh_token ./accessTokenReqResp.json > refresh_token.dat
|
||||||
|
#echo Refresh Token: $(<refresh_token.dat)
|
||||||
|
#rm ./refreshTokenReqResp.json
|
||||||
|
|
||||||
18
getNewRefreshToken.sh
Executable file
18
getNewRefreshToken.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
myIP=$(ip a | grep 'inet.*192.168.10' | awk -F "[ /]*" '{print $3}')
|
||||||
|
curl -s "https://ddns.kawomi.com/?domain=cstradebotcallback&address=${myIP}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo OPEN URL: "https://api.schwabapi.com/v1/oauth/authorize?client_id=$(<appKey.dat)&redirect_uri=https://cstradebotcallback.ddns.kawomi.com/"
|
||||||
|
echo
|
||||||
|
theCode=$(sudo ./codeReceiveServer.py)
|
||||||
|
echo Code is $theCode
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.schwabapi.com/v1/oauth/token" \
|
||||||
|
-H "Authorization: Basic $(<appCredentials.dat)" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
-d "grant_type=authorization_code&code=${theCode}&redirect_uri=https://cstradebotcallback.ddns.kawomi.com/" > temp/refreshTokenReqResp.$$.json
|
||||||
|
jq -r .refresh_token temp/refreshTokenReqResp.$$.json > refresh_token.dat
|
||||||
|
echo Refresh Token: $(<refresh_token.dat)
|
||||||
|
|
||||||
35
recordAllSymbolsSingleRun.sh
Executable file
35
recordAllSymbolsSingleRun.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
|
||||||
|
systemd-cat -t "$0" -p info <<< "Started"
|
||||||
|
|
||||||
|
./getNewAccessToken.sh
|
||||||
|
|
||||||
|
jq -r '.[].Symbol' allSymbols.json | sort | while read theSymbol; do
|
||||||
|
# I am not interested in detail quotes at the moment
|
||||||
|
#./recordQuoteSingleSymbolSingleRun.sh ${theSymbol}
|
||||||
|
#if [[ $? -ne 0 ]]; then
|
||||||
|
# touch temp/hadErrors.$$.tmp
|
||||||
|
#fi
|
||||||
|
|
||||||
|
if [[ '$XSP' = ${theSymbol} ]]; then
|
||||||
|
./recordOptionChainSingleSymbolSpecialRun.sh ${theSymbol}
|
||||||
|
else
|
||||||
|
./recordOptionChainSingleSymbolSingleRun.sh ${theSymbol}
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
touch temp/hadErrors.$$.tmp
|
||||||
|
|
||||||
|
# Working around error in $XSP with special handling
|
||||||
|
# if [[ '$XSP' != ${theSymbol} ]]; then
|
||||||
|
# touch temp/hadErrors.$$.tmp
|
||||||
|
# fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -e temp/hadErrors.$$.tmp ]]; then
|
||||||
|
rm temp/hadErrors.$$.tmp
|
||||||
|
curl -d "" -X POST "https://deadswitch.kawomi.com/api/v1?topic=trade.schwab.error.recordAllSymbolsSingleRun&secs=1&email=joe@kawomi.com"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemd-cat -t "$0" -p info <<< "Finished"
|
||||||
26
recordOptionChainSingleSymbolSingleRun.sh
Executable file
26
recordOptionChainSingleSymbolSingleRun.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
symbol=$1
|
||||||
|
outDir=data/$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
mkdir -p "${outDir}"
|
||||||
|
mkdir -p temp
|
||||||
|
|
||||||
|
curl -s -X GET "https://api.schwabapi.com/marketdata/v1/chains?symbol=${symbol}" \
|
||||||
|
-H "Authorization: Bearer $(<access_token.dat)" > temp/optionChain.${symbol}.tmp.$$.json
|
||||||
|
|
||||||
|
jqQuery=".symbol"
|
||||||
|
symbolCheck=$(jq -r ${jqQuery} < temp/optionChain.${symbol}.tmp.$$.json)
|
||||||
|
if [[ "${symbol}" != "${symbolCheck}" ]]; then
|
||||||
|
errMsg="ERROR: THE RESPONSE IS INCORRECT! Symbol: _${symbol}_ Check: _${symbolCheck}_ check file _temp/optionChain.${symbol}.tmp.$$.json_"
|
||||||
|
systemd-cat -t "$0 $1" -p err <<< ${errMsg}
|
||||||
|
>&2 echo "${errMsg}"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
optionChainFileName=$(date "+%Y-%m-%d %H_%M_%S")_optionChain.${symbol}.json
|
||||||
|
#echo writing to file "${outDir}/${optionChainFileName}"
|
||||||
|
pbzip2 -9 -c < temp/optionChain.${symbol}.tmp.$$.json > "${outDir}/${optionChainFileName}.bz2"
|
||||||
|
rm temp/optionChain.${symbol}.tmp.$$.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
43
recordOptionChainSingleSymbolSpecialRun.sh
Executable file
43
recordOptionChainSingleSymbolSpecialRun.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
# This is for big chains, that cause the Schwab API to error out (like $XSP)
|
||||||
|
# I add the range parameter (ITM/NTM/OTM) so that I get less data per request
|
||||||
|
#
|
||||||
|
symbol=$1
|
||||||
|
outDir=data/$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
mkdir -p "${outDir}"
|
||||||
|
mkdir -p temp
|
||||||
|
|
||||||
|
for range in ITM OTM NTM; do
|
||||||
|
curl -s -X GET "https://api.schwabapi.com/marketdata/v1/chains?symbol=${symbol}&range=${range}" \
|
||||||
|
-H "Authorization: Bearer $(<access_token.dat)" > temp/optionChain.${symbol}.${range}.tmp.$$.json
|
||||||
|
|
||||||
|
jqQuery=".symbol"
|
||||||
|
symbolCheck=$(jq -r ${jqQuery} < temp/optionChain.${symbol}.${range}.tmp.$$.json)
|
||||||
|
if [[ "${symbol}" != "${symbolCheck}" ]]; then
|
||||||
|
errMsg="ERROR: THE RESPONSE IS INCORRECT! Symbol: _${symbol}_ Check: _${symbolCheck}_ check file _temp/optionChain.${symbol}.${range}.tmp.$$.json_"
|
||||||
|
systemd-cat -t "$0 $1" -p err <<< ${errMsg}
|
||||||
|
>&2 echo "${errMsg}"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
optionChainPartFileName=$(date "+%Y-%m-%d %H_%M_%S")_optionChain.${symbol}.${range}.json
|
||||||
|
pbzip2 -9 -c < temp/optionChain.${symbol}.${range}.tmp.$$.json > "${outDir}/${optionChainPartFileName}.bz2"
|
||||||
|
done
|
||||||
|
|
||||||
|
###Paste together the parts, the NTM part, I don't think I need, but I will save it just in case..
|
||||||
|
# sum up the number of Contracts
|
||||||
|
|
||||||
|
ITMtempFile=temp/optionChain.${symbol}.ITM.tmp.$$.json
|
||||||
|
OTMtempFile=temp/optionChain.${symbol}.OTM.tmp.$$.json
|
||||||
|
jq <<< '{ "glued":true, "numberOfContracts":'$(( $(jq '.numberOfContracts' < ${ITMtempFile}) + $(jq '.numberOfContracts' < ${OTMtempFile}) ))' }' > temp/numberOfContracts.$$.tmp
|
||||||
|
jq '. | del(.numberOfContracts) | map_values(select(type!="object"))' < ${ITMtempFile} > temp/CommonDetail.$$.tmp
|
||||||
|
jq '. | map_values(select(type=="object"))' < ${ITMtempFile} > ${ITMtempFile}.objOnly
|
||||||
|
jq '. | map_values(select(type=="object"))' < ${OTMtempFile} > ${OTMtempFile}.objOnly
|
||||||
|
jq -s '.[0] * .[1] * .[2] * .[3]' temp/numberOfContracts.$$.tmp temp/CommonDetail.$$.tmp ${ITMtempFile}.objOnly ${OTMtempFile}.objOnly > temp/optionChain.${symbol}.tmp.$$.json
|
||||||
|
|
||||||
|
optionChainFileName=$(date "+%Y-%m-%d %H_%M_%S")_optionChain.${symbol}.json
|
||||||
|
pbzip2 -9 -c < temp/optionChain.${symbol}.tmp.$$.json > "${outDir}/${optionChainFileName}.bz2"
|
||||||
|
|
||||||
|
rm temp/*$$*
|
||||||
26
recordQuoteSingleSymbolSingleRun.sh
Executable file
26
recordQuoteSingleSymbolSingleRun.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#set -x
|
||||||
|
symbol=$1
|
||||||
|
outDir=data/$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
mkdir -p "${outDir}"
|
||||||
|
mkdir -p temp
|
||||||
|
|
||||||
|
curl -s -X GET "https://api.schwabapi.com/marketdata/v1/${symbol}/quotes?fields=quote%2Creference" \
|
||||||
|
-H "Authorization: Bearer $(<access_token.dat)" > temp/quote.${symbol}.tmp.$$.json
|
||||||
|
|
||||||
|
jqQuery=".\"${symbol}\".symbol"
|
||||||
|
symbolCheck=$(jq -r ${jqQuery} < temp/quote.${symbol}.tmp.$$.json)
|
||||||
|
if [[ "${symbol}" != "${symbolCheck}" ]]; then
|
||||||
|
errMsg="ERROR: THE RESPONSE IS INCORRECT! Symbol: _${symbol}_ Check: _${symbolCheck}_ check file _temp/quote.${symbol}.tmp.$$.json_"
|
||||||
|
systemd-cat -t "$0 $1" -p err <<< ${errMsg}
|
||||||
|
>&2 echo "${errMsg}"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
quoteFileName=$(date "+%Y-%m-%d %H_%M_%S")_quote.${symbol}.json
|
||||||
|
#echo writing to file "${outDir}/${quoteFileName}"
|
||||||
|
pbzip2 -9 -c < temp/quote.${symbol}.tmp.$$.json > "${outDir}/${quoteFileName}.bz2"
|
||||||
|
rm temp/quote.${symbol}.tmp.$$.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user