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.
@@ -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)