mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
OpenBB Script Routines major improvement (#5005)
* Allow to set variables and do loops * Fix mypy errors * improve code * mypy * pylint * accepts Openbb date keywords like 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY * warn fix * add documentation for script foreach and vars creation * refactor controller as per James request * black linting * add another warning * Editing some routine enhancements (#5063) * move routine to new file * Move to relativedelta and add some tests * Fix mocking * Get match_and_return_openbb_keyword_date to 100% cov * start testing parse script * remove rogue prints --------- Co-authored-by: DidierRLopes <dro.lopes@campus.fct.unl.pt> * big improvement in routines tests * mypy lint * ruff lint * lint * typo * test relative time * cover edge case of home at end of script * fix edge case for going to home at start * explain better slicing * foreach tests added * lint * allow foreach loops to have different var names that is iterating * foreach more powerful * warning when var in foreach is not used * fail script when varname that iterates foreach loop is never defined * black * lint again * ruff * improve another edge case * fix test and fix a description * fix nft test and routine test * why mutual fund test failing --------- Co-authored-by: James Maslek <jmaslek11@gmail.com>
This commit is contained in:
@@ -45,12 +45,13 @@ from openbb_terminal.core.session.current_user import get_current_user, set_curr
|
||||
from openbb_terminal.core.session.utils import load_dict_to_model
|
||||
from openbb_terminal.helper_funcs import check_non_negative
|
||||
from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.routine_functions import is_reset
|
||||
from openbb_terminal.terminal_controller import (
|
||||
insert_start_slash,
|
||||
replace_dynamic,
|
||||
terminal,
|
||||
)
|
||||
from openbb_terminal.terminal_helper import is_reset, suppress_stdout
|
||||
from openbb_terminal.terminal_helper import suppress_stdout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
special_arguments_values = [
|
||||
|
||||
2
openbb_terminal/miscellaneous/i18n/en.yml
vendored
2
openbb_terminal/miscellaneous/i18n/en.yml
vendored
@@ -584,7 +584,7 @@ en:
|
||||
crypto/defi/anchor: anchor earnings data
|
||||
crypto/tools/aprtoapy: convert apr to apy
|
||||
crypto/tools/il: calculate impermanent loss
|
||||
crypto/nft/fp: collection sales/floor price
|
||||
crypto/nft/fp: collection floor price
|
||||
crypto/nft/collections: top ethereum collections
|
||||
crypto/nft/stats: collection stats
|
||||
crypto/dd/_overview_: Overview
|
||||
|
||||
489
openbb_terminal/routine_functions.py
Normal file
489
openbb_terminal/routine_functions.py
Normal file
@@ -0,0 +1,489 @@
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Match, Optional, Tuple, Union
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from openbb_terminal.rich_config import console
|
||||
|
||||
# pylint: disable=too-many-statements,eval-used,consider-iterating-dictionary
|
||||
# pylint: disable=too-many-branches,too-many-return-statements
|
||||
|
||||
# Necessary for OpenBB keywords
|
||||
MONTHS_VALUE = {
|
||||
"JANUARY": 1,
|
||||
"FEBRUARY": 2,
|
||||
"MARCH": 3,
|
||||
"APRIL": 4,
|
||||
"MAY": 5,
|
||||
"JUNE": 6,
|
||||
"JULY": 7,
|
||||
"AUGUST": 8,
|
||||
"SEPTEMBER": 9,
|
||||
"OCTOBER": 10,
|
||||
"NOVEMBER": 11,
|
||||
"DECEMBER": 12,
|
||||
}
|
||||
|
||||
WEEKDAY_VALUE = {
|
||||
"MONDAY": 0,
|
||||
"TUESDAY": 1,
|
||||
"WEDNESDAY": 2,
|
||||
"THURSDAY": 3,
|
||||
"FRIDAY": 4,
|
||||
"SATURDAY": 5,
|
||||
"SUNDAY": 6,
|
||||
}
|
||||
|
||||
|
||||
def is_reset(command: str) -> bool:
|
||||
"""Test whether a command is a reset command
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : str
|
||||
The command to test
|
||||
|
||||
Returns
|
||||
-------
|
||||
answer : bool
|
||||
Whether the command is a reset command
|
||||
"""
|
||||
if "reset" in command:
|
||||
return True
|
||||
if command == "r":
|
||||
return True
|
||||
if command == "r\n":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def match_and_return_openbb_keyword_date(keyword: str) -> str:
|
||||
"""Return OpenBB keyword into date
|
||||
|
||||
Parameters
|
||||
----------
|
||||
keyword : str
|
||||
String with potential OpenBB keyword (e.g. 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY)
|
||||
|
||||
Returns
|
||||
----------
|
||||
str: Date with format YYYY-MM-DD
|
||||
"""
|
||||
match = re.match(r"^\$(\d+)([A-Z]+)AGO$", keyword)
|
||||
now = datetime.now()
|
||||
if match:
|
||||
integer_value = int(match.group(1))
|
||||
time_unit = match.group(2)
|
||||
if time_unit == "DAYS":
|
||||
return (now - timedelta(days=integer_value)).strftime("%Y-%m-%d")
|
||||
if time_unit == "MONTHS":
|
||||
return (now - relativedelta(months=integer_value)).strftime("%Y-%m-%d")
|
||||
if time_unit == "YEARS":
|
||||
return (now - relativedelta(years=integer_value)).strftime("%Y-%m-%d")
|
||||
|
||||
match = re.match(r"^\$(\d+)([A-Z]+)FROMNOW$", keyword)
|
||||
if match:
|
||||
integer_value = int(match.group(1))
|
||||
time_unit = match.group(2)
|
||||
if time_unit == "DAYS":
|
||||
return (now + timedelta(days=integer_value)).strftime("%Y-%m-%d")
|
||||
if time_unit == "MONTHS":
|
||||
return (now + relativedelta(months=integer_value)).strftime("%Y-%m-%d")
|
||||
if time_unit == "YEARS":
|
||||
return (now + relativedelta(years=integer_value)).strftime("%Y-%m-%d")
|
||||
|
||||
match = re.search(r"\$LAST(\w+)", keyword)
|
||||
if match:
|
||||
time_unit = match.group(1)
|
||||
# Check if it corresponds to a month
|
||||
if time_unit in list(MONTHS_VALUE.keys()):
|
||||
# Calculate the year and month for last month date
|
||||
if now.month > MONTHS_VALUE[time_unit]:
|
||||
# If the current month is greater than the last date month, it means it is this year
|
||||
return datetime(now.year, MONTHS_VALUE[time_unit], 1).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
return datetime(now.year - 1, MONTHS_VALUE[time_unit], 1).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
# Check if it corresponds to a week day
|
||||
if time_unit in list(WEEKDAY_VALUE.keys()):
|
||||
if datetime.weekday(now) > WEEKDAY_VALUE[time_unit]:
|
||||
return (
|
||||
now
|
||||
- timedelta(datetime.weekday(now))
|
||||
+ timedelta(WEEKDAY_VALUE[time_unit])
|
||||
).strftime("%Y-%m-%d")
|
||||
return (
|
||||
now
|
||||
- timedelta(7)
|
||||
- timedelta(datetime.weekday(now))
|
||||
+ timedelta(WEEKDAY_VALUE[time_unit])
|
||||
).strftime("%Y-%m-%d")
|
||||
|
||||
match = re.search(r"\$NEXT(\w+)", keyword)
|
||||
if match:
|
||||
time_unit = match.group(1)
|
||||
# Check if it corresponds to a month
|
||||
if time_unit in list(MONTHS_VALUE.keys()):
|
||||
# Calculate the year and month for next month date
|
||||
if now.month < MONTHS_VALUE[time_unit]:
|
||||
# If the current month is greater than the last date month, it means it is this year
|
||||
return datetime(now.year, MONTHS_VALUE[time_unit], 1).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
return datetime(now.year + 1, MONTHS_VALUE[time_unit], 1).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
|
||||
# Check if it corresponds to a week day
|
||||
if time_unit in list(WEEKDAY_VALUE.keys()):
|
||||
if datetime.weekday(now) < WEEKDAY_VALUE[time_unit]:
|
||||
return (
|
||||
now
|
||||
- timedelta(datetime.weekday(now))
|
||||
+ timedelta(WEEKDAY_VALUE[time_unit])
|
||||
).strftime("%Y-%m-%d")
|
||||
return (
|
||||
now
|
||||
+ timedelta(7)
|
||||
- timedelta(datetime.weekday(now))
|
||||
+ timedelta(WEEKDAY_VALUE[time_unit])
|
||||
).strftime("%Y-%m-%d")
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def parse_openbb_script(
|
||||
raw_lines: List[str],
|
||||
script_inputs: Optional[List[str]] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Parse .openbb script
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_lines : List[str]
|
||||
Lines from .openbb script
|
||||
script_inputs: str, optional
|
||||
Inputs to the script that come externally
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Error that occurred - if empty means no error
|
||||
str
|
||||
Processed string from .openbb script that can be run by the OpenBB Terminal
|
||||
"""
|
||||
ROUTINE_VARS: Dict[str, Union[str, List[str]]] = dict()
|
||||
if script_inputs:
|
||||
ROUTINE_VARS["$ARGV"] = script_inputs
|
||||
|
||||
## PRE PROCESSING
|
||||
# Remove reset commands, comments, empty lines and trailing/leading whitespaces
|
||||
raw_lines = [
|
||||
x.strip()
|
||||
for x in raw_lines
|
||||
if (not is_reset(x)) and ("#" not in x) and x.strip()
|
||||
]
|
||||
|
||||
## LOOK FOR NEW VARIABLES BEING DECLARED FROM USERS
|
||||
lines_without_declarations = list()
|
||||
for line in raw_lines:
|
||||
# Check if this line has a variable attribution
|
||||
# This currently allows user to override ARGV parameter
|
||||
if "$" in line and "=" in line:
|
||||
match = re.search(r"\$(\w+)\s*=\s*([\w\d,-.\s]+)", line)
|
||||
if match:
|
||||
VAR_NAME = match.group(1)
|
||||
VAR_VALUES = match.group(2)
|
||||
ROUTINE_VARS["$" + VAR_NAME] = (
|
||||
VAR_VALUES if "," not in VAR_VALUES else VAR_VALUES.split(",")
|
||||
)
|
||||
|
||||
# Just throw a warning when user uses wrong convention
|
||||
numdollars = len(re.findall(r"\$", line))
|
||||
if numdollars > 1:
|
||||
console.print(
|
||||
f"The variable {VAR_NAME} should not be declared as "
|
||||
f"{'$' * numdollars}{VAR_NAME}. Instead it will be "
|
||||
f"converted into ${VAR_NAME}."
|
||||
)
|
||||
|
||||
else:
|
||||
lines_without_declarations.append(line)
|
||||
else:
|
||||
lines_without_declarations.append(line)
|
||||
|
||||
# At this stage our ROUTINE_VARS should be completed coming from external AND from internal
|
||||
# Now we want to replace the ROUTINE_VARS to where applicable throughout the .openbb script
|
||||
# Due to this implementation, a variable declared at the end will still be effective
|
||||
|
||||
lines_with_vars_replaced = list()
|
||||
foreach_loop_found = False
|
||||
for line in lines_without_declarations:
|
||||
# Save temporary line to ensure that all vars get replaced by correct vars
|
||||
templine = line
|
||||
|
||||
# Found 'end' keyword which means that a loop has terminated
|
||||
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
|
||||
# Check whether the foreach loop has started or not
|
||||
if not foreach_loop_found:
|
||||
return (
|
||||
"[red]The script has a foreach loop that terminates before it gets started. "
|
||||
"Add the keyword 'foreach' to explicitly start loop[/red]",
|
||||
"",
|
||||
)
|
||||
foreach_loop_found = False
|
||||
|
||||
else:
|
||||
# Found 'foreach' keyword which means there needs to be a matching 'end'
|
||||
if re.search(r"foreach", line, re.IGNORECASE):
|
||||
foreach_loop_found = True
|
||||
|
||||
# Regular expression pattern to match variables starting with $
|
||||
pattern = r"(?<!\$)(\$(\w+)(\[[^]]*\])?)(?![^\[]*\])"
|
||||
|
||||
# Find all matches of the pattern in the line
|
||||
matches: Optional[List[Match[str]]] = re.findall(pattern, line)
|
||||
|
||||
if matches:
|
||||
for match in matches:
|
||||
if match:
|
||||
VAR_NAME = "$" + match[1]
|
||||
VAR_SLICE = match[2][1:-1] if match[2] else ""
|
||||
|
||||
# Within a list refers to a single element
|
||||
if VAR_SLICE.isdigit():
|
||||
# This is an edge case for when the user has a variable such as $DATE = 2022-01-01
|
||||
# We want the user to be able to access it with $DATE or $DATE[0] and the latest
|
||||
# in python will only take the first '2'
|
||||
if VAR_SLICE == "0":
|
||||
if VAR_NAME in ROUTINE_VARS:
|
||||
values = eval(f'ROUTINE_VARS["{VAR_NAME}"]')
|
||||
if isinstance(values, list):
|
||||
templine = templine.replace(
|
||||
match[0],
|
||||
eval(f"values[{VAR_SLICE}]"),
|
||||
)
|
||||
else:
|
||||
templine = templine.replace(match[0], values)
|
||||
else:
|
||||
return (
|
||||
f"[red]Variable {VAR_NAME} not given "
|
||||
"for current routine script.[/red]",
|
||||
"",
|
||||
)
|
||||
|
||||
# Only enters here when any other index from 0 is used
|
||||
else:
|
||||
if VAR_NAME in ROUTINE_VARS:
|
||||
variable = eval(f'ROUTINE_VARS["{VAR_NAME}"]')
|
||||
length_variable = (
|
||||
len(variable)
|
||||
if isinstance(variable, list)
|
||||
else 1
|
||||
)
|
||||
|
||||
# We use <= because we are using 0 index based lists
|
||||
if length_variable <= int(VAR_SLICE):
|
||||
return (
|
||||
f"[red]Variable {VAR_NAME} only has "
|
||||
f"{length_variable} elements and there "
|
||||
f"was an attempt to access it with index {VAR_SLICE}.[/red]",
|
||||
"",
|
||||
)
|
||||
templine = templine.replace(
|
||||
match[0],
|
||||
variable[int(VAR_SLICE)],
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"[red]Variable {VAR_NAME} not given "
|
||||
"for current routine script.[/red]",
|
||||
"",
|
||||
)
|
||||
|
||||
# Involves slicing which is a bit more tricky to use eval on
|
||||
elif (
|
||||
":" in VAR_SLICE
|
||||
and len(VAR_SLICE.split(":")) == 2
|
||||
and (
|
||||
VAR_SLICE.split(":")[0].isdigit()
|
||||
or VAR_SLICE.split(":")[1].isdigit()
|
||||
)
|
||||
):
|
||||
slicing_tuple = "slice("
|
||||
slicing_tuple += (
|
||||
VAR_SLICE.split(":")[0]
|
||||
if VAR_SLICE.split(":")[0].isdigit()
|
||||
else "None"
|
||||
)
|
||||
slicing_tuple += ","
|
||||
slicing_tuple += (
|
||||
VAR_SLICE.split(":")[1]
|
||||
if VAR_SLICE.split(":")[1].isdigit()
|
||||
else "None"
|
||||
)
|
||||
slicing_tuple += ")"
|
||||
|
||||
templine = templine.replace(
|
||||
match[0],
|
||||
",".join(
|
||||
eval(f'ROUTINE_VARS["{VAR_NAME}"][{slicing_tuple}]')
|
||||
),
|
||||
)
|
||||
|
||||
# Just replace value without slicing or list
|
||||
else:
|
||||
if VAR_SLICE:
|
||||
# Check if the string starts with a minus sign
|
||||
if VAR_SLICE.startswith("-"):
|
||||
if not VAR_SLICE[1:].isdigit():
|
||||
return (
|
||||
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
|
||||
"",
|
||||
)
|
||||
if int(VAR_SLICE) < 0:
|
||||
return (
|
||||
f"[red]Negative index on {VAR_NAME} is not allowed[/red]",
|
||||
"",
|
||||
)
|
||||
if not VAR_SLICE.isdigit():
|
||||
return (
|
||||
f"[red]Index '{VAR_SLICE}' is not a value[/red]",
|
||||
"",
|
||||
)
|
||||
|
||||
if VAR_NAME in ROUTINE_VARS:
|
||||
value = eval(f'ROUTINE_VARS["{VAR_NAME}"]')
|
||||
|
||||
# If the value is a list, we want to replace it with the whole list
|
||||
if isinstance(value, list):
|
||||
templine = templine.replace(
|
||||
match[0],
|
||||
",".join(value),
|
||||
)
|
||||
else:
|
||||
templine = templine.replace(match[0], value)
|
||||
|
||||
else:
|
||||
# Check if this is an OpenBB keyword variable like
|
||||
# 1MONTHAGO,LASTFRIDAY,3YEARSFROMNOW,NEXTTUESDAY
|
||||
# and decode it into the right date if it exists
|
||||
potential_date_match = (
|
||||
match_and_return_openbb_keyword_date(VAR_NAME)
|
||||
)
|
||||
if potential_date_match:
|
||||
templine = templine.replace(
|
||||
match[0], potential_date_match
|
||||
)
|
||||
else:
|
||||
return (
|
||||
f"[red]Variable {VAR_NAME} not given for "
|
||||
"current routine script.[/red]",
|
||||
"",
|
||||
)
|
||||
|
||||
lines_with_vars_replaced.append(templine)
|
||||
|
||||
# If this flags ends in True it means that the script routine has a foreach loop that never terminates
|
||||
if foreach_loop_found:
|
||||
return (
|
||||
"[red]The script has a foreach loop that doesn't terminate. "
|
||||
"Add the keyword 'end' to explicitly terminate loop[/red]",
|
||||
"",
|
||||
)
|
||||
|
||||
# Finally the only remaining thing to address are the foreach loops. For that we'll go through
|
||||
# those lines and unroll the arguments that will be iterated by.
|
||||
# Note that the fact that we checked before that the amount of foreach and end matches allow us
|
||||
# to be confident that the script has no clear issues.
|
||||
|
||||
within_foreach = False
|
||||
foreach_lines_loop: List[str] = list()
|
||||
|
||||
parsed_script = ""
|
||||
final_lines = list()
|
||||
varname = "VAR"
|
||||
varused_inside = False
|
||||
for line in lines_with_vars_replaced:
|
||||
# Found 'foreach' header associated with loop
|
||||
match = re.search(
|
||||
r"foreach \$\$([A-Za-z\_]+) in ([A-Za-z0-9,-]+)", line, re.IGNORECASE
|
||||
)
|
||||
if match:
|
||||
varname = match.group(1)
|
||||
foreach_loop = match.group(2).split(",")
|
||||
within_foreach = True
|
||||
|
||||
# We are inside a loop and this is a line that we will want to replicate,
|
||||
# so we need to temporarily store it until we reach the end
|
||||
elif within_foreach:
|
||||
# Found 'end' keyword which means that the foreach loop has reached the end
|
||||
if re.match(r"^\s*end\s*$", line, re.IGNORECASE):
|
||||
# Now we want to process what we were waiting for before
|
||||
|
||||
# Iterate through main foreach header
|
||||
for var in foreach_loop:
|
||||
# Iterate through all lines within foreach and end loop
|
||||
for foreach_line_loop in foreach_lines_loop:
|
||||
if f"$${varname}" in foreach_line_loop:
|
||||
final_lines.append(
|
||||
foreach_line_loop.replace(f"$${varname}", var).strip()
|
||||
)
|
||||
varused_inside = True
|
||||
elif "$$" in foreach_line_loop:
|
||||
return (
|
||||
"[red]The script has a foreach loop that iterates through "
|
||||
f"{','.join(foreach_loop)} with variable $${varname} "
|
||||
"but another var name is being utilized instead[/red]",
|
||||
"",
|
||||
)
|
||||
else:
|
||||
final_lines.append(foreach_line_loop.strip())
|
||||
|
||||
if not varused_inside:
|
||||
console.print(
|
||||
f"The variable {varname} was used in foreach header "
|
||||
"but it wasn't used inside the loop."
|
||||
)
|
||||
varused_inside = False
|
||||
|
||||
# Since this has been processed we reset the foreach loop lines
|
||||
within_foreach = False
|
||||
foreach_lines_loop = list()
|
||||
|
||||
else:
|
||||
foreach_lines_loop.append(line)
|
||||
|
||||
else:
|
||||
final_lines.append(line)
|
||||
|
||||
# If the list is non null, then we want to convert this into a parsed string that is
|
||||
# recognized by the OpenBB Terminal
|
||||
if final_lines:
|
||||
parsed_script = f"{'/'.join([line.rstrip() for line in final_lines])}".replace(
|
||||
"//", "/home/"
|
||||
)
|
||||
if parsed_script[0] == "/":
|
||||
# If the user had added a / at the beginning, then it was converted to //home/
|
||||
# and we need to remove it
|
||||
if parsed_script.startswith("//home"):
|
||||
parsed_script = parsed_script[6:]
|
||||
else:
|
||||
# We want the script to start from the home menu, hence we add it if the user
|
||||
# didn't add it
|
||||
parsed_script = "/" + parsed_script
|
||||
|
||||
# If the script finishes with // it means that we converted it to /home/
|
||||
# This means that we are expecting a command to follow, but since this is
|
||||
# the end of the script, we need to remove the trailing /
|
||||
if parsed_script.endswith("/home/"):
|
||||
parsed_script = parsed_script[:-1]
|
||||
|
||||
return "", parsed_script
|
||||
@@ -51,13 +51,13 @@ from openbb_terminal.menu import is_papermill, session
|
||||
from openbb_terminal.parent_classes import BaseController
|
||||
from openbb_terminal.reports.reports_model import ipykernel_launcher
|
||||
from openbb_terminal.rich_config import MenuText, console
|
||||
from openbb_terminal.routine_functions import is_reset, parse_openbb_script
|
||||
from openbb_terminal.terminal_helper import (
|
||||
bootup,
|
||||
check_for_updates,
|
||||
first_time_user,
|
||||
is_auth_enabled,
|
||||
is_installer,
|
||||
is_reset,
|
||||
print_goodbye,
|
||||
reset,
|
||||
suppress_stdout,
|
||||
@@ -68,7 +68,6 @@ from openbb_terminal.terminal_helper import (
|
||||
# pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args
|
||||
# pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements, inconsistent-return-statements
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
env_file = str(SETTINGS_ENV_FILE)
|
||||
@@ -668,61 +667,36 @@ class TerminalController(BaseController):
|
||||
)
|
||||
else:
|
||||
routine_path = Path(self.ROUTINE_FILES.get(file_path, full_path))
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
with open(routine_path) as fp:
|
||||
raw_lines = [
|
||||
x for x in fp if (not is_reset(x)) and ("#" not in x) and x
|
||||
]
|
||||
raw_lines = [
|
||||
raw_line.strip("\n")
|
||||
for raw_line in raw_lines
|
||||
if raw_line.strip("\n")
|
||||
]
|
||||
raw_lines = list(fp)
|
||||
|
||||
lines = list()
|
||||
for rawline in raw_lines:
|
||||
templine = rawline
|
||||
# Capture ARGV either as list if args separated by commas or as single value
|
||||
if ns_parser.routine_args:
|
||||
script_inputs = (
|
||||
ns_parser.routine_args
|
||||
if "," not in ns_parser.routine_args
|
||||
else ns_parser.routine_args.split(",")
|
||||
)
|
||||
|
||||
# Check if dynamic parameter exists in script
|
||||
if "$ARGV" in rawline:
|
||||
# Check if user has provided inputs through -i or --input
|
||||
if ns_parser.routine_args:
|
||||
for i, arg in enumerate(ns_parser.routine_args):
|
||||
# Check what is the location of the ARGV to be replaced
|
||||
if f"$ARGV[{i}]" in templine:
|
||||
templine = templine.replace(f"$ARGV[{i}]", arg)
|
||||
err, parsed_script = parse_openbb_script(
|
||||
raw_lines=raw_lines,
|
||||
script_inputs=script_inputs if ns_parser.routine_args else None,
|
||||
)
|
||||
|
||||
# Check if all ARGV have been removed, otherwise means that there are less inputs
|
||||
# when running the script than the script expects
|
||||
if "$ARGV" in templine:
|
||||
console.print(
|
||||
"[red]Not enough inputs were provided to fill in dynamic variables. "
|
||||
"E.g. --input VAR1,VAR2,VAR3[/red]\n"
|
||||
)
|
||||
return
|
||||
# If there err output is not an empty string then it means there was an
|
||||
# issue in parsing the routine and therefore we don't want to feed it
|
||||
# to the terminal
|
||||
if err:
|
||||
console.print(err)
|
||||
return
|
||||
|
||||
lines.append(templine)
|
||||
# The script expects a parameter that the user has not provided
|
||||
else:
|
||||
console.print(
|
||||
"[red]The script expects parameters, "
|
||||
"run the script again with --input defined.[/red]\n"
|
||||
)
|
||||
return
|
||||
else:
|
||||
lines.append(templine)
|
||||
|
||||
simulate_argv = f"/{'/'.join([line.rstrip() for line in lines])}"
|
||||
file_cmds = simulate_argv.replace("//", "/home/").split()
|
||||
file_cmds = insert_start_slash(file_cmds) if file_cmds else file_cmds
|
||||
cmds_with_params = " ".join(file_cmds)
|
||||
self.queue = [
|
||||
val
|
||||
for val in parse_and_split_input(
|
||||
an_input=cmds_with_params, custom_filters=[]
|
||||
an_input=parsed_script, custom_filters=[]
|
||||
)
|
||||
if val
|
||||
]
|
||||
@@ -964,6 +938,7 @@ def run_scripts(
|
||||
if not test_mode:
|
||||
terminal()
|
||||
|
||||
# THIS NEEDS TO BE REFACTORED!!! - ITS USED FOR TESTING
|
||||
with path.open() as fp:
|
||||
raw_lines = [x for x in fp if (not is_reset(x)) and ("#" not in x) and x]
|
||||
raw_lines = [
|
||||
@@ -994,6 +969,7 @@ def run_scripts(
|
||||
if test_mode and "exit" not in lines[-1]:
|
||||
lines.append("exit")
|
||||
|
||||
# Deals with the export with a path with "/" in it
|
||||
export_folder = ""
|
||||
if "export" in lines[0]:
|
||||
export_folder = lines[0].split("export ")[1].rstrip()
|
||||
|
||||
@@ -422,28 +422,6 @@ def suppress_stdout():
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
def is_reset(command: str) -> bool:
|
||||
"""Test whether a command is a reset command
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : str
|
||||
The command to test
|
||||
|
||||
Returns
|
||||
-------
|
||||
answer : bool
|
||||
Whether the command is a reset command
|
||||
"""
|
||||
if "reset" in command:
|
||||
return True
|
||||
if command == "r":
|
||||
return True
|
||||
if command == "r\n":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def first_time_user() -> bool:
|
||||
"""Whether a user is a first time user. A first time user is someone with an empty .env file.
|
||||
If this is true, it also adds an env variable to make sure this does not run again.
|
||||
|
||||
17
poetry.lock
generated
vendored
17
poetry.lock
generated
vendored
@@ -2295,6 +2295,21 @@ files = [
|
||||
[package.dependencies]
|
||||
pandas = "*"
|
||||
|
||||
[[package]]
|
||||
name = "freezegun"
|
||||
version = "1.2.2"
|
||||
description = "Let your Python tests travel through time"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"},
|
||||
{file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "frozendict"
|
||||
version = "2.3.5"
|
||||
@@ -9689,4 +9704,4 @@ optimization = ["Riskfolio-Lib"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8,<3.11, !=3.9.7"
|
||||
content-hash = "935a7f7394d9a634c2aa1a23e0951a5351cd6e883b75eca95033d3bcd481e0c0"
|
||||
content-hash = "d234e91fbc23688ef420b144ceb703500b09bcce16710ce9ae5c8037163d6843"
|
||||
|
||||
1
pyproject.toml
vendored
1
pyproject.toml
vendored
@@ -155,6 +155,7 @@ nbmake = ">0.5.0"
|
||||
ruff = "^0.0.256"
|
||||
pytest-timeout = "^2.1.0"
|
||||
pytest-recorder = "^0.2.1"
|
||||
freezegun = "^1.2.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools<65.5.0", "poetry-core>=1.0.0"]
|
||||
|
||||
1
requirements-full.txt
vendored
1
requirements-full.txt
vendored
@@ -91,6 +91,7 @@ fonttools==4.39.2 ; python_version >= "3.8" and python_full_version != "3.9.7" a
|
||||
formulaic==0.3.4 ; python_version >= "3.8" and python_version < "3.11" and python_full_version != "3.9.7"
|
||||
fred==3.1 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
fredapi==0.4.3 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
freezegun==1.2.2 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
frozendict==2.3.5 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
frozenlist==1.3.3 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
fsspec[http]==2023.3.0 ; python_version >= "3.8" and python_full_version != "3.9.7" and python_version < "3.11"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# IMPORTATION THIRDPARTY
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
# IMPORTATION INTERNAL
|
||||
from openbb_terminal.common.ultima_newsmonitor_view import display_news
|
||||
@@ -10,6 +11,7 @@ from openbb_terminal.common.ultima_newsmonitor_view import display_news
|
||||
@pytest.mark.vcr
|
||||
@pytest.mark.record_stdout
|
||||
@pytest.mark.parametrize("term", ["", "AAPL", "TSLA", "FCX", "asdf&%$"])
|
||||
@freeze_time("2023-05-30")
|
||||
def test_display_news(term, mocker):
|
||||
# MOCK EXPORT_DATA
|
||||
mocker.patch(target="openbb_terminal.common.ultima_newsmonitor_view.export_data")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
[purple][bold]Most Relevant Articles for AAPL - 2023-05-29[/bold][/purple]
|
||||
[purple][bold]Most Relevant Articles for AAPL - 2023-05-30[/bold][/purple]
|
||||
|
||||
[green]Regulatory Risk[/green] ([3mThe U.S. solicitor general has urged the Supreme Court to reject an appeal by Apple and Broadcom in their $1.1 billion trial loss to the California Institute of Technology (Caltech) over a patent infringement case. The solicitor general argued that the U.S. Court of Appeals for the Federal Circuit was correct in ruling that the companies could not seek to invalidate Caltech's patents in court after Apple failed to raise its invalidity arguments at the U.S. Patent Office. Caltech has also sued Microsoft, Samsung, Dell, and HP for infringing the same patents in separate cases that are still pending - Reuters (https://www.reuters.com/legal/biden-administration-urges-supreme-court-not-hear-apple-caltech-patent-case-2023-05-24/)[0m)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
[purple][bold]Most Relevant Articles for FCX - 2023-05-29[/bold][/purple]
|
||||
[purple][bold]Most Relevant Articles for FCX - 2023-05-30[/bold][/purple]
|
||||
|
||||
[green]Regulatory Risk[/green] ([3mIndonesia will allow exports of copper, iron ore, lead, zinc, and anode mud from copper concentrates to continue despite a planned export ban in June, as many smelters were delayed by the pandemic. The country's Energy and Mineral Resources Minister, Arifin Tasrif, stated that banning these materials prematurely would cost the country revenue and jobs. Copper miners Freeport Indonesia and Amman Mineral Nusa Tenggara will be exempt from the ban due to smelter development delays caused by the pandemic. This decision may impact FCX's operations in Indonesia and its supply chain. - Zawya (https://www.zawya.com/en/world/china-and-asia-pacific/indonesia-to-allow-exports-of-five-raw-minerals-despite-june-ban-w16a5k93)[0m)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
[purple][bold]Most Relevant Articles for TSLA - 2023-05-29[/bold][/purple]
|
||||
[purple][bold]Most Relevant Articles for TSLA - 2023-05-30[/bold][/purple]
|
||||
|
||||
[green]Competition Risk[/green] ([3mUber announced plans to launch a fleet of 25,000 electric vehicles in India, a country where Tesla has been speculated to build a new production facility. Uber's program, called Uber Green, aims to boost financing options and partner with local fleet operators to expand the presence of electric powertrains in India. Tesla has been in talks with the Indian government for years, but disagreements over import duties and local manufacturing have stalled progress. However, Elon Musk hinted that Tesla could reveal the location of its next production plant by the end of the year, with India being a favorable location. - Teslarati (https://www.teslarati.com/uber-launch-evs-india-tesla-next-factory/)[0m)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
stats collection stats [OpenSea]
|
||||
fp collection sales/floor price [NFTPriceFloor]
|
||||
fp collection floor price [NFTPriceFloor]
|
||||
collections top ethereum collections [NFTPriceFloor]
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ def test_display_load(term, country):
|
||||
assert isinstance(funds, EXAMPLE_FUND.__class__)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.record_verify_screen
|
||||
@pytest.mark.parametrize(
|
||||
"term, country, limit",
|
||||
|
||||
546
tests/openbb_terminal/routines/test_routines.py
Normal file
546
tests/openbb_terminal/routines/test_routines.py
Normal file
@@ -0,0 +1,546 @@
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
from openbb_terminal.routine_functions import (
|
||||
match_and_return_openbb_keyword_date,
|
||||
parse_openbb_script,
|
||||
)
|
||||
|
||||
|
||||
def cleanup_script(script: str) -> List[str]:
|
||||
raw_lines = [x for x in script.split("\n") if ("#" not in x) and x]
|
||||
raw_lines = [raw_line.strip("\n") for raw_line in raw_lines if raw_line.strip("\n")]
|
||||
return raw_lines
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_days_ago():
|
||||
assert match_and_return_openbb_keyword_date("$1DAYSAGO") == "2023-05-16"
|
||||
assert match_and_return_openbb_keyword_date("$10DAYSAGO") == "2023-05-07"
|
||||
assert match_and_return_openbb_keyword_date("$-5DAYSAGO") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_months_ago():
|
||||
assert match_and_return_openbb_keyword_date("$1MONTHSAGO") == "2023-04-17"
|
||||
assert match_and_return_openbb_keyword_date("$10MONTHSAGO") == "2022-07-17"
|
||||
assert match_and_return_openbb_keyword_date("$-5MONTHSAGO") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_years_ago():
|
||||
assert match_and_return_openbb_keyword_date("$1YEARSAGO") == "2022-05-17"
|
||||
assert match_and_return_openbb_keyword_date("$10YEARSAGO") == "2013-05-17"
|
||||
assert match_and_return_openbb_keyword_date("$-5YEARSAGO") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_days_from_now():
|
||||
assert match_and_return_openbb_keyword_date("$1DAYSFROMNOW") == "2023-05-18"
|
||||
assert match_and_return_openbb_keyword_date("$10DAYSFROMNOW") == "2023-05-27"
|
||||
assert match_and_return_openbb_keyword_date("$-5DAYSFROMNOW") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_months_from_now():
|
||||
assert match_and_return_openbb_keyword_date("$1MONTHSFROMNOW") == "2023-06-17"
|
||||
assert match_and_return_openbb_keyword_date("$10MONTHSFROMNOW") == "2024-03-17"
|
||||
assert match_and_return_openbb_keyword_date("$-5MONTHSFROMNOW") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_years_from_now():
|
||||
assert match_and_return_openbb_keyword_date("$1YEARSFROMNOW") == "2024-05-17"
|
||||
assert match_and_return_openbb_keyword_date("$10YEARSFROMNOW") == "2033-05-17"
|
||||
assert match_and_return_openbb_keyword_date("$-5YEARSFROMNOW") == ""
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_others():
|
||||
assert match_and_return_openbb_keyword_date("$LASTMONDAY") == "2023-05-15"
|
||||
assert match_and_return_openbb_keyword_date("$LASTFRIDAY") == "2023-05-12"
|
||||
assert match_and_return_openbb_keyword_date("$NEXTFRIDAY") == "2023-05-19"
|
||||
assert match_and_return_openbb_keyword_date("$LASTJANUARY") == "2023-01-01"
|
||||
assert match_and_return_openbb_keyword_date("$NEXTMARCH") == "2024-03-01"
|
||||
assert match_and_return_openbb_keyword_date("$LASTDECEMBER") == "2022-12-01"
|
||||
assert match_and_return_openbb_keyword_date("$NEXTTUESDAY") == "2023-05-23"
|
||||
assert match_and_return_openbb_keyword_date("$NEXTSEPTEMBER") == "2023-09-01"
|
||||
|
||||
|
||||
@freeze_time("2023-05-17")
|
||||
def test_parse_openbb_script():
|
||||
raw_lines = [
|
||||
"stocks",
|
||||
"foreach $$VAR in PLTR,BB",
|
||||
" load $$VAR --start $LASTJANUARY",
|
||||
" candle",
|
||||
"END",
|
||||
]
|
||||
test_out = parse_openbb_script(raw_lines)
|
||||
assert test_out[1].split("/")[1:] == [
|
||||
"stocks",
|
||||
"load PLTR --start 2023-01-01",
|
||||
"candle",
|
||||
"load BB --start 2023-01-01",
|
||||
"candle",
|
||||
]
|
||||
|
||||
raw_lines2 = [
|
||||
"$DATE = 2022-01-01",
|
||||
"$DATES = 2022-01-01,2023-01-01",
|
||||
"stocks",
|
||||
"load $ARGV[0]",
|
||||
"ta/ema/..",
|
||||
"foreach $$VAR in $ARGV[1:]",
|
||||
" load $$VAR --start $DATE[0]",
|
||||
" candle",
|
||||
"end",
|
||||
"ca/set $ARGV[2:]/historical/..",
|
||||
"FOREACH $$VAR in $ARGV",
|
||||
"load $$VAR --start $DATES[0] --end $DATES[1]",
|
||||
"candle",
|
||||
" end",
|
||||
"foreach $$VAR in $ARGV[1:3]",
|
||||
" load $$VAR --start 2022-01-01",
|
||||
" candle",
|
||||
"END",
|
||||
"foreach $$VAR in PLTR,BB",
|
||||
" load $$VAR --start 2022-01-01",
|
||||
" candle",
|
||||
"END",
|
||||
]
|
||||
test_out2 = parse_openbb_script(raw_lines2, None)
|
||||
assert (
|
||||
test_out2[0]
|
||||
== "[red]Variable $ARGV not given for current routine script.[/red]"
|
||||
)
|
||||
|
||||
|
||||
@freeze_time("2023-06-04")
|
||||
@pytest.mark.parametrize(
|
||||
"routine, input_args, expected_error, expected_queue",
|
||||
[
|
||||
(
|
||||
"""
|
||||
# This is a comment that will be ignored
|
||||
stocks/load aapl/candle
|
||||
/
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load aapl/candle/home",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load AAPL --start 2022-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[0]
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load AAPL --start 2022-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2024-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[0]
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load AAPL --start 2022-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2024-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[0] --end $DATE[1]
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load AAPL --start 2022-01-01 --end 2024-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[1]
|
||||
""",
|
||||
None,
|
||||
"[red]Variable $DATE only has 1 elements and there was an attempt to access it with index 1.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2023-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[2]
|
||||
""",
|
||||
None,
|
||||
"[red]Variable $DATE only has 2 elements and there was an attempt to access it with index 2.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks/load AAPL --start $DATE[1]
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2023-01-01
|
||||
""",
|
||||
None,
|
||||
"",
|
||||
"/stocks/load AAPL --start 2023-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2023-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[5]
|
||||
""",
|
||||
None,
|
||||
"[red]Variable $DATE only has 2 elements and there was an attempt to access it with index 5.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[5]
|
||||
""",
|
||||
None,
|
||||
"[red]Variable $DATE only has 1 elements and there was an attempt to access it with index 5.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[-2]
|
||||
""",
|
||||
None,
|
||||
"[red]Negative index on $DATE is not allowed[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
$DATE = 2022-01-01,2024-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[-1]
|
||||
""",
|
||||
None,
|
||||
"[red]Negative index on $DATE is not allowed[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set variable
|
||||
DATE = 2022-01-01,2024-01-01
|
||||
|
||||
stocks/load AAPL --start $DATE[0]
|
||||
""",
|
||||
None,
|
||||
"[red]Variable $DATE not given for current routine script.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks/load $ARGV[3]
|
||||
""",
|
||||
"TSLA,MSFT".split(","),
|
||||
"[red]Variable $ARGV only has 2 elements and there was an attempt to access it with index 3.[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks/load $ARGV[-1]
|
||||
""",
|
||||
"TSLA,MSFT".split(","),
|
||||
"[red]Negative index on $ARGV is not allowed[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks/load $ARGV[a]
|
||||
""",
|
||||
"TSLA,MSFT".split(","),
|
||||
"[red]Index 'a' is not a value[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks/load $ARGV[0]/candle --start $1MONTHSAGO
|
||||
""",
|
||||
"TSLA,MSFT".split(","),
|
||||
"",
|
||||
"/stocks/load TSLA/candle --start 2023-05-04",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# cool beans this is a comment that should be ignored
|
||||
|
||||
stocks/ca/set $ARGV[1:]/historical
|
||||
..
|
||||
""",
|
||||
"TSLA,MSFT,AAPL,NVDA".split(","),
|
||||
"",
|
||||
"/stocks/ca/set MSFT,AAPL,NVDA/historical/..",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# cool beans this is a comment that should be ignored
|
||||
|
||||
stocks/ca/set $ARGV/historical
|
||||
/
|
||||
""",
|
||||
"TSLA,MSFT,AAPL,NVDA".split(","),
|
||||
"",
|
||||
"/stocks/ca/set TSLA,MSFT,AAPL,NVDA/historical/home",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
/stocks/ca/set $ARGV[1:3]/historical
|
||||
""",
|
||||
"TSLA,MSFT,AAPL,NVDA".split(","),
|
||||
"",
|
||||
"/stocks/ca/set MSFT,AAPL/historical",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
/
|
||||
stocks/ca/set $ARGV[1:3]/historical
|
||||
""",
|
||||
"TSLA,MSFT,AAPL,NVDA".split(","),
|
||||
"",
|
||||
"/home/stocks/ca/set MSFT,AAPL/historical",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
|
||||
# Set list of fixed variables
|
||||
$DATES = 2022-01-01,2023-01-01
|
||||
|
||||
# Charts from all ARGV elements from position 1 onwards
|
||||
stocks
|
||||
foreach $$VAR in $ARGV[1:]
|
||||
load $$VAR --start $DATES[0] --end $DATES[1]/dps/psi/..
|
||||
end
|
||||
""",
|
||||
"TSLA,MSFT,NVDA".split(","),
|
||||
"",
|
||||
"/stocks/load MSFT --start 2022-01-01 --end 2023-01-01/dps/psi/../"
|
||||
"load NVDA --start 2022-01-01 --end 2023-01-01/dps/psi/..",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set list of fixed variables
|
||||
$DATE = 2022-01-01
|
||||
|
||||
# Charts from all ARGV elements from position 1 onwards
|
||||
stocks
|
||||
FOREACH $$VAR in $ARGV
|
||||
load $$VAR --start $DATE[0]/ins/stats/..
|
||||
end
|
||||
""",
|
||||
"TSLA,MSFT".split(","),
|
||||
"",
|
||||
"/stocks/load TSLA --start 2022-01-01/ins/stats/../load MSFT --start 2022-01-01/ins/stats/..",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Set list of fixed variables
|
||||
$DATE = 2022-01-01
|
||||
|
||||
# Charts from all ARGV elements from position 1 onwards
|
||||
stocks
|
||||
# Charts elements in positions 1 and 3 of ARGV
|
||||
foreach $$VAR in $ARGV[1:3]
|
||||
load $$VAR --start $DATE
|
||||
ba
|
||||
regions
|
||||
..
|
||||
END
|
||||
""",
|
||||
"TSLA,MSFT,AAPL,PLTR".split(","),
|
||||
"",
|
||||
"/stocks/load MSFT --start 2022-01-01/ba/regions/../load AAPL --start 2022-01-01/ba/regions/..",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
|
||||
# Charts for PLTR and BB
|
||||
foreach $$VAR in PLTR,BB
|
||||
load $$VAR --start $LASTJANUARY
|
||||
candle
|
||||
END
|
||||
|
||||
home
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"/stocks/load PLTR --start 2023-01-01/candle/load BB --start 2023-01-01/candle/home",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
# Charts for PLTR and BB
|
||||
foreach $$SOMETHING in PLTR,BB
|
||||
load $$SOMETHING --start $LASTJANUARY
|
||||
candle
|
||||
END
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"/stocks/load PLTR --start 2023-01-01/candle/load BB --start 2023-01-01/candle",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
# forgetting the end on purpose
|
||||
foreach $$X in PLTR,BB
|
||||
load $$X --start $LASTJANUARY
|
||||
""",
|
||||
"",
|
||||
"[red]The script has a foreach loop that doesn't terminate. "
|
||||
"Add the keyword 'end' to explicitly terminate loop[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
# pretending I forgot the header
|
||||
load $$X --start $LASTJANUARY
|
||||
end
|
||||
""",
|
||||
"",
|
||||
"[red]The script has a foreach loop that terminates before it gets started. "
|
||||
"Add the keyword 'foreach' to explicitly start loop[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
foreach $$X in PLTR,BB
|
||||
end
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"/stocks",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
foreach $$D in 2022-01-01,2023-01-01
|
||||
load AAPL --start $$D
|
||||
end
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"/stocks/load AAPL --start 2022-01-01/load AAPL --start 2023-01-01",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
foreach $$UNUSED in 1,2
|
||||
load AAPL
|
||||
end
|
||||
""",
|
||||
"",
|
||||
"",
|
||||
"/stocks/load AAPL/load AAPL",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
stocks
|
||||
foreach $$UNUSED in 1,2
|
||||
load $$TYPO
|
||||
end
|
||||
""",
|
||||
"",
|
||||
"[red]The script has a foreach loop that iterates through "
|
||||
"1,2 with variable $$UNUSED but another var name is being "
|
||||
"utilized instead[/red]",
|
||||
"",
|
||||
),
|
||||
#############################
|
||||
(
|
||||
"""
|
||||
# Defining variables
|
||||
$TICKERS = AAPL,TSLA,MSFT
|
||||
$DATES = 2020-01-01,2021-01-01
|
||||
|
||||
stocks/load $TICKERS[0] --start $DATES[1]/candle
|
||||
|
||||
foreach $$VAR in $TICKERS
|
||||
load $$VAR/fa/income
|
||||
..
|
||||
dps/psi/..
|
||||
end
|
||||
|
||||
load $ARGV[0]/ta/ema -l 50,100,200
|
||||
""",
|
||||
"NVDA",
|
||||
"",
|
||||
"/stocks/load AAPL --start 2021-01-01/candle/load AAPL/fa/income/../dps/psi/"
|
||||
"../load TSLA/fa/income/../dps/psi/../load MSFT/fa/income/../dps/psi/../"
|
||||
"load NVDA/ta/ema -l 50,100,200",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_openbb_routines(routine, input_args, expected_error, expected_queue):
|
||||
err, queue = parse_openbb_script(routine.split("\n"), input_args)
|
||||
|
||||
assert err == expected_error
|
||||
assert queue == expected_queue
|
||||
@@ -9,12 +9,13 @@ import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
|
||||
|
||||
<HeadTitle title="Scripts & Routines - Terminal | OpenBB Docs" />
|
||||
|
||||
OpenBB Routine Scripts, `.openbb`, are simple scripts for automating processes and repetitive tasks. They represent a 1:1 relationship with any command in the OpenBB Terminal; one line represents one function. Other software like STATA, SPSS and R-Studio share similar functionality in the area of Econometrics and the OpenBB routine scripts venture into the area of financial analysis and data collection to speed up the process. For example, not only is it possible to automate a set of functionality, it is also possible to export a large amount of data to Excel through the usage of `--export` and `--sheet-name` making data collection efficient, reproducible and customizable.
|
||||
OpenBB Routine Scripts allow users to write simple scripts for automating processes and repetitive tasks. In essence these are text plain-text files that can be created or modified in any basic text editor with the only difference of having an `.openbb` extension.
|
||||
|
||||
Other software like STATA, SPSS and R-Studio share similar functionality in the area of Econometrics and the OpenBB routine scripts venture into the area of financial analysis and data collection to speed up the process.
|
||||
|
||||
For example, not only is it possible to automate a set of functionality, it is also possible to export a large amount of data to Excel through the usage of `--export` and `--sheet-name` making data collection efficient, reproducible and customizable.
|
||||
|
||||
## Introduction
|
||||
A routine file can be created or modified in any basic text editor. The only difference between a plain-text file and an OpenBB Routine file is the `.openbb` file extension. Additionally, routine files can be captured with the macro recorder, controlled with global commands, `record` & `stop`. Upon `stop`. The script is automatically saved to the, `~/OpenBBUserData/routines/`, folder.
|
||||
|
||||
Routine files stored there are callable from the main menu, using the `exe` function. By deploying variable arguments - `$ARGV[0]` - to any line in the script, dates, symbols, exported filenames, or any Terminal function argument, scripts become dynamic.
|
||||
|
||||
Run a routine file from the main menu, with the `exe` command. A great start would be to use `exe --example` to get a sense of what this functionality does. Below, the `--help` dialogue is displayed.
|
||||
|
||||
@@ -33,44 +34,32 @@ options:
|
||||
|
||||
## The Macro Recorder
|
||||
|
||||
As a starting point, you can use the `record` and `stop` functionalitsy. This shares similarities with that of Excel's VBA methods. This means that any command you run will be automatically recorded for the routine script and once you type `stop` it automatically saves the file. For example, if you cope the following code in the OpenBB Terminal and run it, you will see an example.
|
||||
OpenBB script routines can be captured with the macro recorder, controlled with global commands, `record` to start saving commands and `stop` to terminate the recording. This shares similarities with that of Excel's VBA methods. This means that any command you run will be automatically recorded for the routine script and once you type `stop` it automatically saves the file to the `~/OpenBBUserData/routines/` folder.
|
||||
|
||||
For example, if you copy and paste the following prompt in the OpenBB Terminal and press enter, you will see an example.
|
||||
|
||||
```console
|
||||
record/economy/cpi/treasury/index sp500/stop
|
||||
$ /record/economy/cpi/treasury/index sp500/stop
|
||||
```
|
||||
|
||||
The following shows the script being ran and the charts that are created because of it.
|
||||
The following shows the output from this pipeline of commands.
|
||||
|
||||

|
||||
|
||||
After the script is finished, you are able to access the routine file when using `exe --file`. The naming of the file can differ for you based on the time you are executing the script. You can find this script inside the `routines` folder within the `OpenBBUserData` folder (as found [here](https://docs.openbb.co/terminal/usage/guides/data))
|
||||
Because there was a `record` and `stop` at the `start` and `end` respectively, a routine script was created. This file cane be found inside the `routines` folder within the `OpenBBUserData` folder (more on exporting and import data [here](https://docs.openbb.co/terminal/usage/guides/data)).
|
||||
|
||||
Now, you should be able to access the routine file from the terminal main menu by doing `/exe --file` and using the auto-completer. Note that the naming of the file will differ for you based on the time you are executing the script.
|
||||
|
||||

|
||||
|
||||
## Create your own Script
|
||||
## Basic Script
|
||||
|
||||
As mentioned earlier, the scripts and routines reside in the `routines` folder within the `OpenBBUserData` folder (as found [here](https://docs.openbb.co/terminal/usage/guides/data)) and are automatically shown when you type `exe` from the home screen (`home`).
|
||||
The most basic script style contains 2 main elements:
|
||||
|
||||
:::note To manually create your own .openbb script use the following:
|
||||
1. Download the file that can be used as a template **[here](https://www.dropbox.com/s/73g9qx9xgtbb2ec/routines_template.openbb?dl=1)**.
|
||||
2. Move the file inside the `routines` folder within the [OpenBBUserData](https://docs.openbb.co/terminal/usage/guides/data) folder and, optionally, adjust the name to your liking.
|
||||
3. Open the file with a Text Editor (e.g. Notepad or TextEdit) and adjust the file accordingly.
|
||||
4. Open up the OpenBB Terminal, and type `exe --file`. The file should then be one of the options.
|
||||
5. Select the file to run the routine script.
|
||||
:::
|
||||
- **Comments**: any text after a hashtag (`#`) is referred to as a comment. This is used to explain what is happening within the line below and is ignored when the file is executed.
|
||||
- **Commands**: any text *without* a hashtag is being ran inside the OpenBB Terminal as if the user had prompted that line in the terminal. Note that this means that you are able to create a pipeline of commands in a single line, i.e. `stocks/load AAPL/candle --ma 20` is a valid line for the script.
|
||||
|
||||
As long as the file remains in the `routines` folder, you will be able to find your file automatically as shown below. Note that this also shows the script that was created within [The Macro Recorder](#the-macro-recorder) section.
|
||||
|
||||

|
||||
|
||||
## Explanation of Scripts
|
||||
|
||||
The script file follows the following logic:
|
||||
|
||||
- **Comments**: any text after a hashtag (`#`) is referred to as a comment. This is used to explain what is happening within the script and is not taking into account when running terminal commands.
|
||||
- **Commands**: any text *without* a hashtag is being ran inside the OpenBB Terminal. E.g. on the second line it says `stocks` thus within the OpenBB Terminal the script will enter `stocks` and run this for you.
|
||||
|
||||
These scripts have a 1-to-1 relationship with how you would normally use the terminal. To get a better understanding of how the terminal is used, please see <a href="https://docs.openbb.co/terminal/guides/basics" target="_blank" rel="noreferrer noopener">Structure of the OpenBB Terminal</a>. The example below can be executed by running `exe --example`.
|
||||
For instance, the text below corresponds to the example file that OpenBB provides.
|
||||
|
||||
```
|
||||
# Go into the stocks context
|
||||
@@ -98,17 +87,23 @@ est
|
||||
home
|
||||
```
|
||||
|
||||
## Executing a Script
|
||||
## Creating and running a routine script
|
||||
|
||||
By going to the main menu as depicted below (accessible with `home`), the `exe` command can be used. With this command you can run any `.openbb` script. These scripts are located where the application is located inside the `routines` folder as found in the `OpenBBUserData` folder.
|
||||
As a starting point, let's use the example above.
|
||||
|
||||
Thus, using the earlier mentioned script, we can enter `exe --file routines_template.openbb` which automatically runs all commands within the script file. Thus, it will return a candle chart with a moving average of 20 days, expectations and price targets from analysts and estimated future performance before returning to the home window.
|
||||
1. Create a new text file with name `routines_template.openbb` and copy paste the routine above. For simplicity you can also download the template file **[here](https://www.dropbox.com/s/73g9qx9xgtbb2ec/routines_template.openbb?dl=1)**.
|
||||
2. Move the file inside the `routines` folder within the [OpenBBUserData](https://docs.openbb.co/terminal/usage/guides/data) folder and, optionally, adjust the name to your liking.
|
||||
3. Open up the OpenBB Terminal, and type `exe --file routines_template`. If you changed the name of the file, then replace `routines_template` by such. As long as the file remains in the `routines` folder, you will be able to find your file through OpenBB Terminal's auto-completer capability.
|
||||
|
||||
Now you should expect the contents of the example above to be run. This means that a candle chart with a moving average of 20 days, expectations and price targets from analysts and estimated future performance should pop up before returning to the home window.
|
||||
|
||||

|
||||
|
||||
## Custom Arguments
|
||||
## Input variables
|
||||
|
||||
Next to that, it is also possible to add in custom arguments to your script making the script more interactive and allow you to do the same analysis for multiple companies. This is done in the following script (and can be downloaded [here](https://www.dropbox.com/s/usooz6y29r1xldb/routines_template_with_inputs.openbb?dl=1)):
|
||||
The previous example shows the promise of this language. However, it would be exhausting to have to create one file per ticker of interest. And this is why we allow the user to create arguments within a script that will be populated at the time of execution, which provides a more efficient experience.
|
||||
|
||||
As an example, let's look into the following routine (the file can be downloaded [here](https://www.dropbox.com/s/usooz6y29r1xldb/routines_template_with_inputs.openbb?dl=1)):
|
||||
|
||||
```
|
||||
# This script requires you to use arguments. This can be done with the following:
|
||||
@@ -139,8 +134,99 @@ historical
|
||||
home
|
||||
```
|
||||
|
||||
This script includes `$ARGV[0]`, `$ARGV[1]` and `$ARGV[2]`. This means that the script requires you to submit three arguments. In this case, they refer to <a href="https://www.investopedia.com/ask/answers/12/what-is-a-stock-ticker.asp" target="_blank" rel="noreferrer noopener">stock tickers</a>. Therefore, like the script also says, you can include these arguments with `-i` followed by three tickers (e.g. `routines_template_with_inputs.openbb`). This results in the following:
|
||||
This script includes `$ARGV[0]`, `$ARGV[1]` and `$ARGV[2]`. This means that the script requires you to submit three arguments. In this case, they refer to <a href="https://www.investopedia.com/ask/answers/12/what-is-a-stock-ticker.asp" target="_blank" rel="noreferrer noopener">stock tickers</a>. Therefore, like the script also says, you can include these arguments with `-i` or `--input` followed by three tickers (e.g. `/exe routines_template_with_inputs.openbb -i TSLA,AAPL,MSFT`). Resulting in the following,
|
||||
|
||||

|
||||
|
||||
It is a simple script but it gives an understanding what the possibilities are. Do make sure you saved this script in the `routines` folder else you are not able to execute it.
|
||||
Note: Make sure you saved this script in the `~/OpenBBUserData/routines/` folder else you are not able to execute it.
|
||||
|
||||
## Set variables
|
||||
|
||||
In addition, OpenBB accepts the creation of variables inside the script. This can be useful when the user utilizes a ticker as a benchmark/reference, and therefore instead of having to change the ticker of interest in multiple places they can just change the variable. Example of script below:
|
||||
|
||||
```
|
||||
# Set date variable
|
||||
$DATE = 2022-01-01
|
||||
|
||||
# Set list of tickers to iterate
|
||||
$TICKERS = AAPL,MSFT
|
||||
|
||||
# dive into stocks
|
||||
stocks
|
||||
|
||||
# candle chart for first ticker
|
||||
load $TICKERS[0] --start $DATE/candle
|
||||
|
||||
# candle chart for second ticker
|
||||
load $TICKERS[1] --start $DATE[0]/candle
|
||||
```
|
||||
|
||||
Note that a variable can be declared as a single argument `$DATE = 2022-01-01` but it can also be declared as a list `$TICKERS = AAPL,MSFT`.
|
||||
|
||||
When declared as a list, the user needs to use the indexing to access the element of interest, i.e. if interested in `MSFT` then `$TICKERS[1]` should be used.
|
||||
|
||||
When a single element is defined, then the user can access it through the variable name or indexing the first position equally, i.e. `$DATE` = `$DATE[0]`.
|
||||
|
||||
Note that slicing is also possible, and the same convention as python is utilized. If the user has defined inputs `AAPL,MSFT,TSLA,NVDA,GOOG` then by selecting `$ARGV[1:3]` the tickers `MSFT,TSLA` are selected.
|
||||
|
||||
## Relative time kewyword variables
|
||||
|
||||
The previous input variables are very powerful and can be used for a wide range of functionalities - one of them being dates. E.g. a typical example could be running a script routine with `exe myscript -i TSLA,2010-01-01` where `$ARGV[1]=2010-01-01` and thus it denotes an instant of time.
|
||||
|
||||
However, what if instead of wanting an absolute date we want a relative date? The OpenBB scripts now have the capability to recognize these. There are 4 main types of keywords:
|
||||
|
||||
* **AGO** - this corresponds to a time in the past relative to today. Valid examples are: `$365DAYSAGO`, `$12MONTHSAGO`, `$1YEARSAGO`.
|
||||
|
||||
* **FROMNOW** - this corresponds to a time in the future relative to today. Valid examples are: `$365DAYSFROMNOW`, `$12MONTHSFROMNOW`, `$1YEARSFROMNOW`.
|
||||
|
||||
* **LAST** - this refers to the last specific day of the week or month that has occurred. Valid examples are: `$LASTMONDAY`, `$LASTJUNE`.
|
||||
|
||||
* **NEXT** - this refers to the next specific day of the week or month that will occur. Valid examples are: `$NEXTFRIDAY`, `$NEXTNOVEMBER`.
|
||||
|
||||
The result will be a date with the conventional date associated with OpenBB, i.e. `YYYY-MM-dd`.
|
||||
|
||||
## Foreach loop
|
||||
|
||||
To be a powerful scripting language, we needed to introduce the concept of foreach loops. These allows to iterate through a list of variables / arguments to execute a sequence commands.
|
||||
|
||||
In order to create a foreach loop all you need to do is:
|
||||
|
||||
1. Create the header `foreach $$VAR in X` where `X` can be an argument or a list of variables. Note that the `$$VAR` can take other naming as long as the `$$` convention is kept.
|
||||
|
||||
2. Insert commands you are interested in repeating in the following lines
|
||||
|
||||
3. Finish with a `end`.
|
||||
|
||||
Some valid examples are shown below as reference
|
||||
|
||||
```
|
||||
# Iterates through ARGV elements from position 1 onwards
|
||||
foreach $$VAR in $ARGV[1:]
|
||||
load $$VAR --start $DATES[0] --end $DATES[1]/dps/psi/..
|
||||
end
|
||||
```
|
||||
|
||||
```
|
||||
# Loops through all $ARGV variables
|
||||
FOREACH $$SOMETHING in $ARGV
|
||||
load $$SOMETHING --start $DATE[0]/ins/stats/..
|
||||
end
|
||||
```
|
||||
|
||||
```
|
||||
# Iterates through ARGV elements in position 1,2
|
||||
foreach $$VAR in $ARGV[1:3]
|
||||
load $$VAR --start 2022-01-01
|
||||
ba
|
||||
regions
|
||||
..
|
||||
END
|
||||
```
|
||||
|
||||
```
|
||||
# Loops through PLTR and BB
|
||||
foreach $$X in PLTR,BB
|
||||
load $$X --start $LASTJANUARY
|
||||
candle
|
||||
END
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user