borro-live 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- borro_live-0.1.0/LICENSE +21 -0
- borro_live-0.1.0/PKG-INFO +212 -0
- borro_live-0.1.0/README.md +196 -0
- borro_live-0.1.0/borro_live/__init__.py +16 -0
- borro_live-0.1.0/borro_live/__main__.py +56 -0
- borro_live-0.1.0/borro_live/ai_hedge_fund.py +164 -0
- borro_live-0.1.0/borro_live/broker.py +403 -0
- borro_live-0.1.0/borro_live/chain.py +127 -0
- borro_live-0.1.0/borro_live/mcp.py +84 -0
- borro_live-0.1.0/borro_live/tradingagents.py +84 -0
- borro_live-0.1.0/borro_live.egg-info/PKG-INFO +212 -0
- borro_live-0.1.0/borro_live.egg-info/SOURCES.txt +17 -0
- borro_live-0.1.0/borro_live.egg-info/dependency_links.txt +1 -0
- borro_live-0.1.0/borro_live.egg-info/entry_points.txt +2 -0
- borro_live-0.1.0/borro_live.egg-info/requires.txt +1 -0
- borro_live-0.1.0/borro_live.egg-info/top_level.txt +1 -0
- borro_live-0.1.0/pyproject.toml +25 -0
- borro_live-0.1.0/setup.cfg +4 -0
- borro_live-0.1.0/tests/test_unit.py +634 -0
borro_live-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Borro
|
|
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.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: borro-live
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Send a trading agent's decisions to Borro on Robinhood Chain: buy, sell, short, cover and lend tokenized stocks against USDG. Adapters for TradingAgents and ai-hedge-fund.
|
|
5
|
+
Author: Borro
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://borro.markets/go-live
|
|
8
|
+
Project-URL: Documentation, https://borro.markets/developers
|
|
9
|
+
Project-URL: Repository, https://github.com/BorroMarkets/borro-live
|
|
10
|
+
Keywords: borro,robinhood-chain,tradingagents,ai-hedge-fund,mcp,agents
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: eth-account>=0.13
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Go Live on Borro
|
|
18
|
+
|
|
19
|
+
`borro-live` sends a trading agent's decisions to [Borro](https://borro.markets) for real: buy, sell, short, cover and
|
|
20
|
+
lend tokenized stocks against USDG on Robinhood Chain. It comes with adapters for
|
|
21
|
+
[TradingAgents](https://github.com/TauricResearch/TradingAgents) and
|
|
22
|
+
[ai-hedge-fund](https://github.com/virattt/ai-hedge-fund), which decide and simulate but place no orders.
|
|
23
|
+
|
|
24
|
+
It is a small Python package: one dependency (`eth-account`), no framework imported unless you have it. It talks to
|
|
25
|
+
Borro's public MCP server (`https://borro.markets/mcp`, no key), which builds each transaction and simulates it; the
|
|
26
|
+
package checks the simulation, signs with your agent's own key and sends through your own RPC.
|
|
27
|
+
|
|
28
|
+
**Read this first.**
|
|
29
|
+
|
|
30
|
+
- **Not advice.** This is plumbing. Nothing here tells you what to trade, and nothing here promises a return. You can
|
|
31
|
+
lose everything you send.
|
|
32
|
+
- **What a stock token is.** A token on Robinhood Chain, issued by Robinhood Assets (Jersey) Limited, whose price
|
|
33
|
+
follows a stock or fund. It is not the share: no vote, no brokerage account, no investor protection scheme. Its price
|
|
34
|
+
on chain can sit away from the stock's, most of all while the stock market is shut.
|
|
35
|
+
- **A short can be liquidated.** If the stock rises far enough, Borro closes the short for you at a loss, and the
|
|
36
|
+
deposit can be lost. A buy here is a long with a small loan against it, and it can be liquidated too if the token
|
|
37
|
+
falls nearly to zero.
|
|
38
|
+
- **It starts as a dry run.** Nothing is sent until you ask for live mode twice: a flag in code and a variable in the
|
|
39
|
+
environment.
|
|
40
|
+
|
|
41
|
+
## Five minutes
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pip install borro-live # or, from a clone: pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Three environment variables:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
export BORRO_AGENT_KEY=0x... # the agent's own wallet key. Use a fresh wallet with little in it.
|
|
51
|
+
export BORRO_RPC_URL=https://... # your own Robinhood Chain RPC (chain id 4663). There is no built-in one.
|
|
52
|
+
export BORRO_LIVE=1 # leave this out until you mean it
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To try it with no key at all, set `BORRO_WALLET=0x...` to any address in place of `BORRO_AGENT_KEY`: every order is
|
|
56
|
+
then a dry run simulated from that address, and nothing can be sent. A simulation passes only if that address holds
|
|
57
|
+
the USDG the order needs.
|
|
58
|
+
|
|
59
|
+
Dry run (leave `BORRO_LIVE` out). It prints Borro's simulation of every step and sends nothing:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
borro-live tickers
|
|
63
|
+
borro-live buy NVDA 5 # 5 USDG of NVDA
|
|
64
|
+
borro-live positions
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Live. The wallet needs USDG for the order and a little ETH for gas:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
BORRO_LIVE=1 borro-live buy NVDA 5 --live
|
|
71
|
+
BORRO_LIVE=1 borro-live sell NVDA --live
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Each live order prints every transaction hash. A dry run needs no RPC and no key at all:
|
|
75
|
+
`borro-live buy NVDA 5 --wallet 0xYourAddress`.
|
|
76
|
+
|
|
77
|
+
## TradingAgents
|
|
78
|
+
|
|
79
|
+
`TradingAgentsGraph.propagate(ticker, date)` returns `(final_state, signal)`, where the signal is Buy, Overweight, Hold,
|
|
80
|
+
Underweight or Sell (or REVIEW when the run reached no rating). One line wraps it:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
84
|
+
from tradingagents.default_config import DEFAULT_CONFIG
|
|
85
|
+
from borro_live import Broker
|
|
86
|
+
from borro_live.tradingagents import go_live
|
|
87
|
+
|
|
88
|
+
ta = go_live(TradingAgentsGraph(config=DEFAULT_CONFIG.copy()), Broker.from_env(), usdg=5)
|
|
89
|
+
_, decision = ta.propagate("NVDA", "2026-09-01")
|
|
90
|
+
print(decision, ta.borro_orders, ta.borro_error)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`propagate` answers exactly as before. What the adapter does with each rating:
|
|
94
|
+
|
|
95
|
+
| Rating | Order |
|
|
96
|
+
|---|---|
|
|
97
|
+
| Buy | cover a short if there is one, then buy `usdg` worth |
|
|
98
|
+
| Overweight | the same, half the size |
|
|
99
|
+
| Hold, REVIEW | nothing |
|
|
100
|
+
| Underweight | sell half of the long, if there is one |
|
|
101
|
+
| Sell | sell all of the long. With `allow_short=True` and no long, short `usdg` worth instead |
|
|
102
|
+
|
|
103
|
+
Without the graph: `borro_live.tradingagents.execute(broker, "NVDA", "Buy", usdg=5)`. Runnable:
|
|
104
|
+
`python examples/tradingagents_example.py NVDA --decision Buy`.
|
|
105
|
+
|
|
106
|
+
## ai-hedge-fund
|
|
107
|
+
|
|
108
|
+
The portfolio manager's decisions (`{ticker: {"action": "buy" | "sell" | "short" | "cover" | "hold", "quantity":
|
|
109
|
+
shares}}`, what `run_hedge_fund` returns under `"decisions"`) go straight in:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from borro_live import Broker
|
|
113
|
+
from borro_live.ai_hedge_fund import execute_decisions
|
|
114
|
+
|
|
115
|
+
results = execute_decisions(Broker.from_env(), result["decisions"], scale=0.01)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`scale` multiplies every quantity, since the model sizes in whole shares for a paper portfolio and the caps here are
|
|
119
|
+
in USDG. Pick it so the largest order lands under your per-order cap (10 USDG unless you raise it): an order over the
|
|
120
|
+
cap is refused, not shrunk. Each ticker gets its own answer; one refusal never stops the others. Runnable:
|
|
121
|
+
`python examples/ai_hedge_fund_example.py --buy NVDA 3`.
|
|
122
|
+
|
|
123
|
+
The v2 engine (`hedge_fund/`, the `aihf` package) has a `Broker` protocol instead: `positions()`, `cash()` and
|
|
124
|
+
`place_order(order)`. `borro_live.ai_hedge_fund.BorroBroker(Broker.from_env())` satisfies it. Whole shares on a signed
|
|
125
|
+
book meet two limits on Borro: a short is closed whole, and an order may not cross zero in one step. Both are refused
|
|
126
|
+
in words, never half done. Treat it as experimental, for three reasons. The engine orders whole shares, so with the
|
|
127
|
+
default 10 USDG cap every order is refused: raise the caps to more than a share of what you trade. The engine expects
|
|
128
|
+
`place_order` to fill or raise, so in a dry run it raises with Borro's simulation and the engine's session stops there.
|
|
129
|
+
And the engine reconciles its ledger against whole shares, while a buy on Borro fills at the share count the swap
|
|
130
|
+
gave. Its own data client also needs its own key.
|
|
131
|
+
|
|
132
|
+
## What each order really is
|
|
133
|
+
|
|
134
|
+
Worked out from the server's own `tools/list`:
|
|
135
|
+
|
|
136
|
+
| `Broker` | Borro tool | What happens |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `buy(symbol, usdg)` | `open_long` at 1.01x | Your USDG buys the stock token; it is lent (it earns lending fees) and posted; 1% of the order is borrowed against it. |
|
|
139
|
+
| `sell(symbol, percent=100)` | `close_long` | The token is sold, the small loan repaid, USDG comes back. |
|
|
140
|
+
| `short(symbol, usdg)` | `open_short` | The stock is borrowed and sold; the sale money and a USDG deposit are held. You pay a borrow fee while it is open. |
|
|
141
|
+
| `cover(symbol)` | `close_short` | Buys back what is owed, returns it, pays out what is left. Borro has one short per stock: this closes all of it. |
|
|
142
|
+
| `lend(asset, amount)` | `supply` | USDG earns interest; a stock token you hold earns lending fees. |
|
|
143
|
+
| `positions()` | `position` | Longs, shorts, what is lent, wallet balances. |
|
|
144
|
+
| `buy_spot_with_eth(symbol, shares)` | `buy_with_eth` | A plain spot buy paid in the wallet's ETH. The token lands in the wallet. |
|
|
145
|
+
|
|
146
|
+
**Borro's MCP server has no tool for a plain spot buy or sell of a stock token for USDG.** `buy` is therefore the
|
|
147
|
+
nearest thing that can also be sold back through Borro: a long at 1.01x. It behaves like holding the token, with two
|
|
148
|
+
differences you should know: the token is lent out while you hold it, and there is a loan of 1% of the order against
|
|
149
|
+
it, so the position can be liquidated if the token loses nearly all its value. `buy_spot_with_eth` is a true spot
|
|
150
|
+
buy, but it pays in ETH and no Borro tool sells the token back; lend it, or sell it elsewhere.
|
|
151
|
+
|
|
152
|
+
`short` deposits 1 USDG for each USDG shorted by default (`short_deposit=1.0`), and never less than a tenth over the
|
|
153
|
+
least Borro asks. At that default a short is closed for you after a rise of roughly half; the quote in every answer
|
|
154
|
+
gives the exact price (`closed_for_you_at_price`). A thinner deposit is closed sooner.
|
|
155
|
+
|
|
156
|
+
## Safety
|
|
157
|
+
|
|
158
|
+
- **Dry run by default.** Without `live=True` and `BORRO_LIVE=1` together, an order returns the tool's own simulation
|
|
159
|
+
and sends nothing. `live=True` without the variable refuses to start; the variable without the flag stays a dry run.
|
|
160
|
+
- **Caps you set.** `max_order_usdg` (default 10) is the most one buy, short or lend may be worth; `max_day_usdg`
|
|
161
|
+
(default 25) is the most they may add up to in one UTC day, per wallet. Over either, the order is refused before
|
|
162
|
+
anything is signed. The day's total is kept in `~/.borro-live/state.json` (or `BORRO_LIVE_STATE`), counted before the
|
|
163
|
+
first transaction goes out. Selling and covering are never stopped by a cap. Set them in code or with
|
|
164
|
+
`BORRO_MAX_ORDER_USDG` and `BORRO_MAX_DAY_USDG`.
|
|
165
|
+
- **Listed tickers only.** A ticker Borro does not list is refused before any trade tool is called.
|
|
166
|
+
- **A failed simulation is never sent.** Every step must pass Borro's simulation, and each step is run again from the
|
|
167
|
+
wallet (`eth_call`, then a gas estimate) just before it is signed. Steps go in order; each receipt is checked before
|
|
168
|
+
the next.
|
|
169
|
+
- **What is signed.** Only transactions for your wallet on chain 4663. No order sends ETH except
|
|
170
|
+
`buy_spot_with_eth`, and never more than its quote. A USDG approval is refused if it is for more than the order.
|
|
171
|
+
- **The key.** Read from `BORRO_AGENT_KEY`, kept in memory, never printed, logged or written. Errors never carry your
|
|
172
|
+
RPC address.
|
|
173
|
+
- **Trust.** Borro's server builds the transactions. Read a dry run's `transactions` before going live: each has the
|
|
174
|
+
contract it goes to and what it does. The `rules` tool lists Borro's contracts.
|
|
175
|
+
|
|
176
|
+
## Builder codes: a fee share for whoever ships the integration
|
|
177
|
+
|
|
178
|
+
Borro shares its own fees with whoever brings the business (the Bookrunners programme). Give the broker a code and
|
|
179
|
+
installs that carry it pay the code's owner:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
Broker.from_env(builder_code="yourcode") # or BORRO_BUILDER_CODE=yourcode
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- The wallet is bound to the code once, with one free signature (`bind_builder`, no transaction). From then on the
|
|
186
|
+
code earns Borro's fee on the interest that wallet pays when it borrows (shorts and the loan under a buy).
|
|
187
|
+
- Lends go through the `BorroBuilders` contract with the code, so the code earns Borro's fee on what those deposits
|
|
188
|
+
earn. The wallet keeps its vault shares.
|
|
189
|
+
- The share is all of Borro's fee for 12 months, half after, paid monthly in USDG to the wallet that registered the
|
|
190
|
+
code. The user pays nothing extra. Fees float with demand: with no borrowing there is no fee to share.
|
|
191
|
+
- Register a code with your own wallet at [borro.markets/bookrunners](https://borro.markets/bookrunners). First come,
|
|
192
|
+
first served; only that wallet can move it. One code per wallet: the first binding stands.
|
|
193
|
+
|
|
194
|
+
There is no default code. If you ship this in a framework, set yours.
|
|
195
|
+
|
|
196
|
+
## Tests
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
python -m unittest discover -s tests -p 'test_unit.py' # 50 tests against recorded MCP answers, no network
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Borro also runs the kit end to end on a local fork of Robinhood Chain in its own repository: a dry run, a live buy, lend,
|
|
203
|
+
short, cover and sell, both adapters, the caps and a builder code. `BORRO_MCP_URL` points the client at another MCP
|
|
204
|
+
address, which is how that test reaches its local server.
|
|
205
|
+
|
|
206
|
+
`walk.sh` is the one real walk: one small live order per framework from a funded wallet. It asks before it sends.
|
|
207
|
+
|
|
208
|
+
## Not here
|
|
209
|
+
|
|
210
|
+
Options, perps and Borro's dark pool are not part of this kit.
|
|
211
|
+
|
|
212
|
+
MIT licence.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Go Live on Borro
|
|
2
|
+
|
|
3
|
+
`borro-live` sends a trading agent's decisions to [Borro](https://borro.markets) for real: buy, sell, short, cover and
|
|
4
|
+
lend tokenized stocks against USDG on Robinhood Chain. It comes with adapters for
|
|
5
|
+
[TradingAgents](https://github.com/TauricResearch/TradingAgents) and
|
|
6
|
+
[ai-hedge-fund](https://github.com/virattt/ai-hedge-fund), which decide and simulate but place no orders.
|
|
7
|
+
|
|
8
|
+
It is a small Python package: one dependency (`eth-account`), no framework imported unless you have it. It talks to
|
|
9
|
+
Borro's public MCP server (`https://borro.markets/mcp`, no key), which builds each transaction and simulates it; the
|
|
10
|
+
package checks the simulation, signs with your agent's own key and sends through your own RPC.
|
|
11
|
+
|
|
12
|
+
**Read this first.**
|
|
13
|
+
|
|
14
|
+
- **Not advice.** This is plumbing. Nothing here tells you what to trade, and nothing here promises a return. You can
|
|
15
|
+
lose everything you send.
|
|
16
|
+
- **What a stock token is.** A token on Robinhood Chain, issued by Robinhood Assets (Jersey) Limited, whose price
|
|
17
|
+
follows a stock or fund. It is not the share: no vote, no brokerage account, no investor protection scheme. Its price
|
|
18
|
+
on chain can sit away from the stock's, most of all while the stock market is shut.
|
|
19
|
+
- **A short can be liquidated.** If the stock rises far enough, Borro closes the short for you at a loss, and the
|
|
20
|
+
deposit can be lost. A buy here is a long with a small loan against it, and it can be liquidated too if the token
|
|
21
|
+
falls nearly to zero.
|
|
22
|
+
- **It starts as a dry run.** Nothing is sent until you ask for live mode twice: a flag in code and a variable in the
|
|
23
|
+
environment.
|
|
24
|
+
|
|
25
|
+
## Five minutes
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install borro-live # or, from a clone: pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Three environment variables:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
export BORRO_AGENT_KEY=0x... # the agent's own wallet key. Use a fresh wallet with little in it.
|
|
35
|
+
export BORRO_RPC_URL=https://... # your own Robinhood Chain RPC (chain id 4663). There is no built-in one.
|
|
36
|
+
export BORRO_LIVE=1 # leave this out until you mean it
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To try it with no key at all, set `BORRO_WALLET=0x...` to any address in place of `BORRO_AGENT_KEY`: every order is
|
|
40
|
+
then a dry run simulated from that address, and nothing can be sent. A simulation passes only if that address holds
|
|
41
|
+
the USDG the order needs.
|
|
42
|
+
|
|
43
|
+
Dry run (leave `BORRO_LIVE` out). It prints Borro's simulation of every step and sends nothing:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
borro-live tickers
|
|
47
|
+
borro-live buy NVDA 5 # 5 USDG of NVDA
|
|
48
|
+
borro-live positions
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Live. The wallet needs USDG for the order and a little ETH for gas:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
BORRO_LIVE=1 borro-live buy NVDA 5 --live
|
|
55
|
+
BORRO_LIVE=1 borro-live sell NVDA --live
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Each live order prints every transaction hash. A dry run needs no RPC and no key at all:
|
|
59
|
+
`borro-live buy NVDA 5 --wallet 0xYourAddress`.
|
|
60
|
+
|
|
61
|
+
## TradingAgents
|
|
62
|
+
|
|
63
|
+
`TradingAgentsGraph.propagate(ticker, date)` returns `(final_state, signal)`, where the signal is Buy, Overweight, Hold,
|
|
64
|
+
Underweight or Sell (or REVIEW when the run reached no rating). One line wraps it:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
68
|
+
from tradingagents.default_config import DEFAULT_CONFIG
|
|
69
|
+
from borro_live import Broker
|
|
70
|
+
from borro_live.tradingagents import go_live
|
|
71
|
+
|
|
72
|
+
ta = go_live(TradingAgentsGraph(config=DEFAULT_CONFIG.copy()), Broker.from_env(), usdg=5)
|
|
73
|
+
_, decision = ta.propagate("NVDA", "2026-09-01")
|
|
74
|
+
print(decision, ta.borro_orders, ta.borro_error)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`propagate` answers exactly as before. What the adapter does with each rating:
|
|
78
|
+
|
|
79
|
+
| Rating | Order |
|
|
80
|
+
|---|---|
|
|
81
|
+
| Buy | cover a short if there is one, then buy `usdg` worth |
|
|
82
|
+
| Overweight | the same, half the size |
|
|
83
|
+
| Hold, REVIEW | nothing |
|
|
84
|
+
| Underweight | sell half of the long, if there is one |
|
|
85
|
+
| Sell | sell all of the long. With `allow_short=True` and no long, short `usdg` worth instead |
|
|
86
|
+
|
|
87
|
+
Without the graph: `borro_live.tradingagents.execute(broker, "NVDA", "Buy", usdg=5)`. Runnable:
|
|
88
|
+
`python examples/tradingagents_example.py NVDA --decision Buy`.
|
|
89
|
+
|
|
90
|
+
## ai-hedge-fund
|
|
91
|
+
|
|
92
|
+
The portfolio manager's decisions (`{ticker: {"action": "buy" | "sell" | "short" | "cover" | "hold", "quantity":
|
|
93
|
+
shares}}`, what `run_hedge_fund` returns under `"decisions"`) go straight in:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from borro_live import Broker
|
|
97
|
+
from borro_live.ai_hedge_fund import execute_decisions
|
|
98
|
+
|
|
99
|
+
results = execute_decisions(Broker.from_env(), result["decisions"], scale=0.01)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`scale` multiplies every quantity, since the model sizes in whole shares for a paper portfolio and the caps here are
|
|
103
|
+
in USDG. Pick it so the largest order lands under your per-order cap (10 USDG unless you raise it): an order over the
|
|
104
|
+
cap is refused, not shrunk. Each ticker gets its own answer; one refusal never stops the others. Runnable:
|
|
105
|
+
`python examples/ai_hedge_fund_example.py --buy NVDA 3`.
|
|
106
|
+
|
|
107
|
+
The v2 engine (`hedge_fund/`, the `aihf` package) has a `Broker` protocol instead: `positions()`, `cash()` and
|
|
108
|
+
`place_order(order)`. `borro_live.ai_hedge_fund.BorroBroker(Broker.from_env())` satisfies it. Whole shares on a signed
|
|
109
|
+
book meet two limits on Borro: a short is closed whole, and an order may not cross zero in one step. Both are refused
|
|
110
|
+
in words, never half done. Treat it as experimental, for three reasons. The engine orders whole shares, so with the
|
|
111
|
+
default 10 USDG cap every order is refused: raise the caps to more than a share of what you trade. The engine expects
|
|
112
|
+
`place_order` to fill or raise, so in a dry run it raises with Borro's simulation and the engine's session stops there.
|
|
113
|
+
And the engine reconciles its ledger against whole shares, while a buy on Borro fills at the share count the swap
|
|
114
|
+
gave. Its own data client also needs its own key.
|
|
115
|
+
|
|
116
|
+
## What each order really is
|
|
117
|
+
|
|
118
|
+
Worked out from the server's own `tools/list`:
|
|
119
|
+
|
|
120
|
+
| `Broker` | Borro tool | What happens |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `buy(symbol, usdg)` | `open_long` at 1.01x | Your USDG buys the stock token; it is lent (it earns lending fees) and posted; 1% of the order is borrowed against it. |
|
|
123
|
+
| `sell(symbol, percent=100)` | `close_long` | The token is sold, the small loan repaid, USDG comes back. |
|
|
124
|
+
| `short(symbol, usdg)` | `open_short` | The stock is borrowed and sold; the sale money and a USDG deposit are held. You pay a borrow fee while it is open. |
|
|
125
|
+
| `cover(symbol)` | `close_short` | Buys back what is owed, returns it, pays out what is left. Borro has one short per stock: this closes all of it. |
|
|
126
|
+
| `lend(asset, amount)` | `supply` | USDG earns interest; a stock token you hold earns lending fees. |
|
|
127
|
+
| `positions()` | `position` | Longs, shorts, what is lent, wallet balances. |
|
|
128
|
+
| `buy_spot_with_eth(symbol, shares)` | `buy_with_eth` | A plain spot buy paid in the wallet's ETH. The token lands in the wallet. |
|
|
129
|
+
|
|
130
|
+
**Borro's MCP server has no tool for a plain spot buy or sell of a stock token for USDG.** `buy` is therefore the
|
|
131
|
+
nearest thing that can also be sold back through Borro: a long at 1.01x. It behaves like holding the token, with two
|
|
132
|
+
differences you should know: the token is lent out while you hold it, and there is a loan of 1% of the order against
|
|
133
|
+
it, so the position can be liquidated if the token loses nearly all its value. `buy_spot_with_eth` is a true spot
|
|
134
|
+
buy, but it pays in ETH and no Borro tool sells the token back; lend it, or sell it elsewhere.
|
|
135
|
+
|
|
136
|
+
`short` deposits 1 USDG for each USDG shorted by default (`short_deposit=1.0`), and never less than a tenth over the
|
|
137
|
+
least Borro asks. At that default a short is closed for you after a rise of roughly half; the quote in every answer
|
|
138
|
+
gives the exact price (`closed_for_you_at_price`). A thinner deposit is closed sooner.
|
|
139
|
+
|
|
140
|
+
## Safety
|
|
141
|
+
|
|
142
|
+
- **Dry run by default.** Without `live=True` and `BORRO_LIVE=1` together, an order returns the tool's own simulation
|
|
143
|
+
and sends nothing. `live=True` without the variable refuses to start; the variable without the flag stays a dry run.
|
|
144
|
+
- **Caps you set.** `max_order_usdg` (default 10) is the most one buy, short or lend may be worth; `max_day_usdg`
|
|
145
|
+
(default 25) is the most they may add up to in one UTC day, per wallet. Over either, the order is refused before
|
|
146
|
+
anything is signed. The day's total is kept in `~/.borro-live/state.json` (or `BORRO_LIVE_STATE`), counted before the
|
|
147
|
+
first transaction goes out. Selling and covering are never stopped by a cap. Set them in code or with
|
|
148
|
+
`BORRO_MAX_ORDER_USDG` and `BORRO_MAX_DAY_USDG`.
|
|
149
|
+
- **Listed tickers only.** A ticker Borro does not list is refused before any trade tool is called.
|
|
150
|
+
- **A failed simulation is never sent.** Every step must pass Borro's simulation, and each step is run again from the
|
|
151
|
+
wallet (`eth_call`, then a gas estimate) just before it is signed. Steps go in order; each receipt is checked before
|
|
152
|
+
the next.
|
|
153
|
+
- **What is signed.** Only transactions for your wallet on chain 4663. No order sends ETH except
|
|
154
|
+
`buy_spot_with_eth`, and never more than its quote. A USDG approval is refused if it is for more than the order.
|
|
155
|
+
- **The key.** Read from `BORRO_AGENT_KEY`, kept in memory, never printed, logged or written. Errors never carry your
|
|
156
|
+
RPC address.
|
|
157
|
+
- **Trust.** Borro's server builds the transactions. Read a dry run's `transactions` before going live: each has the
|
|
158
|
+
contract it goes to and what it does. The `rules` tool lists Borro's contracts.
|
|
159
|
+
|
|
160
|
+
## Builder codes: a fee share for whoever ships the integration
|
|
161
|
+
|
|
162
|
+
Borro shares its own fees with whoever brings the business (the Bookrunners programme). Give the broker a code and
|
|
163
|
+
installs that carry it pay the code's owner:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
Broker.from_env(builder_code="yourcode") # or BORRO_BUILDER_CODE=yourcode
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- The wallet is bound to the code once, with one free signature (`bind_builder`, no transaction). From then on the
|
|
170
|
+
code earns Borro's fee on the interest that wallet pays when it borrows (shorts and the loan under a buy).
|
|
171
|
+
- Lends go through the `BorroBuilders` contract with the code, so the code earns Borro's fee on what those deposits
|
|
172
|
+
earn. The wallet keeps its vault shares.
|
|
173
|
+
- The share is all of Borro's fee for 12 months, half after, paid monthly in USDG to the wallet that registered the
|
|
174
|
+
code. The user pays nothing extra. Fees float with demand: with no borrowing there is no fee to share.
|
|
175
|
+
- Register a code with your own wallet at [borro.markets/bookrunners](https://borro.markets/bookrunners). First come,
|
|
176
|
+
first served; only that wallet can move it. One code per wallet: the first binding stands.
|
|
177
|
+
|
|
178
|
+
There is no default code. If you ship this in a framework, set yours.
|
|
179
|
+
|
|
180
|
+
## Tests
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
python -m unittest discover -s tests -p 'test_unit.py' # 50 tests against recorded MCP answers, no network
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Borro also runs the kit end to end on a local fork of Robinhood Chain in its own repository: a dry run, a live buy, lend,
|
|
187
|
+
short, cover and sell, both adapters, the caps and a builder code. `BORRO_MCP_URL` points the client at another MCP
|
|
188
|
+
address, which is how that test reaches its local server.
|
|
189
|
+
|
|
190
|
+
`walk.sh` is the one real walk: one small live order per framework from a funded wallet. It asks before it sends.
|
|
191
|
+
|
|
192
|
+
## Not here
|
|
193
|
+
|
|
194
|
+
Options, perps and Borro's dark pool are not part of this kit.
|
|
195
|
+
|
|
196
|
+
MIT licence.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""borro-live: send a trading agent's decisions to Borro on Robinhood Chain.
|
|
2
|
+
|
|
3
|
+
from borro_live import Broker
|
|
4
|
+
broker = Broker.from_env() # dry run unless BORRO_LIVE=1
|
|
5
|
+
broker.buy("NVDA", 5) # 5 USDG
|
|
6
|
+
|
|
7
|
+
Adapters: ``borro_live.tradingagents`` and ``borro_live.ai_hedge_fund``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .broker import BorroLiveError, Broker, CapExceeded, Result, SimulationFailed, UnlistedTicker, Unsafe
|
|
11
|
+
from .chain import Chain, ChainError
|
|
12
|
+
from .mcp import McpClient, McpError, ToolRefused
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
__all__ = ["Broker", "Result", "McpClient", "Chain", "BorroLiveError", "UnlistedTicker", "CapExceeded", "SimulationFailed", "Unsafe",
|
|
16
|
+
"ChainError", "McpError", "ToolRefused"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""borro-live on the command line: one order, or the wallet's positions, printed as JSON.
|
|
2
|
+
|
|
3
|
+
borro-live tickers
|
|
4
|
+
borro-live positions
|
|
5
|
+
borro-live buy NVDA 5 a dry run: Borro's simulation, nothing sent
|
|
6
|
+
borro-live buy NVDA 5 --live sends, and only with BORRO_LIVE=1 in the environment as well
|
|
7
|
+
borro-live sell NVDA [percent] | short NVDA 5 | cover NVDA | lend USDG 5
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .broker import BorroLiveError, Broker
|
|
17
|
+
from .chain import ChainError
|
|
18
|
+
from .mcp import McpClient, McpError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
ap = argparse.ArgumentParser(prog="borro-live", description="One order on Borro, dry run unless --live and BORRO_LIVE=1.")
|
|
23
|
+
ap.add_argument("action", choices=["tickers", "positions", "buy", "sell", "short", "cover", "lend"])
|
|
24
|
+
ap.add_argument("symbol", nargs="?")
|
|
25
|
+
ap.add_argument("amount", nargs="?", type=float, help="USDG for buy and short; percent for sell; units for lend")
|
|
26
|
+
ap.add_argument("--live", action="store_true", help="send for real (also needs BORRO_LIVE=1)")
|
|
27
|
+
ap.add_argument("--wallet", help="an address, for dry runs without a key")
|
|
28
|
+
ap.add_argument("--builder-code")
|
|
29
|
+
ap.add_argument("--max-order", type=float)
|
|
30
|
+
ap.add_argument("--max-day", type=float)
|
|
31
|
+
a = ap.parse_args(argv)
|
|
32
|
+
try:
|
|
33
|
+
if a.action == "tickers": # a public read: no key and no wallet needed
|
|
34
|
+
print(json.dumps([s["symbol"] for s in McpClient().call("markets")["stocks"]], indent=1))
|
|
35
|
+
return 0
|
|
36
|
+
b = Broker(live=a.live, wallet=a.wallet, builder_code=a.builder_code, max_order_usdg=a.max_order, max_day_usdg=a.max_day)
|
|
37
|
+
if a.action == "positions":
|
|
38
|
+
out = {k: v for k, v in b.positions().items() if k != "raw"}
|
|
39
|
+
else:
|
|
40
|
+
if not a.symbol or (a.amount is None and a.action in ("buy", "short", "lend")):
|
|
41
|
+
ap.error(f"{a.action} needs a symbol" + (" and an amount" if a.action in ("buy", "short", "lend") else ""))
|
|
42
|
+
if a.action == "sell":
|
|
43
|
+
out = b.sell(a.symbol, a.amount if a.amount is not None else 100).to_dict()
|
|
44
|
+
elif a.action == "cover":
|
|
45
|
+
out = b.cover(a.symbol).to_dict()
|
|
46
|
+
else:
|
|
47
|
+
out = getattr(b, a.action)(a.symbol, a.amount).to_dict()
|
|
48
|
+
except (BorroLiveError, McpError, ChainError, ValueError) as e:
|
|
49
|
+
print(json.dumps({"ok": False, "error": str(e)}))
|
|
50
|
+
return 1
|
|
51
|
+
print(json.dumps(out, indent=1))
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
sys.exit(main())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""ai-hedge-fund on Borro.
|
|
2
|
+
|
|
3
|
+
Two shapes, because the project has two:
|
|
4
|
+
|
|
5
|
+
* The portfolio manager's decisions, ``{ticker: {"action": "buy" | "sell" | "short" | "cover" | "hold",
|
|
6
|
+
"quantity": shares, ...}}`` (what ``run_hedge_fund`` returns under ``"decisions"``): ``execute_decisions``.
|
|
7
|
+
* The v2 engine's ``Broker`` protocol (``positions()``, ``cash()``, ``place_order(order)`` with buy and sell orders
|
|
8
|
+
on signed share counts): ``BorroBroker``.
|
|
9
|
+
|
|
10
|
+
ai-hedge-fund itself is never imported unless it is installed: plain dicts and objects work.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from .broker import BorroLiveError, Broker, Result
|
|
18
|
+
from .chain import ChainError
|
|
19
|
+
from .mcp import McpError
|
|
20
|
+
|
|
21
|
+
ACTIONS = ("buy", "sell", "short", "cover", "hold")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get(obj, name, default=None):
|
|
25
|
+
return obj.get(name, default) if isinstance(obj, dict) else getattr(obj, name, default)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _long_shares(pos: dict, symbol: str) -> float:
|
|
29
|
+
if not any(p.get("symbol") == symbol for p in pos["longs"]):
|
|
30
|
+
return 0.0
|
|
31
|
+
row = next((s for s in pos["stocks"] if s.get("symbol") == symbol), {})
|
|
32
|
+
return float((row.get("usdg_loan") or {}).get("posted_stock") or 0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _short_shares(pos: dict, symbol: str) -> float:
|
|
36
|
+
return float(next((s.get("owed_stock") or 0 for s in pos["shorts"] if s.get("symbol") == symbol), 0))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def execute_decisions(broker: Broker, decisions, scale: float = 1.0) -> dict:
|
|
40
|
+
"""Execute the portfolio manager's decisions, ticker by ticker.
|
|
41
|
+
|
|
42
|
+
``decisions`` is the dict of decisions, the whole ``run_hedge_fund`` result, or the manager's output object.
|
|
43
|
+
``scale`` multiplies every quantity (0.01 turns 10 shares into 0.1), since the model sizes for a paper
|
|
44
|
+
portfolio and the caps here are in USDG. Returns ``{ticker: Result}``; a ticker this kit or Borro refused maps
|
|
45
|
+
to the reason as a string, and a hold to None, so one refusal never stops the others.
|
|
46
|
+
|
|
47
|
+
buy buy ``quantity`` shares' worth sell sell that share of the long
|
|
48
|
+
short short ``quantity`` shares' worth cover close the short (Borro closes a short whole)
|
|
49
|
+
"""
|
|
50
|
+
inner = _get(decisions, "decisions")
|
|
51
|
+
decisions = inner if inner is not None else decisions
|
|
52
|
+
if not isinstance(decisions, dict):
|
|
53
|
+
raise BorroLiveError("decisions must be a dict of ticker to {action, quantity}")
|
|
54
|
+
out: dict = {}
|
|
55
|
+
pos = None
|
|
56
|
+
for ticker, d in decisions.items():
|
|
57
|
+
symbol = str(ticker).strip().upper()
|
|
58
|
+
action = str(_get(d, "action", "hold")).strip().lower()
|
|
59
|
+
qty = float(_get(d, "quantity", 0) or 0) * scale
|
|
60
|
+
try:
|
|
61
|
+
if action not in ACTIONS:
|
|
62
|
+
raise BorroLiveError(f"unknown action {action}")
|
|
63
|
+
if action == "hold" or (qty <= 0 and action != "cover"):
|
|
64
|
+
out[symbol] = None
|
|
65
|
+
elif action == "buy":
|
|
66
|
+
out[symbol] = broker.buy(symbol, qty * broker.price(symbol))
|
|
67
|
+
elif action == "short":
|
|
68
|
+
out[symbol] = broker.short(symbol, qty * broker.price(symbol))
|
|
69
|
+
elif action == "cover":
|
|
70
|
+
out[symbol] = broker.cover(symbol)
|
|
71
|
+
else:
|
|
72
|
+
broker.price(symbol)
|
|
73
|
+
pos = pos or broker.positions()
|
|
74
|
+
held = _long_shares(pos, symbol)
|
|
75
|
+
if held <= 0:
|
|
76
|
+
raise BorroLiveError(f"no long on {symbol} to sell")
|
|
77
|
+
out[symbol] = broker.sell(symbol, max(1.0, min(100.0, 100.0 * qty / held)))
|
|
78
|
+
except (BorroLiveError, McpError, ChainError) as e:
|
|
79
|
+
out[symbol] = f"refused: {e}"
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class _Position:
|
|
85
|
+
ticker: str
|
|
86
|
+
shares: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class _Fill:
|
|
91
|
+
ticker: str
|
|
92
|
+
side: str
|
|
93
|
+
quantity: int
|
|
94
|
+
price: float
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _models():
|
|
98
|
+
try:
|
|
99
|
+
from hedge_fund.brokers.models import Fill, Position # the real ones when ai-hedge-fund is installed
|
|
100
|
+
return Position, Fill
|
|
101
|
+
except Exception:
|
|
102
|
+
return _Position, _Fill
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DryRun(BorroLiveError):
|
|
106
|
+
"""place_order on a broker that is not live: nothing was sent. ``result`` holds Borro's simulation."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, result: Result):
|
|
109
|
+
super().__init__(f"dry run: {result.action} {result.symbol} was simulated ({'ok' if result.ok else 'it would fail'}) and not sent")
|
|
110
|
+
self.result = result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BorroBroker:
|
|
114
|
+
"""ai-hedge-fund's v2 ``Broker`` protocol over a live ``borro_live.Broker``.
|
|
115
|
+
|
|
116
|
+
Orders there are whole shares on a signed book: a sell past zero is a short, a buy toward zero a cover. On Borro
|
|
117
|
+
a buy is a 1.01x long and its share count is what the swap gave, so ``positions()`` rounds to whole shares. An
|
|
118
|
+
order fills completely or raises: one that would cross zero, or cover part of a short, is refused (Borro closes
|
|
119
|
+
a short whole). A dry-run broker raises ``DryRun`` with the simulation, since nothing was filled.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, broker: Broker):
|
|
123
|
+
self.broker = broker
|
|
124
|
+
self.results: list[Result] = []
|
|
125
|
+
|
|
126
|
+
def positions(self) -> dict:
|
|
127
|
+
Position, _ = _models()
|
|
128
|
+
pos = self.broker.positions()
|
|
129
|
+
book = {}
|
|
130
|
+
for symbol in {p["symbol"] for p in pos["longs"]} | {p["symbol"] for p in pos["shorts"]}:
|
|
131
|
+
shares = round(_long_shares(pos, symbol) - _short_shares(pos, symbol))
|
|
132
|
+
if shares:
|
|
133
|
+
book[symbol] = Position(ticker=symbol, shares=shares)
|
|
134
|
+
return book
|
|
135
|
+
|
|
136
|
+
def cash(self) -> float:
|
|
137
|
+
return float(self.broker.positions()["usdg"] or 0)
|
|
138
|
+
|
|
139
|
+
def place_order(self, order):
|
|
140
|
+
_, Fill = _models()
|
|
141
|
+
symbol, side, qty, ref = str(_get(order, "ticker")).upper(), _get(order, "side"), int(_get(order, "quantity")), float(_get(order, "price"))
|
|
142
|
+
if side not in ("buy", "sell") or qty <= 0:
|
|
143
|
+
raise BorroLiveError("an order is buy or sell of at least one share")
|
|
144
|
+
pos = self.broker.positions()
|
|
145
|
+
long_, short_ = _long_shares(pos, symbol), _short_shares(pos, symbol)
|
|
146
|
+
price = self.broker.price(symbol)
|
|
147
|
+
if side == "buy" and short_ > 0:
|
|
148
|
+
if qty != round(short_):
|
|
149
|
+
raise BorroLiveError(f"Borro closes a short whole: buy {round(short_)} {symbol} to cover it, then buy more")
|
|
150
|
+
res = self.broker.cover(symbol)
|
|
151
|
+
elif side == "buy":
|
|
152
|
+
res = self.broker.buy(symbol, qty * price)
|
|
153
|
+
elif long_ > 0:
|
|
154
|
+
if qty > round(long_):
|
|
155
|
+
raise BorroLiveError(f"sell the {round(long_)} {symbol} held first, then short")
|
|
156
|
+
res = self.broker.sell(symbol, min(100.0, 100.0 * qty / long_))
|
|
157
|
+
else:
|
|
158
|
+
res = self.broker.short(symbol, qty * price)
|
|
159
|
+
self.results.append(res)
|
|
160
|
+
if not res.sent:
|
|
161
|
+
raise DryRun(res)
|
|
162
|
+
q = res.quote
|
|
163
|
+
filled = float(q["size_usdg"]) / float(q["stock"]) if q.get("size_usdg") and q.get("stock") else float(q.get("price_usd") or price or ref)
|
|
164
|
+
return Fill(ticker=symbol, side=side, quantity=qty, price=filled)
|