diff --git a/README.md b/README.md index 8cf58fadab..1f0bb6eaa2 100755 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Key contributors include: - [**Hongyang (Bruce) Yang**](https://www.linkedin.com/in/brucehy/) – research and development on financial reinforcement learning frameworks, market environments, and quantitative trading applications - [other contributors…] - + ## Overview FinRL has three layers: market environments, agents, and applications. For a trading task (on the top), an agent (in the middle) interacts with a market environment (at the bottom), making sequential decisions. diff --git a/finrl/meta/paper_trading/alpaca.py b/finrl/meta/paper_trading/alpaca.py index e614f6e810..be1866a441 100644 --- a/finrl/meta/paper_trading/alpaca.py +++ b/finrl/meta/paper_trading/alpaca.py @@ -177,7 +177,8 @@ def run(self): qty = abs(int(float(position.qty))) respSO = [] tSubmitOrder = threading.Thread( - target=self.submitOrder(qty, position.symbol, orderSide, respSO) + target=self.submitOrder, + args=(qty, position.symbol, orderSide, respSO), ) tSubmitOrder.start() threads.append(tSubmitOrder) # record thread for joining later @@ -239,9 +240,8 @@ def trade(self): qty = abs(int(sell_num_shares)) respSO = [] tSubmitOrder = threading.Thread( - target=self.submitOrder( - qty, self.stockUniverse[index], "sell", respSO - ) + target=self.submitOrder, + args=(qty, self.stockUniverse[index], "sell", respSO), ) tSubmitOrder.start() threads.append(tSubmitOrder) # record thread for joining later @@ -266,9 +266,8 @@ def trade(self): qty = abs(int(buy_num_shares)) respSO = [] tSubmitOrder = threading.Thread( - target=self.submitOrder( - qty, self.stockUniverse[index], "buy", respSO - ) + target=self.submitOrder, + args=(qty, self.stockUniverse[index], "buy", respSO), ) tSubmitOrder.start() threads.append(tSubmitOrder) # record thread for joining later @@ -289,7 +288,8 @@ def trade(self): qty = abs(int(float(position.qty))) respSO = [] tSubmitOrder = threading.Thread( - target=self.submitOrder(qty, position.symbol, orderSide, respSO) + target=self.submitOrder, + args=(qty, position.symbol, orderSide, respSO), ) tSubmitOrder.start() threads.append(tSubmitOrder) # record thread for joining later diff --git a/unit_tests/test_alpaca_threading.py b/unit_tests/test_alpaca_threading.py new file mode 100644 index 0000000000..ce6db0875e --- /dev/null +++ b/unit_tests/test_alpaca_threading.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import ast +from pathlib import Path + +ALPACA_PATH = ( + Path(__file__).resolve().parents[1] + / "finrl" + / "meta" + / "paper_trading" + / "alpaca.py" +) + + +def _thread_calls(tree: ast.AST) -> list[ast.Call]: + calls: list[ast.Call] = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + func = node.func + if isinstance(func, ast.Attribute) and func.attr == "Thread": + calls.append(node) + return calls + + +def _is_submit_order_attr(node: ast.AST) -> bool: + return isinstance(node, ast.Attribute) and node.attr == "submitOrder" + + +def test_submit_order_thread_target_is_not_called_immediately() -> None: + source = ALPACA_PATH.read_text(encoding="utf-8") + tree = ast.parse(source) + + thread_calls = _thread_calls(tree) + assert thread_calls, "Expected threading.Thread usage in alpaca.py" + + for call in thread_calls: + target_kw = next((kw for kw in call.keywords if kw.arg == "target"), None) + if target_kw is None: + continue + + target = target_kw.value + + # Regression check: ensure submitOrder is not invoked when creating Thread. + if isinstance(target, ast.Call) and _is_submit_order_attr(target.func): + raise AssertionError( + "submitOrder should be passed as target, not called immediately" + ) + + if _is_submit_order_attr(target): + args_kw = next((kw for kw in call.keywords if kw.arg == "args"), None) + assert args_kw is not None, "Thread target submitOrder should pass args=..."