Quickly convert Tradovate API Docs into a JavaScript API

Hey folks, more or less a PSA / helpful project for the community.

I found it a bit tedious to manually build functions in JavaScript by hand based on the Tradovate docs; to try to expedite my development process, I wrote a brief Python script to auto-generate a JavaScript API directly from the docs so that I could focus more on the actual trading logic rather than infrastructure. For those coding in JavaScript with some light Python exposure, hoping this might be helpful.

Download the OpenAPI (Swagger-based) specification from api.tradovate.com, store it in the same folder as the Python script, run the script, and you’ll have all of the current API endpoints ready-to-use in a class-based JavaScript file. Because it’s class based, you’ll need to make some slight modifications based on the Tradovate WebSocket tutorial to ensure that the request() function is compatible and that the Socket function() can be extended / adapted to this class-based approach (or feel free to adapt my script to the Tradovate example!).

The script (and JSDoc adaption) is mainly intended to be run / utilized in VS Code. Further, this is intended for the WebSocket API, and would need further effort to make it more compatible with the REST API. Would recommend that, if following the class-based approach, you create a generic “Socket” class, extend it with the API endpoints as an “API” subclass, then further extend that subclass as two distinct “MarketDataSocket” and “TradovateSocket” subclasses per the Tradovate tutorials.

@BWeis @Alexander If this isn’t appropriate to share with the community, please let me know and I can redact the post.

# PARSE THE TRADOVATE SWAGGER API SPEC INTO A JAVASCRIPT CLASS-BASED API
# One-time run script, e.g., -python swagger-to-js.py in terminal or in VS Code
# Save swagger.json from the Tradovate API specification on https://api.tradovate.com/
# Store swagger.json in the same folder as this script

# Import the required libraries
import json
import os
import sys

# Parameters
# The super class should be whatever you call your parent WebSocket class, and is where you define your
# generic WebSocket methods (i.e., request(), connect(), etc. per the Tradovate tutorials)
# The subclass will extend the super class and add API endpoints to it in a clean parent / child structure,
# e.g., parent/child such as auth/accessTokenRequest
superclass = 'API'
subclass = 'Socket'
readfile = 'swagger.json'
writefile = 'api.js'

# Opening the Swagger JSON file
f = open(os.path.join(os.path.dirname(sys.argv[0]), readfile))

# Returns JSON object as a dictionary
data = json.load(f)

# Export dictionary: We will aggregate endpoint "families" (e.g., auth, order) under a dict key,
# and endpoints themselves as functions underneath the family
# This will lead to API interrogation with the structure:
# <WebSocket Class>.<family>.<endpoint>({ query: { params }, body: { params }})
consolidated = {}

# Helper function: Nested Schema interprets the schema for documentation recursively
# Requires the 'docs' array to be predefined externally.
def nestedSchema(data,target,nest=''):

    params = data['components']['schemas'][target]['properties']
    for key, val in params.items():
        # print(val)
        if 'type' in val:
            docs.append('* @param {{{t}}} {n}{k} {t}'.format(t=val['type'], k=key,n=nest + '.' if nest != '' else ''))
        elif '$ref' in val:
            print(val['$ref'], len(docs))
            recurse = val['$ref'].split('/')[-1]
            nestedSchema(data, target=recurse, nest=key)

    return docs

# Iterating through the JSON dictionary
for url, value in data['paths'].items():

    # Determine if it is a GET or POST request
    method = 'get' if 'get' in value else 'post' if 'post' in value else 'error'
    
    # Extract the endpoint family from the main URL path
    [family, *other] = [x for x in url.split('/') if x]

    # If it is a new prefix, store it, this will be used for a parent object
    if family not in consolidated:
        consolidated[family] = []

    # Helper variable for readability
    request = value[method]
    operation = request['operationId']
    description = request['description']
    
    # Build the query schema if applicable
    query = []
    if 'parameters' in request:
        query = [param['name'] for param in request['parameters']]

    # Build the body schema if applicable
    # Build the type / documentation definitions if applicable
    body = []
    docs = []
    if 'requestBody' in request:
        target = request['requestBody']['content']['application/json']['schema']['$ref'].split('/')[-1]
        params = data['components']['schemas'][target]['properties']
        body = [key for key, val in params.items()]
        docs = nestedSchema(data,target)  

    # Build each API endpoint definition
    # Class Based Approach
    # Note: Pre-define any parameters as "undefined" in order to filter them out of the request if not applicable.
    # The endpoint function has the structure:
    # <WebSocket Class>.<family>.<endpoint>({ query: { params }, body: { params }})
    # The url key is predefined, you can overwrite it but this will defeat the purpose of a unique function for the API
    # The intent is to generalize the API so that any API functional request can be passed through the "request()" function
    # The documentation is (loosely) generated as well based on JSDoc (to be viewed in VSCode) and based on the docs provided on api.tradovate.com
    # but is not necessarily complete; just referential to get you started.
    definition = '''/**
*
* {d}{dc}
*
*/
    {o}: async function({{
    url = '{u}',
    query = {{ {q}{q1} }},
    body = {{ {b}{b1} }}
}} = {{ }}) {{
    return await API.request({{ url, query, body }})
}}'''.format(
        d=description,
        dc='\n' + '\n'.join(docs) if len(docs) else '',
        o=operation,
        q=': undefined, '.join(query),
        q1=': undefined' if len(query) > 0 else '',
        b=': undefined, '.join(body),
        b1=': undefined' if len(body) > 0 else '',
        u=url[1:].lower()
    )

    consolidated[family].append(definition)

# Set up the output string
# Note: We need a request() function to interface with the API, this is your own definition
# This creates the template superclass and subclass with the API endpoints and a placeholder request() 
# function, but you need to work on your end to integrate it with whatever program you are building.
# Please note, this is a CLASS-BASED approach, not a FUNCTIONAL approach, so you may need to modify the 
# example JS code provided on Tradovate's GitHub repo.
output = '''export class {c} {{
    constructor() {{
        this.abc = 0;
    }}

    // TODO: Implement your own definition of request()
    // The intent is structured off the JavaScript WebSocket docs
    // https://github.com/tradovate/example-api-js/tree/main/tutorial/WebSockets
    request({{ query, body, url }}) {{
        this.abc += 1;
        console.log(this.abc, query, body, url);
    }}
}} 
export class {s} extends {c} {{
    constructor() {{
        super();
        let API = this;\n'''.format(s=subclass,c=superclass) + \
        '\n\t'.join(['''this.{f} = {{\n {__} \n}}'''.format(
            f=family,
            __=',\n\n'.join(paths)
        ) for family, paths in consolidated.items()]) + '}\n}'

# Closing the input file
f.close()

# Write the output file
# Note: Writes to the same directory you are in
# Note: Some of the tabbing / formatting may be "off", if so, use the below in VS Code:
# On Windows Shift + Alt + F
# On Mac Shift + Option + F
# On Ubuntu Ctrl + Shift + I
o = open(os.path.join(os.path.dirname(sys.argv[0]), writefile), 'w')
o.write(output)
o.close()
2 Likes

By all means, sharing is welcome! I’m sure many users will appreciate this script.