Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- README.md +3 -9
- __pycache__/accounts_client.cpython-312.pyc +0 -0
- __pycache__/traders.cpython-312.pyc +0 -0
- __pycache__/trading_floor.cpython-312.pyc +0 -0
- __pycache__/util.cpython-312.pyc +0 -0
- accounts.db +3 -0
- accounts.py +186 -0
- accounts_client.py +50 -0
- accounts_server.py +70 -0
- app.py +190 -0
- database.py +101 -0
- market.py +70 -0
- market_server.py +16 -0
- mcp_params.py +46 -0
- memory/Cathie.db +0 -0
- memory/George.db +0 -0
- memory/Ray.db +0 -0
- memory/Warren.db +0 -0
- memory/ed.db +0 -0
- memory/memory.txt +0 -0
- push_server.py +31 -0
- requirements.txt +10 -0
- reset.py +50 -0
- templates.py +86 -0
- tracers.py +75 -0
- traders.py +131 -0
- trading_floor.py +54 -0
- util.py +50 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
accounts.db filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 📈
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: blue
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 5.43.1
|
| 8 |
app_file: app.py
|
| 9 |
-
|
|
|
|
| 10 |
---
|
| 11 |
-
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: MCP_TradingAgents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
app_file: app.py
|
| 4 |
+
sdk: gradio
|
| 5 |
+
sdk_version: 5.42.0
|
| 6 |
---
|
|
|
|
|
|
__pycache__/accounts_client.cpython-312.pyc
ADDED
|
Binary file (5.29 kB). View file
|
|
|
__pycache__/traders.cpython-312.pyc
ADDED
|
Binary file (7.95 kB). View file
|
|
|
__pycache__/trading_floor.cpython-312.pyc
ADDED
|
Binary file (2.71 kB). View file
|
|
|
__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (1.27 kB). View file
|
|
|
accounts.db
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1dd78c0461a4717cb42ffcbe55365a3a585948eda79e17e3a69fc65f663d03dd
|
| 3 |
+
size 241664
|
accounts.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
import json
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from market import get_share_price
|
| 6 |
+
from database import write_account, read_account, write_log
|
| 7 |
+
|
| 8 |
+
load_dotenv(override=True)
|
| 9 |
+
|
| 10 |
+
INITIAL_BALANCE = 10_000.0
|
| 11 |
+
SPREAD = 0.002
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Transaction(BaseModel):
|
| 15 |
+
symbol: str
|
| 16 |
+
quantity: int
|
| 17 |
+
price: float
|
| 18 |
+
timestamp: str
|
| 19 |
+
rationale: str
|
| 20 |
+
|
| 21 |
+
def total(self) -> float:
|
| 22 |
+
return self.quantity * self.price
|
| 23 |
+
|
| 24 |
+
def __repr__(self):
|
| 25 |
+
return f"{abs(self.quantity)} shares of {self.symbol} at {self.price} each."
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Account(BaseModel):
|
| 29 |
+
name: str
|
| 30 |
+
balance: float
|
| 31 |
+
strategy: str
|
| 32 |
+
holdings: dict[str, int]
|
| 33 |
+
transactions: list[Transaction]
|
| 34 |
+
portfolio_value_time_series: list[tuple[str, float]]
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def get(cls, name: str):
|
| 38 |
+
fields = read_account(name.lower())
|
| 39 |
+
if not fields:
|
| 40 |
+
fields = {
|
| 41 |
+
"name": name.lower(),
|
| 42 |
+
"balance": INITIAL_BALANCE,
|
| 43 |
+
"strategy": "",
|
| 44 |
+
"holdings": {},
|
| 45 |
+
"transactions": [],
|
| 46 |
+
"portfolio_value_time_series": []
|
| 47 |
+
}
|
| 48 |
+
write_account(name, fields)
|
| 49 |
+
return cls(**fields)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def save(self):
|
| 53 |
+
write_account(self.name.lower(), self.model_dump())
|
| 54 |
+
|
| 55 |
+
def reset(self, strategy: str):
|
| 56 |
+
self.balance = INITIAL_BALANCE
|
| 57 |
+
self.strategy = strategy
|
| 58 |
+
self.holdings = {}
|
| 59 |
+
self.transactions = []
|
| 60 |
+
self.portfolio_value_time_series = []
|
| 61 |
+
self.save()
|
| 62 |
+
|
| 63 |
+
def deposit(self, amount: float):
|
| 64 |
+
""" Deposit funds into the account. """
|
| 65 |
+
if amount <= 0:
|
| 66 |
+
raise ValueError("Deposit amount must be positive.")
|
| 67 |
+
self.balance += amount
|
| 68 |
+
print(f"Deposited ${amount}. New balance: ${self.balance}")
|
| 69 |
+
self.save()
|
| 70 |
+
|
| 71 |
+
def withdraw(self, amount: float):
|
| 72 |
+
""" Withdraw funds from the account, ensuring it doesn't go negative. """
|
| 73 |
+
if amount > self.balance:
|
| 74 |
+
raise ValueError("Insufficient funds for withdrawal.")
|
| 75 |
+
self.balance -= amount
|
| 76 |
+
print(f"Withdrew ${amount}. New balance: ${self.balance}")
|
| 77 |
+
self.save()
|
| 78 |
+
|
| 79 |
+
def buy_shares(self, symbol: str, quantity: int, rationale: str) -> str:
|
| 80 |
+
""" Buy shares of a stock if sufficient funds are available. """
|
| 81 |
+
price = get_share_price(symbol)
|
| 82 |
+
buy_price = price * (1 + SPREAD)
|
| 83 |
+
total_cost = buy_price * quantity
|
| 84 |
+
|
| 85 |
+
if total_cost > self.balance:
|
| 86 |
+
raise ValueError("Insufficient funds to buy shares.")
|
| 87 |
+
elif price==0:
|
| 88 |
+
raise ValueError(f"Unrecognized symbol {symbol}")
|
| 89 |
+
|
| 90 |
+
# Update holdings
|
| 91 |
+
self.holdings[symbol] = self.holdings.get(symbol, 0) + quantity
|
| 92 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 93 |
+
# Record transaction
|
| 94 |
+
transaction = Transaction(symbol=symbol, quantity=quantity, price=buy_price, timestamp=timestamp, rationale=rationale)
|
| 95 |
+
self.transactions.append(transaction)
|
| 96 |
+
|
| 97 |
+
# Update balance
|
| 98 |
+
self.balance -= total_cost
|
| 99 |
+
self.save()
|
| 100 |
+
write_log(self.name, "account", f"Bought {quantity} of {symbol}")
|
| 101 |
+
return "Completed. Latest details:\n" + self.report()
|
| 102 |
+
|
| 103 |
+
def sell_shares(self, symbol: str, quantity: int, rationale: str) -> str:
|
| 104 |
+
""" Sell shares of a stock if the user has enough shares. """
|
| 105 |
+
if self.holdings.get(symbol, 0) < quantity:
|
| 106 |
+
raise ValueError(f"Cannot sell {quantity} shares of {symbol}. Not enough shares held.")
|
| 107 |
+
|
| 108 |
+
price = get_share_price(symbol)
|
| 109 |
+
sell_price = price * (1 - SPREAD)
|
| 110 |
+
total_proceeds = sell_price * quantity
|
| 111 |
+
|
| 112 |
+
# Update holdings
|
| 113 |
+
self.holdings[symbol] -= quantity
|
| 114 |
+
|
| 115 |
+
# If shares are completely sold, remove from holdings
|
| 116 |
+
if self.holdings[symbol] == 0:
|
| 117 |
+
del self.holdings[symbol]
|
| 118 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 119 |
+
# Record transaction
|
| 120 |
+
transaction = Transaction(symbol=symbol, quantity=-quantity, price=sell_price, timestamp=timestamp, rationale=rationale) # negative quantity for sell
|
| 121 |
+
self.transactions.append(transaction)
|
| 122 |
+
|
| 123 |
+
# Update balance
|
| 124 |
+
self.balance += total_proceeds
|
| 125 |
+
self.save()
|
| 126 |
+
write_log(self.name, "account", f"Sold {quantity} of {symbol}")
|
| 127 |
+
return "Completed. Latest details:\n" + self.report()
|
| 128 |
+
|
| 129 |
+
def calculate_portfolio_value(self):
|
| 130 |
+
""" Calculate the total value of the user's portfolio. """
|
| 131 |
+
total_value = self.balance
|
| 132 |
+
for symbol, quantity in self.holdings.items():
|
| 133 |
+
total_value += get_share_price(symbol) * quantity
|
| 134 |
+
return total_value
|
| 135 |
+
|
| 136 |
+
def calculate_profit_loss(self, portfolio_value: float):
|
| 137 |
+
""" Calculate profit or loss from the initial spend. """
|
| 138 |
+
initial_spend = sum(transaction.total() for transaction in self.transactions)
|
| 139 |
+
return portfolio_value - initial_spend - self.balance
|
| 140 |
+
|
| 141 |
+
def get_holdings(self):
|
| 142 |
+
""" Report the current holdings of the user. """
|
| 143 |
+
return self.holdings
|
| 144 |
+
|
| 145 |
+
def get_profit_loss(self):
|
| 146 |
+
""" Report the user's profit or loss at any point in time. """
|
| 147 |
+
return self.calculate_profit_loss()
|
| 148 |
+
|
| 149 |
+
def list_transactions(self):
|
| 150 |
+
""" List all transactions made by the user. """
|
| 151 |
+
return [transaction.model_dump() for transaction in self.transactions]
|
| 152 |
+
|
| 153 |
+
def report(self) -> str:
|
| 154 |
+
""" Return a json string representing the account. """
|
| 155 |
+
portfolio_value = self.calculate_portfolio_value()
|
| 156 |
+
self.portfolio_value_time_series.append((datetime.now().strftime("%Y-%m-%d %H:%M:%S"), portfolio_value))
|
| 157 |
+
self.save()
|
| 158 |
+
pnl = self.calculate_profit_loss(portfolio_value)
|
| 159 |
+
data = self.model_dump()
|
| 160 |
+
data["total_portfolio_value"] = portfolio_value
|
| 161 |
+
data["total_profit_loss"] = pnl
|
| 162 |
+
write_log(self.name, "account", f"Retrieved account details")
|
| 163 |
+
return json.dumps(data)
|
| 164 |
+
|
| 165 |
+
def get_strategy(self) -> str:
|
| 166 |
+
""" Return the strategy of the account """
|
| 167 |
+
write_log(self.name, "account", f"Retrieved strategy")
|
| 168 |
+
return self.strategy
|
| 169 |
+
|
| 170 |
+
def change_strategy(self, strategy: str) -> str:
|
| 171 |
+
""" At your discretion, if you choose to, call this to change your investment strategy for the future """
|
| 172 |
+
self.strategy = strategy
|
| 173 |
+
self.save()
|
| 174 |
+
write_log(self.name, "account", f"Changed strategy")
|
| 175 |
+
return "Changed strategy"
|
| 176 |
+
|
| 177 |
+
# Example of usage:
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
account = Account("John Doe")
|
| 180 |
+
account.deposit(1000)
|
| 181 |
+
account.buy_shares("AAPL", 5)
|
| 182 |
+
account.sell_shares("AAPL", 2)
|
| 183 |
+
print(f"Current Holdings: {account.get_holdings()}")
|
| 184 |
+
print(f"Total Portfolio Value: {account.calculate_portfolio_value()}")
|
| 185 |
+
print(f"Profit/Loss: {account.get_profit_loss()}")
|
| 186 |
+
print(f"Transactions: {account.list_transactions()}")
|
accounts_client.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mcp
|
| 2 |
+
from mcp.client.stdio import stdio_client
|
| 3 |
+
from mcp import StdioServerParameters
|
| 4 |
+
from agents import FunctionTool
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
params = StdioServerParameters(command="uv", args=["run", "accounts_server.py"], env=None)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
async def list_accounts_tools():
|
| 11 |
+
async with stdio_client(params) as streams:
|
| 12 |
+
async with mcp.ClientSession(*streams) as session:
|
| 13 |
+
await session.initialize()
|
| 14 |
+
tools_result = await session.list_tools()
|
| 15 |
+
return tools_result.tools
|
| 16 |
+
|
| 17 |
+
async def call_accounts_tool(tool_name, tool_args):
|
| 18 |
+
async with stdio_client(params) as streams:
|
| 19 |
+
async with mcp.ClientSession(*streams) as session:
|
| 20 |
+
await session.initialize()
|
| 21 |
+
result = await session.call_tool(tool_name, tool_args)
|
| 22 |
+
return result
|
| 23 |
+
|
| 24 |
+
async def read_accounts_resource(name):
|
| 25 |
+
async with stdio_client(params) as streams:
|
| 26 |
+
async with mcp.ClientSession(*streams) as session:
|
| 27 |
+
await session.initialize()
|
| 28 |
+
result = await session.read_resource(f"accounts://accounts_server/{name}")
|
| 29 |
+
return result.contents[0].text
|
| 30 |
+
|
| 31 |
+
async def read_strategy_resource(name):
|
| 32 |
+
async with stdio_client(params) as streams:
|
| 33 |
+
async with mcp.ClientSession(*streams) as session:
|
| 34 |
+
await session.initialize()
|
| 35 |
+
result = await session.read_resource(f"accounts://strategy/{name}")
|
| 36 |
+
return result.contents[0].text
|
| 37 |
+
|
| 38 |
+
async def get_accounts_tools_openai():
|
| 39 |
+
openai_tools = []
|
| 40 |
+
for tool in await list_accounts_tools():
|
| 41 |
+
schema = {**tool.inputSchema, "additionalProperties": False}
|
| 42 |
+
openai_tool = FunctionTool(
|
| 43 |
+
name=tool.name,
|
| 44 |
+
description=tool.description,
|
| 45 |
+
params_json_schema=schema,
|
| 46 |
+
on_invoke_tool=lambda ctx, args, toolname=tool.name: call_accounts_tool(toolname, json.loads(args))
|
| 47 |
+
|
| 48 |
+
)
|
| 49 |
+
openai_tools.append(openai_tool)
|
| 50 |
+
return openai_tools
|
accounts_server.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mcp.server.fastmcp import FastMCP
|
| 2 |
+
from accounts import Account
|
| 3 |
+
|
| 4 |
+
mcp = FastMCP("accounts_server")
|
| 5 |
+
|
| 6 |
+
@mcp.tool()
|
| 7 |
+
async def get_balance(name: str) -> float:
|
| 8 |
+
"""Get the cash balance of the given account name.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
name: The name of the account holder
|
| 12 |
+
"""
|
| 13 |
+
return Account.get(name).balance
|
| 14 |
+
|
| 15 |
+
@mcp.tool()
|
| 16 |
+
async def get_holdings(name: str) -> dict[str, int]:
|
| 17 |
+
"""Get the holdings of the given account name.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
name: The name of the account holder
|
| 21 |
+
"""
|
| 22 |
+
return Account.get(name).holdings
|
| 23 |
+
|
| 24 |
+
@mcp.tool()
|
| 25 |
+
async def buy_shares(name: str, symbol: str, quantity: int, rationale: str) -> float:
|
| 26 |
+
"""Buy shares of a stock.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
name: The name of the account holder
|
| 30 |
+
symbol: The symbol of the stock
|
| 31 |
+
quantity: The quantity of shares to buy
|
| 32 |
+
rationale: The rationale for the purchase and fit with the account's strategy
|
| 33 |
+
"""
|
| 34 |
+
return Account.get(name).buy_shares(symbol, quantity, rationale)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@mcp.tool()
|
| 38 |
+
async def sell_shares(name: str, symbol: str, quantity: int, rationale: str) -> float:
|
| 39 |
+
"""Sell shares of a stock.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
name: The name of the account holder
|
| 43 |
+
symbol: The symbol of the stock
|
| 44 |
+
quantity: The quantity of shares to sell
|
| 45 |
+
rationale: The rationale for the sale and fit with the account's strategy
|
| 46 |
+
"""
|
| 47 |
+
return Account.get(name).sell_shares(symbol, quantity, rationale)
|
| 48 |
+
|
| 49 |
+
@mcp.tool()
|
| 50 |
+
async def change_strategy(name: str, strategy: str) -> str:
|
| 51 |
+
"""At your discretion, if you choose to, call this to change your investment strategy for the future.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
name: The name of the account holder
|
| 55 |
+
strategy: The new strategy for the account
|
| 56 |
+
"""
|
| 57 |
+
return Account.get(name).change_strategy(strategy)
|
| 58 |
+
|
| 59 |
+
@mcp.resource("accounts://accounts_server/{name}")
|
| 60 |
+
async def read_account_resource(name: str) -> str:
|
| 61 |
+
account = Account.get(name.lower())
|
| 62 |
+
return account.report()
|
| 63 |
+
|
| 64 |
+
@mcp.resource("accounts://strategy/{name}")
|
| 65 |
+
async def read_strategy_resource(name: str) -> str:
|
| 66 |
+
account = Account.get(name.lower())
|
| 67 |
+
return account.get_strategy()
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
mcp.run(transport='stdio')
|
app.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from util import css, js, Color
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from trading_floor import names, lastnames, short_model_names
|
| 5 |
+
import plotly.express as px
|
| 6 |
+
from accounts import Account
|
| 7 |
+
from database import read_log
|
| 8 |
+
|
| 9 |
+
mapper = {
|
| 10 |
+
"trace": Color.WHITE,
|
| 11 |
+
"agent": Color.CYAN,
|
| 12 |
+
"function": Color.GREEN,
|
| 13 |
+
"generation": Color.YELLOW,
|
| 14 |
+
"response": Color.MAGENTA,
|
| 15 |
+
"account": Color.RED,
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Trader:
|
| 20 |
+
def __init__(self, name: str, lastname: str, model_name: str):
|
| 21 |
+
self.name = name
|
| 22 |
+
self.lastname = lastname
|
| 23 |
+
self.model_name = model_name
|
| 24 |
+
self.account = Account.get(name)
|
| 25 |
+
|
| 26 |
+
def reload(self):
|
| 27 |
+
self.account = Account.get(self.name)
|
| 28 |
+
|
| 29 |
+
def get_title(self) -> str:
|
| 30 |
+
return f"<div style='text-align: center;font-size:34px;'>{self.name}<span style='color:#ccc;font-size:24px;'> ({self.model_name}) - {self.lastname}</span></div>"
|
| 31 |
+
|
| 32 |
+
def get_strategy(self) -> str:
|
| 33 |
+
return self.account.get_strategy()
|
| 34 |
+
|
| 35 |
+
def get_portfolio_value_df(self) -> pd.DataFrame:
|
| 36 |
+
df = pd.DataFrame(self.account.portfolio_value_time_series, columns=["datetime", "value"])
|
| 37 |
+
df["datetime"] = pd.to_datetime(df["datetime"])
|
| 38 |
+
return df
|
| 39 |
+
|
| 40 |
+
def get_portfolio_value_chart(self):
|
| 41 |
+
df = self.get_portfolio_value_df()
|
| 42 |
+
fig = px.line(df, x="datetime", y="value")
|
| 43 |
+
margin = dict(l=40, r=20, t=20, b=40)
|
| 44 |
+
fig.update_layout(
|
| 45 |
+
height=300,
|
| 46 |
+
margin=margin,
|
| 47 |
+
xaxis_title=None,
|
| 48 |
+
yaxis_title=None,
|
| 49 |
+
paper_bgcolor="#bbb",
|
| 50 |
+
plot_bgcolor="#dde",
|
| 51 |
+
)
|
| 52 |
+
fig.update_xaxes(tickformat="%m/%d", tickangle=45, tickfont=dict(size=8))
|
| 53 |
+
fig.update_yaxes(tickfont=dict(size=8), tickformat=",.0f")
|
| 54 |
+
return fig
|
| 55 |
+
|
| 56 |
+
def get_holdings_df(self) -> pd.DataFrame:
|
| 57 |
+
"""Convert holdings to DataFrame for display"""
|
| 58 |
+
holdings = self.account.get_holdings()
|
| 59 |
+
if not holdings:
|
| 60 |
+
return pd.DataFrame(columns=["Symbol", "Quantity"])
|
| 61 |
+
|
| 62 |
+
df = pd.DataFrame(
|
| 63 |
+
[{"Symbol": symbol, "Quantity": quantity} for symbol, quantity in holdings.items()]
|
| 64 |
+
)
|
| 65 |
+
return df
|
| 66 |
+
|
| 67 |
+
def get_transactions_df(self) -> pd.DataFrame:
|
| 68 |
+
"""Convert transactions to DataFrame for display"""
|
| 69 |
+
transactions = self.account.list_transactions()
|
| 70 |
+
if not transactions:
|
| 71 |
+
return pd.DataFrame(columns=["Timestamp", "Symbol", "Quantity", "Price", "Rationale"])
|
| 72 |
+
|
| 73 |
+
return pd.DataFrame(transactions)
|
| 74 |
+
|
| 75 |
+
def get_portfolio_value(self) -> str:
|
| 76 |
+
"""Calculate total portfolio value based on current prices"""
|
| 77 |
+
portfolio_value = self.account.calculate_portfolio_value() or 0.0
|
| 78 |
+
pnl = self.account.calculate_profit_loss(portfolio_value) or 0.0
|
| 79 |
+
color = "green" if pnl >= 0 else "red"
|
| 80 |
+
emoji = "⬆" if pnl >= 0 else "⬇"
|
| 81 |
+
return f"<div style='text-align: center;background-color:{color};'><span style='font-size:32px'>${portfolio_value:,.0f}</span><span style='font-size:24px'> {emoji} ${pnl:,.0f}</span></div>"
|
| 82 |
+
|
| 83 |
+
def get_logs(self, previous=None) -> str:
|
| 84 |
+
logs = read_log(self.name, last_n=13)
|
| 85 |
+
response = ""
|
| 86 |
+
for log in logs:
|
| 87 |
+
timestamp, type, message = log
|
| 88 |
+
color = mapper.get(type, Color.WHITE).value
|
| 89 |
+
response += f"<span style='color:{color}'>{timestamp} : [{type}] {message}</span><br/>"
|
| 90 |
+
response = f"<div style='height:250px; overflow-y:auto;'>{response}</div>"
|
| 91 |
+
if response != previous:
|
| 92 |
+
return response
|
| 93 |
+
return gr.update()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class TraderView:
|
| 97 |
+
def __init__(self, trader: Trader):
|
| 98 |
+
self.trader = trader
|
| 99 |
+
self.portfolio_value = None
|
| 100 |
+
self.chart = None
|
| 101 |
+
self.holdings_table = None
|
| 102 |
+
self.transactions_table = None
|
| 103 |
+
|
| 104 |
+
def make_ui(self):
|
| 105 |
+
with gr.Column():
|
| 106 |
+
gr.HTML(self.trader.get_title())
|
| 107 |
+
with gr.Row():
|
| 108 |
+
self.portfolio_value = gr.HTML(self.trader.get_portfolio_value)
|
| 109 |
+
with gr.Row():
|
| 110 |
+
self.chart = gr.Plot(
|
| 111 |
+
self.trader.get_portfolio_value_chart, container=True, show_label=False
|
| 112 |
+
)
|
| 113 |
+
with gr.Row(variant="panel"):
|
| 114 |
+
self.log = gr.HTML(self.trader.get_logs)
|
| 115 |
+
with gr.Row():
|
| 116 |
+
self.holdings_table = gr.Dataframe(
|
| 117 |
+
value=self.trader.get_holdings_df,
|
| 118 |
+
label="Holdings",
|
| 119 |
+
headers=["Symbol", "Quantity"],
|
| 120 |
+
row_count=(5, "dynamic"),
|
| 121 |
+
col_count=2,
|
| 122 |
+
max_height=300,
|
| 123 |
+
elem_classes=["dataframe-fix-small"],
|
| 124 |
+
)
|
| 125 |
+
with gr.Row():
|
| 126 |
+
self.transactions_table = gr.Dataframe(
|
| 127 |
+
value=self.trader.get_transactions_df,
|
| 128 |
+
label="Recent Transactions",
|
| 129 |
+
headers=["Timestamp", "Symbol", "Quantity", "Price", "Rationale"],
|
| 130 |
+
row_count=(5, "dynamic"),
|
| 131 |
+
col_count=5,
|
| 132 |
+
max_height=300,
|
| 133 |
+
elem_classes=["dataframe-fix"],
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
timer = gr.Timer(value=120)
|
| 137 |
+
timer.tick(
|
| 138 |
+
fn=self.refresh,
|
| 139 |
+
inputs=[],
|
| 140 |
+
outputs=[
|
| 141 |
+
self.portfolio_value,
|
| 142 |
+
self.chart,
|
| 143 |
+
self.holdings_table,
|
| 144 |
+
self.transactions_table,
|
| 145 |
+
],
|
| 146 |
+
show_progress="hidden",
|
| 147 |
+
queue=False,
|
| 148 |
+
)
|
| 149 |
+
log_timer = gr.Timer(value=0.5)
|
| 150 |
+
log_timer.tick(
|
| 151 |
+
fn=self.trader.get_logs,
|
| 152 |
+
inputs=[self.log],
|
| 153 |
+
outputs=[self.log],
|
| 154 |
+
show_progress="hidden",
|
| 155 |
+
queue=False,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
def refresh(self):
|
| 159 |
+
self.trader.reload()
|
| 160 |
+
return (
|
| 161 |
+
self.trader.get_portfolio_value(),
|
| 162 |
+
self.trader.get_portfolio_value_chart(),
|
| 163 |
+
self.trader.get_holdings_df(),
|
| 164 |
+
self.trader.get_transactions_df(),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# Main UI construction
|
| 169 |
+
def create_ui():
|
| 170 |
+
"""Create the main Gradio UI for the trading simulation"""
|
| 171 |
+
|
| 172 |
+
traders = [
|
| 173 |
+
Trader(trader_name, lastname, model_name)
|
| 174 |
+
for trader_name, lastname, model_name in zip(names, lastnames, short_model_names)
|
| 175 |
+
]
|
| 176 |
+
trader_views = [TraderView(trader) for trader in traders]
|
| 177 |
+
|
| 178 |
+
with gr.Blocks(
|
| 179 |
+
title="Traders", css=css, js=js, theme=gr.themes.Default(primary_hue="sky"), fill_width=True
|
| 180 |
+
) as ui:
|
| 181 |
+
with gr.Row():
|
| 182 |
+
for trader_view in trader_views:
|
| 183 |
+
trader_view.make_ui()
|
| 184 |
+
|
| 185 |
+
return ui
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
if __name__ == "__main__":
|
| 189 |
+
ui = create_ui()
|
| 190 |
+
ui.launch(inbrowser=True)
|
database.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
load_dotenv(override=True)
|
| 7 |
+
|
| 8 |
+
DB = "accounts.db"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
with sqlite3.connect(DB) as conn:
|
| 12 |
+
cursor = conn.cursor()
|
| 13 |
+
cursor.execute('CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, account TEXT)')
|
| 14 |
+
cursor.execute('''
|
| 15 |
+
CREATE TABLE IF NOT EXISTS logs (
|
| 16 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 17 |
+
name TEXT,
|
| 18 |
+
datetime DATETIME,
|
| 19 |
+
type TEXT,
|
| 20 |
+
message TEXT
|
| 21 |
+
)
|
| 22 |
+
''')
|
| 23 |
+
cursor.execute('CREATE TABLE IF NOT EXISTS market (date TEXT PRIMARY KEY, data TEXT)')
|
| 24 |
+
conn.commit()
|
| 25 |
+
|
| 26 |
+
def write_account(name, account_dict):
|
| 27 |
+
json_data = json.dumps(account_dict)
|
| 28 |
+
with sqlite3.connect(DB) as conn:
|
| 29 |
+
cursor = conn.cursor()
|
| 30 |
+
cursor.execute('''
|
| 31 |
+
INSERT INTO accounts (name, account)
|
| 32 |
+
VALUES (?, ?)
|
| 33 |
+
ON CONFLICT(name) DO UPDATE SET account=excluded.account
|
| 34 |
+
''', (name.lower(), json_data))
|
| 35 |
+
conn.commit()
|
| 36 |
+
|
| 37 |
+
def read_account(name):
|
| 38 |
+
with sqlite3.connect(DB) as conn:
|
| 39 |
+
cursor = conn.cursor()
|
| 40 |
+
cursor.execute('SELECT account FROM accounts WHERE name = ?', (name.lower(),))
|
| 41 |
+
row = cursor.fetchone()
|
| 42 |
+
return json.loads(row[0]) if row else None
|
| 43 |
+
|
| 44 |
+
def write_log(name: str, type: str, message: str):
|
| 45 |
+
"""
|
| 46 |
+
Write a log entry to the logs table.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
name (str): The name associated with the log
|
| 50 |
+
type (str): The type of log entry
|
| 51 |
+
message (str): The log message
|
| 52 |
+
"""
|
| 53 |
+
now = datetime.now().isoformat()
|
| 54 |
+
|
| 55 |
+
with sqlite3.connect(DB) as conn:
|
| 56 |
+
cursor = conn.cursor()
|
| 57 |
+
cursor.execute('''
|
| 58 |
+
INSERT INTO logs (name, datetime, type, message)
|
| 59 |
+
VALUES (?, datetime('now'), ?, ?)
|
| 60 |
+
''', (name.lower(), type, message))
|
| 61 |
+
conn.commit()
|
| 62 |
+
|
| 63 |
+
def read_log(name: str, last_n=10):
|
| 64 |
+
"""
|
| 65 |
+
Read the most recent log entries for a given name.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
name (str): The name to retrieve logs for
|
| 69 |
+
last_n (int): Number of most recent entries to retrieve
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
list: A list of tuples containing (datetime, type, message)
|
| 73 |
+
"""
|
| 74 |
+
with sqlite3.connect(DB) as conn:
|
| 75 |
+
cursor = conn.cursor()
|
| 76 |
+
cursor.execute('''
|
| 77 |
+
SELECT datetime, type, message FROM logs
|
| 78 |
+
WHERE name = ?
|
| 79 |
+
ORDER BY datetime DESC
|
| 80 |
+
LIMIT ?
|
| 81 |
+
''', (name.lower(), last_n))
|
| 82 |
+
|
| 83 |
+
return reversed(cursor.fetchall())
|
| 84 |
+
|
| 85 |
+
def write_market(date: str, data: dict) -> None:
|
| 86 |
+
data_json = json.dumps(data)
|
| 87 |
+
with sqlite3.connect(DB) as conn:
|
| 88 |
+
cursor = conn.cursor()
|
| 89 |
+
cursor.execute('''
|
| 90 |
+
INSERT INTO market (date, data)
|
| 91 |
+
VALUES (?, ?)
|
| 92 |
+
ON CONFLICT(date) DO UPDATE SET data=excluded.data
|
| 93 |
+
''', (date, data_json))
|
| 94 |
+
conn.commit()
|
| 95 |
+
|
| 96 |
+
def read_market(date: str) -> dict | None:
|
| 97 |
+
with sqlite3.connect(DB) as conn:
|
| 98 |
+
cursor = conn.cursor()
|
| 99 |
+
cursor.execute('SELECT data FROM market WHERE date = ?', (date,))
|
| 100 |
+
row = cursor.fetchone()
|
| 101 |
+
return json.loads(row[0]) if row else None
|
market.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from polygon import RESTClient
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import random
|
| 6 |
+
from database import write_market, read_market
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
from datetime import timezone
|
| 9 |
+
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
|
| 12 |
+
polygon_api_key = os.getenv("POLYGON_API_KEY")
|
| 13 |
+
polygon_plan = os.getenv("POLYGON_PLAN")
|
| 14 |
+
|
| 15 |
+
is_paid_polygon = polygon_plan == "paid"
|
| 16 |
+
is_realtime_polygon = polygon_plan == "realtime"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def is_market_open() -> bool:
|
| 20 |
+
client = RESTClient(polygon_api_key)
|
| 21 |
+
market_status = client.get_market_status()
|
| 22 |
+
return market_status.market == "open"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_all_share_prices_polygon_eod() -> dict[str, float]:
|
| 26 |
+
"""With much thanks to student Reema R. for fixing the timezone issue with this!"""
|
| 27 |
+
client = RESTClient(polygon_api_key)
|
| 28 |
+
|
| 29 |
+
probe = client.get_previous_close_agg("SPY")[0]
|
| 30 |
+
last_close = datetime.fromtimestamp(probe.timestamp / 1000, tz=timezone.utc).date()
|
| 31 |
+
|
| 32 |
+
results = client.get_grouped_daily_aggs(last_close, adjusted=True, include_otc=False)
|
| 33 |
+
return {result.ticker: result.close for result in results}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@lru_cache(maxsize=2)
|
| 37 |
+
def get_market_for_prior_date(today):
|
| 38 |
+
market_data = read_market(today)
|
| 39 |
+
if not market_data:
|
| 40 |
+
market_data = get_all_share_prices_polygon_eod()
|
| 41 |
+
write_market(today, market_data)
|
| 42 |
+
return market_data
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_share_price_polygon_eod(symbol) -> float:
|
| 46 |
+
today = datetime.now().date().strftime("%Y-%m-%d")
|
| 47 |
+
market_data = get_market_for_prior_date(today)
|
| 48 |
+
return market_data.get(symbol, 0.0)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_share_price_polygon_min(symbol) -> float:
|
| 52 |
+
client = RESTClient(polygon_api_key)
|
| 53 |
+
result = client.get_snapshot_ticker("stocks", symbol)
|
| 54 |
+
return result.min.close or result.prev_day.close
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_share_price_polygon(symbol) -> float:
|
| 58 |
+
if is_paid_polygon:
|
| 59 |
+
return get_share_price_polygon_min(symbol)
|
| 60 |
+
else:
|
| 61 |
+
return get_share_price_polygon_eod(symbol)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def get_share_price(symbol) -> float:
|
| 65 |
+
if polygon_api_key:
|
| 66 |
+
try:
|
| 67 |
+
return get_share_price_polygon(symbol)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Was not able to use the polygon API due to {e}; using a random number")
|
| 70 |
+
return float(random.randint(1, 100))
|
market_server.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from mcp.server.fastmcp import FastMCP
|
| 2 |
+
from market import get_share_price
|
| 3 |
+
|
| 4 |
+
mcp = FastMCP("market_server")
|
| 5 |
+
|
| 6 |
+
@mcp.tool()
|
| 7 |
+
async def lookup_share_price(symbol: str) -> float:
|
| 8 |
+
"""This tool provides the current price of the given stock symbol.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
symbol: the symbol of the stock
|
| 12 |
+
"""
|
| 13 |
+
return get_share_price(symbol)
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
mcp.run(transport='stdio')
|
mcp_params.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from market import is_paid_polygon, is_realtime_polygon
|
| 4 |
+
|
| 5 |
+
load_dotenv(override=True)
|
| 6 |
+
|
| 7 |
+
brave_env = {"BRAVE_API_KEY": os.getenv("BRAVE_API_KEY")}
|
| 8 |
+
polygon_api_key = os.getenv("POLYGON_API_KEY")
|
| 9 |
+
|
| 10 |
+
# The MCP server for the Trader to read Market Data
|
| 11 |
+
|
| 12 |
+
if is_paid_polygon or is_realtime_polygon:
|
| 13 |
+
market_mcp = {
|
| 14 |
+
"command": "uvx",
|
| 15 |
+
"args": ["--from", "git+https://github.com/polygon-io/mcp_polygon@v0.1.0", "mcp_polygon"],
|
| 16 |
+
"env": {"POLYGON_API_KEY": polygon_api_key},
|
| 17 |
+
}
|
| 18 |
+
else:
|
| 19 |
+
market_mcp = {"command": "uv", "args": ["run", "market_server.py"]}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# The full set of MCP servers for the trader: Accounts, Push Notification and the Market
|
| 23 |
+
|
| 24 |
+
trader_mcp_server_params = [
|
| 25 |
+
{"command": "uv", "args": ["run", "accounts_server.py"]},
|
| 26 |
+
{"command": "uv", "args": ["run", "push_server.py"]},
|
| 27 |
+
market_mcp,
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
# The full set of MCP servers for the researcher: Fetch, Brave Search and Memory
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def researcher_mcp_server_params(name: str):
|
| 34 |
+
return [
|
| 35 |
+
{"command": "uvx", "args": ["mcp-server-fetch"]},
|
| 36 |
+
{
|
| 37 |
+
"command": "npx",
|
| 38 |
+
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
| 39 |
+
"env": brave_env,
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"command": "npx",
|
| 43 |
+
"args": ["-y", "mcp-memory-libsql"],
|
| 44 |
+
"env": {"LIBSQL_URL": f"file:./memory/{name}.db"},
|
| 45 |
+
},
|
| 46 |
+
]
|
memory/Cathie.db
ADDED
|
Binary file (57.3 kB). View file
|
|
|
memory/George.db
ADDED
|
Binary file (57.3 kB). View file
|
|
|
memory/Ray.db
ADDED
|
Binary file (57.3 kB). View file
|
|
|
memory/Warren.db
ADDED
|
Binary file (57.3 kB). View file
|
|
|
memory/ed.db
ADDED
|
Binary file (57.3 kB). View file
|
|
|
memory/memory.txt
ADDED
|
File without changes
|
push_server.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import requests
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from mcp.server.fastmcp import FastMCP
|
| 6 |
+
|
| 7 |
+
load_dotenv(override=True)
|
| 8 |
+
|
| 9 |
+
pushover_user = os.getenv("PUSHOVER_USER")
|
| 10 |
+
pushover_token = os.getenv("PUSHOVER_TOKEN")
|
| 11 |
+
pushover_url = "https://api.pushover.net/1/messages.json"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
mcp = FastMCP("push_server")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PushModelArgs(BaseModel):
|
| 18 |
+
message: str = Field(description="A brief message to push")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@mcp.tool()
|
| 22 |
+
def push(args: PushModelArgs):
|
| 23 |
+
"""Send a push notification with this brief message"""
|
| 24 |
+
print(f"Push: {args.message}")
|
| 25 |
+
payload = {"user": pushover_user, "token": pushover_token, "message": args.message}
|
| 26 |
+
requests.post(pushover_url, data=payload)
|
| 27 |
+
return "Push notification sent"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
if __name__ == "__main__":
|
| 31 |
+
mcp.run(transport="stdio")
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai
|
| 2 |
+
openai-agents
|
| 3 |
+
sendgrid
|
| 4 |
+
pydantic
|
| 5 |
+
requests
|
| 6 |
+
playwright
|
| 7 |
+
dotenv
|
| 8 |
+
python-dotenv
|
| 9 |
+
gradio
|
| 10 |
+
polygon
|
reset.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from accounts import Account
|
| 2 |
+
|
| 3 |
+
waren_strategy = """
|
| 4 |
+
You are Warren, and you are named in homage to your role model, Warren Buffett.
|
| 5 |
+
You are a value-oriented investor who prioritizes long-term wealth creation.
|
| 6 |
+
You identify high-quality companies trading below their intrinsic value.
|
| 7 |
+
You invest patiently and hold positions through market fluctuations,
|
| 8 |
+
relying on meticulous fundamental analysis, steady cash flows, strong management teams,
|
| 9 |
+
and competitive advantages. You rarely react to short-term market movements,
|
| 10 |
+
trusting your deep research and value-driven strategy.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
george_strategy = """
|
| 14 |
+
You are George, and you are named in homage to your role model, George Soros.
|
| 15 |
+
You are an aggressive macro trader who actively seeks significant market
|
| 16 |
+
mispricings. You look for large-scale economic and
|
| 17 |
+
geopolitical events that create investment opportunities. Your approach is contrarian,
|
| 18 |
+
willing to bet boldly against prevailing market sentiment when your macroeconomic analysis
|
| 19 |
+
suggests a significant imbalance. You leverage careful timing and decisive action to
|
| 20 |
+
capitalize on rapid market shifts.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
ray_strategy = """
|
| 24 |
+
You are Ray, and you are named in homage to your role model, Ray Dalio.
|
| 25 |
+
You apply a systematic, principles-based approach rooted in macroeconomic insights and diversification.
|
| 26 |
+
You invest broadly across asset classes, utilizing risk parity strategies to achieve balanced returns
|
| 27 |
+
in varying market environments. You pay close attention to macroeconomic indicators, central bank policies,
|
| 28 |
+
and economic cycles, adjusting your portfolio strategically to manage risk and preserve capital across diverse market conditions.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
cathie_strategy = """
|
| 32 |
+
You are Cathie, and you are named in homage to your role model, Cathie Wood.
|
| 33 |
+
You aggressively pursue opportunities in disruptive innovation, particularly focusing on Crypto ETFs.
|
| 34 |
+
Your strategy is to identify and invest boldly in sectors poised to revolutionize the economy,
|
| 35 |
+
accepting higher volatility for potentially exceptional returns. You closely monitor technological breakthroughs,
|
| 36 |
+
regulatory changes, and market sentiment in crypto ETFs, ready to take bold positions
|
| 37 |
+
and actively manage your portfolio to capitalize on rapid growth trends.
|
| 38 |
+
You focus your trading on crypto ETFs.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def reset_traders():
|
| 43 |
+
Account.get("Warren").reset(waren_strategy)
|
| 44 |
+
Account.get("George").reset(george_strategy)
|
| 45 |
+
Account.get("Ray").reset(ray_strategy)
|
| 46 |
+
Account.get("Cathie").reset(cathie_strategy)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
reset_traders()
|
templates.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from market import is_paid_polygon, is_realtime_polygon
|
| 3 |
+
|
| 4 |
+
if is_realtime_polygon:
|
| 5 |
+
note = "You have access to realtime market data tools; use your get_last_trade tool for the latest trade price. You can also use tools for share information, trends and technical indicators and fundamentals."
|
| 6 |
+
elif is_paid_polygon:
|
| 7 |
+
note = "You have access to market data tools but without access to the trade or quote tools; use your get_snapshot_ticker tool to get the latest share price on a 15 min delay. You can also use tools for share information, trends and technical indicators and fundamentals."
|
| 8 |
+
else:
|
| 9 |
+
note = "You have access to end of day market data; use you get_share_price tool to get the share price as of the prior close."
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def researcher_instructions():
|
| 13 |
+
return f"""You are a financial researcher. You are able to search the web for interesting financial news,
|
| 14 |
+
look for possible trading opportunities, and help with research.
|
| 15 |
+
Based on the request, you carry out necessary research and respond with your findings.
|
| 16 |
+
Take time to make multiple searches to get a comprehensive overview, and then summarize your findings.
|
| 17 |
+
If the web search tool raises an error due to rate limits, then use your other tool that fetches web pages instead.
|
| 18 |
+
|
| 19 |
+
Important: making use of your knowledge graph to retrieve and store information on companies, websites and market conditions:
|
| 20 |
+
|
| 21 |
+
Make use of your knowledge graph tools to store and recall entity information; use it to retrieve information that
|
| 22 |
+
you have worked on previously, and store new information about companies, stocks and market conditions.
|
| 23 |
+
Also use it to store web addresses that you find interesting so you can check them later.
|
| 24 |
+
Draw on your knowledge graph to build your expertise over time.
|
| 25 |
+
|
| 26 |
+
If there isn't a specific request, then just respond with investment opportunities based on searching latest news.
|
| 27 |
+
The current datetime is {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def research_tool():
|
| 31 |
+
return "This tool researches online for news and opportunities, \
|
| 32 |
+
either based on your specific request to look into a certain stock, \
|
| 33 |
+
or generally for notable financial news and opportunities. \
|
| 34 |
+
Describe what kind of research you're looking for."
|
| 35 |
+
|
| 36 |
+
def trader_instructions(name: str):
|
| 37 |
+
return f"""
|
| 38 |
+
You are {name}, a trader on the stock market. Your account is under your name, {name}.
|
| 39 |
+
You actively manage your portfolio according to your strategy.
|
| 40 |
+
You have access to tools including a researcher to research online for news and opportunities, based on your request.
|
| 41 |
+
You also have tools to access to financial data for stocks. {note}
|
| 42 |
+
And you have tools to buy and sell stocks using your account name {name}.
|
| 43 |
+
You can use your entity tools as a persistent memory to store and recall information; you share
|
| 44 |
+
this memory with other traders and can benefit from the group's knowledge.
|
| 45 |
+
Use these tools to carry out research, make decisions, and execute trades.
|
| 46 |
+
After you've completed trading, send a push notification with a brief summary of activity, then reply with a 2-3 sentence appraisal.
|
| 47 |
+
Your goal is to maximize your profits according to your strategy.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def trade_message(name, strategy, account):
|
| 51 |
+
return f"""Based on your investment strategy, you should now look for new opportunities.
|
| 52 |
+
Use the research tool to find news and opportunities consistent with your strategy.
|
| 53 |
+
Do not use the 'get company news' tool; use the research tool instead.
|
| 54 |
+
Use the tools to research stock price and other company information. {note}
|
| 55 |
+
Finally, make you decision, then execute trades using the tools.
|
| 56 |
+
Your tools only allow you to trade equities, but you are able to use ETFs to take positions in other markets.
|
| 57 |
+
You do not need to rebalance your portfolio; you will be asked to do so later.
|
| 58 |
+
Just make trades based on your strategy as needed.
|
| 59 |
+
Your investment strategy:
|
| 60 |
+
{strategy}
|
| 61 |
+
Here is your current account:
|
| 62 |
+
{account}
|
| 63 |
+
Here is the current datetime:
|
| 64 |
+
{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 65 |
+
Now, carry out analysis, make your decision and execute trades. Your account name is {name}.
|
| 66 |
+
After you've executed your trades, send a push notification with a brief sumnmary of trades and the health of the portfolio, then
|
| 67 |
+
respond with a brief 2-3 sentence appraisal of your portfolio and its outlook.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def rebalance_message(name, strategy, account):
|
| 71 |
+
return f"""Based on your investment strategy, you should now examine your portfolio and decide if you need to rebalance.
|
| 72 |
+
Use the research tool to find news and opportunities affecting your existing portfolio.
|
| 73 |
+
Use the tools to research stock price and other company information affecting your existing portfolio. {note}
|
| 74 |
+
Finally, make you decision, then execute trades using the tools as needed.
|
| 75 |
+
You do not need to identify new investment opportunities at this time; you will be asked to do so later.
|
| 76 |
+
Just rebalance your portfolio based on your strategy as needed.
|
| 77 |
+
Your investment strategy:
|
| 78 |
+
{strategy}
|
| 79 |
+
You also have a tool to change your strategy if you wish; you can decide at any time that you would like to evolve or even switch your strategy.
|
| 80 |
+
Here is your current account:
|
| 81 |
+
{account}
|
| 82 |
+
Here is the current datetime:
|
| 83 |
+
{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 84 |
+
Now, carry out analysis, make your decision and execute trades. Your account name is {name}.
|
| 85 |
+
After you've executed your trades, send a push notification with a brief sumnmary of trades and the health of the portfolio, then
|
| 86 |
+
respond with a brief 2-3 sentence appraisal of your portfolio and its outlook."""
|
tracers.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agents import TracingProcessor, Trace, Span
|
| 2 |
+
from database import write_log
|
| 3 |
+
import secrets
|
| 4 |
+
import string
|
| 5 |
+
|
| 6 |
+
ALPHANUM = string.ascii_lowercase + string.digits
|
| 7 |
+
|
| 8 |
+
def make_trace_id(tag: str) -> str:
|
| 9 |
+
"""
|
| 10 |
+
Return a string of the form 'trace_<tag><random>',
|
| 11 |
+
where the total length after 'trace_' is 32 chars.
|
| 12 |
+
"""
|
| 13 |
+
tag += "0"
|
| 14 |
+
pad_len = 32 - len(tag)
|
| 15 |
+
random_suffix = ''.join(secrets.choice(ALPHANUM) for _ in range(pad_len))
|
| 16 |
+
return f"trace_{tag}{random_suffix}"
|
| 17 |
+
|
| 18 |
+
class LogTracer(TracingProcessor):
|
| 19 |
+
|
| 20 |
+
def get_name(self, trace_or_span: Trace | Span) -> str | None:
|
| 21 |
+
trace_id = trace_or_span.trace_id
|
| 22 |
+
name = trace_id.split("_")[1]
|
| 23 |
+
if '0' in name:
|
| 24 |
+
return name.split("0")[0]
|
| 25 |
+
else:
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
def on_trace_start(self, trace) -> None:
|
| 29 |
+
name = self.get_name(trace)
|
| 30 |
+
if name:
|
| 31 |
+
write_log(name, "trace", f"Started: {trace.name}")
|
| 32 |
+
|
| 33 |
+
def on_trace_end(self, trace) -> None:
|
| 34 |
+
name = self.get_name(trace)
|
| 35 |
+
if name:
|
| 36 |
+
write_log(name, "trace", f"Ended: {trace.name}")
|
| 37 |
+
|
| 38 |
+
def on_span_start(self, span) -> None:
|
| 39 |
+
name = self.get_name(span)
|
| 40 |
+
type = span.span_data.type if span.span_data else "span"
|
| 41 |
+
if name:
|
| 42 |
+
message = "Started"
|
| 43 |
+
if span.span_data:
|
| 44 |
+
if span.span_data.type:
|
| 45 |
+
message += f" {span.span_data.type}"
|
| 46 |
+
if hasattr(span.span_data, "name") and span.span_data.name:
|
| 47 |
+
message += f" {span.span_data.name}"
|
| 48 |
+
if hasattr(span.span_data, "server") and span.span_data.server:
|
| 49 |
+
message += f" {span.span_data.server}"
|
| 50 |
+
if span.error:
|
| 51 |
+
message += f" {span.error}"
|
| 52 |
+
write_log(name, type, message)
|
| 53 |
+
|
| 54 |
+
def on_span_end(self, span) -> None:
|
| 55 |
+
name = self.get_name(span)
|
| 56 |
+
type = span.span_data.type if span.span_data else "span"
|
| 57 |
+
if name:
|
| 58 |
+
message = "Ended"
|
| 59 |
+
if span.span_data:
|
| 60 |
+
if span.span_data.type:
|
| 61 |
+
|
| 62 |
+
message += f" {span.span_data.type}"
|
| 63 |
+
if hasattr(span.span_data, "name") and span.span_data.name:
|
| 64 |
+
message += f" {span.span_data.name}"
|
| 65 |
+
if hasattr(span.span_data, "server") and span.span_data.server:
|
| 66 |
+
message += f" {span.span_data.server}"
|
| 67 |
+
if span.error:
|
| 68 |
+
message += f" {span.error}"
|
| 69 |
+
write_log(name, type, message)
|
| 70 |
+
|
| 71 |
+
def force_flush(self) -> None:
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
def shutdown(self) -> None:
|
| 75 |
+
pass
|
traders.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import AsyncExitStack
|
| 2 |
+
from accounts_client import read_accounts_resource, read_strategy_resource
|
| 3 |
+
from tracers import make_trace_id
|
| 4 |
+
from agents import Agent, Tool, Runner, OpenAIChatCompletionsModel, trace
|
| 5 |
+
from openai import AsyncOpenAI
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
from agents.mcp import MCPServerStdio
|
| 10 |
+
from templates import (
|
| 11 |
+
researcher_instructions,
|
| 12 |
+
trader_instructions,
|
| 13 |
+
trade_message,
|
| 14 |
+
rebalance_message,
|
| 15 |
+
research_tool,
|
| 16 |
+
)
|
| 17 |
+
from mcp_params import trader_mcp_server_params, researcher_mcp_server_params
|
| 18 |
+
|
| 19 |
+
load_dotenv(override=True)
|
| 20 |
+
|
| 21 |
+
deepseek_api_key = os.getenv("DEEPSEEK_API_KEY")
|
| 22 |
+
google_api_key = os.getenv("GOOGLE_API_KEY")
|
| 23 |
+
grok_api_key = os.getenv("GROK_API_KEY")
|
| 24 |
+
openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
|
| 25 |
+
|
| 26 |
+
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
| 27 |
+
GROK_BASE_URL = "https://api.x.ai/v1"
|
| 28 |
+
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
| 29 |
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
| 30 |
+
|
| 31 |
+
MAX_TURNS = 30
|
| 32 |
+
|
| 33 |
+
openrouter_client = AsyncOpenAI(base_url=OPENROUTER_BASE_URL, api_key=openrouter_api_key)
|
| 34 |
+
deepseek_client = AsyncOpenAI(base_url=DEEPSEEK_BASE_URL, api_key=deepseek_api_key)
|
| 35 |
+
grok_client = AsyncOpenAI(base_url=GROK_BASE_URL, api_key=grok_api_key)
|
| 36 |
+
gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def get_model(model_name: str):
|
| 40 |
+
if "/" in model_name:
|
| 41 |
+
return OpenAIChatCompletionsModel(model=model_name, openai_client=openrouter_client)
|
| 42 |
+
elif "deepseek" in model_name:
|
| 43 |
+
return OpenAIChatCompletionsModel(model=model_name, openai_client=deepseek_client)
|
| 44 |
+
elif "grok" in model_name:
|
| 45 |
+
return OpenAIChatCompletionsModel(model=model_name, openai_client=grok_client)
|
| 46 |
+
elif "gemini" in model_name:
|
| 47 |
+
return OpenAIChatCompletionsModel(model=model_name, openai_client=gemini_client)
|
| 48 |
+
else:
|
| 49 |
+
return model_name
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
async def get_researcher(mcp_servers, model_name) -> Agent:
|
| 53 |
+
researcher = Agent(
|
| 54 |
+
name="Researcher",
|
| 55 |
+
instructions=researcher_instructions(),
|
| 56 |
+
model=get_model(model_name),
|
| 57 |
+
mcp_servers=mcp_servers,
|
| 58 |
+
)
|
| 59 |
+
return researcher
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
async def get_researcher_tool(mcp_servers, model_name) -> Tool:
|
| 63 |
+
researcher = await get_researcher(mcp_servers, model_name)
|
| 64 |
+
return researcher.as_tool(tool_name="Researcher", tool_description=research_tool())
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class Trader:
|
| 68 |
+
def __init__(self, name: str, lastname="Trader", model_name="gpt-4o-mini"):
|
| 69 |
+
self.name = name
|
| 70 |
+
self.lastname = lastname
|
| 71 |
+
self.agent = None
|
| 72 |
+
self.model_name = model_name
|
| 73 |
+
self.do_trade = True
|
| 74 |
+
|
| 75 |
+
async def create_agent(self, trader_mcp_servers, researcher_mcp_servers) -> Agent:
|
| 76 |
+
tool = await get_researcher_tool(researcher_mcp_servers, self.model_name)
|
| 77 |
+
self.agent = Agent(
|
| 78 |
+
name=self.name,
|
| 79 |
+
instructions=trader_instructions(self.name),
|
| 80 |
+
model=get_model(self.model_name),
|
| 81 |
+
tools=[tool],
|
| 82 |
+
mcp_servers=trader_mcp_servers,
|
| 83 |
+
)
|
| 84 |
+
return self.agent
|
| 85 |
+
|
| 86 |
+
async def get_account_report(self) -> str:
|
| 87 |
+
account = await read_accounts_resource(self.name)
|
| 88 |
+
account_json = json.loads(account)
|
| 89 |
+
account_json.pop("portfolio_value_time_series", None)
|
| 90 |
+
return json.dumps(account_json)
|
| 91 |
+
|
| 92 |
+
async def run_agent(self, trader_mcp_servers, researcher_mcp_servers):
|
| 93 |
+
self.agent = await self.create_agent(trader_mcp_servers, researcher_mcp_servers)
|
| 94 |
+
account = await self.get_account_report()
|
| 95 |
+
strategy = await read_strategy_resource(self.name)
|
| 96 |
+
message = (
|
| 97 |
+
trade_message(self.name, strategy, account)
|
| 98 |
+
if self.do_trade
|
| 99 |
+
else rebalance_message(self.name, strategy, account)
|
| 100 |
+
)
|
| 101 |
+
await Runner.run(self.agent, message, max_turns=MAX_TURNS)
|
| 102 |
+
|
| 103 |
+
async def run_with_mcp_servers(self):
|
| 104 |
+
async with AsyncExitStack() as stack:
|
| 105 |
+
trader_mcp_servers = [
|
| 106 |
+
await stack.enter_async_context(
|
| 107 |
+
MCPServerStdio(params, client_session_timeout_seconds=120)
|
| 108 |
+
)
|
| 109 |
+
for params in trader_mcp_server_params
|
| 110 |
+
]
|
| 111 |
+
async with AsyncExitStack() as stack:
|
| 112 |
+
researcher_mcp_servers = [
|
| 113 |
+
await stack.enter_async_context(
|
| 114 |
+
MCPServerStdio(params, client_session_timeout_seconds=120)
|
| 115 |
+
)
|
| 116 |
+
for params in researcher_mcp_server_params(self.name)
|
| 117 |
+
]
|
| 118 |
+
await self.run_agent(trader_mcp_servers, researcher_mcp_servers)
|
| 119 |
+
|
| 120 |
+
async def run_with_trace(self):
|
| 121 |
+
trace_name = f"{self.name}-trading" if self.do_trade else f"{self.name}-rebalancing"
|
| 122 |
+
trace_id = make_trace_id(f"{self.name.lower()}")
|
| 123 |
+
with trace(trace_name, trace_id=trace_id):
|
| 124 |
+
await self.run_with_mcp_servers()
|
| 125 |
+
|
| 126 |
+
async def run(self):
|
| 127 |
+
try:
|
| 128 |
+
await self.run_with_trace()
|
| 129 |
+
except Exception as e:
|
| 130 |
+
print(f"Error running trader {self.name}: {e}")
|
| 131 |
+
self.do_trade = not self.do_trade
|
trading_floor.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from traders import Trader
|
| 2 |
+
from typing import List
|
| 3 |
+
import asyncio
|
| 4 |
+
from tracers import LogTracer
|
| 5 |
+
from agents import add_trace_processor
|
| 6 |
+
from market import is_market_open
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
|
| 12 |
+
RUN_EVERY_N_MINUTES = int(os.getenv("RUN_EVERY_N_MINUTES", "60"))
|
| 13 |
+
RUN_EVEN_WHEN_MARKET_IS_CLOSED = (
|
| 14 |
+
os.getenv("RUN_EVEN_WHEN_MARKET_IS_CLOSED", "false").strip().lower() == "true"
|
| 15 |
+
)
|
| 16 |
+
USE_MANY_MODELS = os.getenv("USE_MANY_MODELS", "false").strip().lower() == "true"
|
| 17 |
+
|
| 18 |
+
names = ["Warren", "George", "Ray", "Cathie"]
|
| 19 |
+
lastnames = ["Patience", "Bold", "Systematic", "Crypto"]
|
| 20 |
+
|
| 21 |
+
if USE_MANY_MODELS:
|
| 22 |
+
model_names = [
|
| 23 |
+
"gpt-4.1-mini",
|
| 24 |
+
"deepseek-chat",
|
| 25 |
+
"gemini-2.5-flash-preview-04-17",
|
| 26 |
+
"grok-3-mini-beta",
|
| 27 |
+
]
|
| 28 |
+
short_model_names = ["GPT 4.1 Mini", "DeepSeek V3", "Gemini 2.5 Flash", "Grok 3 Mini"]
|
| 29 |
+
else:
|
| 30 |
+
model_names = ["gpt-4o-mini"] * 4
|
| 31 |
+
short_model_names = ["GPT 4o mini"] * 4
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def create_traders() -> List[Trader]:
|
| 35 |
+
traders = []
|
| 36 |
+
for name, lastname, model_name in zip(names, lastnames, model_names):
|
| 37 |
+
traders.append(Trader(name, lastname, model_name))
|
| 38 |
+
return traders
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def run_every_n_minutes():
|
| 42 |
+
add_trace_processor(LogTracer())
|
| 43 |
+
traders = create_traders()
|
| 44 |
+
while True:
|
| 45 |
+
if RUN_EVEN_WHEN_MARKET_IS_CLOSED or is_market_open():
|
| 46 |
+
await asyncio.gather(*[trader.run() for trader in traders])
|
| 47 |
+
else:
|
| 48 |
+
print("Market is closed, skipping run")
|
| 49 |
+
await asyncio.sleep(RUN_EVERY_N_MINUTES * 60)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
print(f"Starting scheduler to run every {RUN_EVERY_N_MINUTES} minutes")
|
| 54 |
+
asyncio.run(run_every_n_minutes())
|
util.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
|
| 3 |
+
css = """
|
| 4 |
+
.positive-pnl {
|
| 5 |
+
color: green !important;
|
| 6 |
+
font-weight: bold;
|
| 7 |
+
}
|
| 8 |
+
.positive-bg {
|
| 9 |
+
background-color: green !important;
|
| 10 |
+
font-weight: bold;
|
| 11 |
+
}
|
| 12 |
+
.negative-bg {
|
| 13 |
+
background-color: red !important;
|
| 14 |
+
font-weight: bold;
|
| 15 |
+
}
|
| 16 |
+
.negative-pnl {
|
| 17 |
+
color: red !important;
|
| 18 |
+
font-weight: bold;
|
| 19 |
+
}
|
| 20 |
+
.dataframe-fix-small .table-wrap {
|
| 21 |
+
min-height: 150px;
|
| 22 |
+
max-height: 150px;
|
| 23 |
+
}
|
| 24 |
+
.dataframe-fix .table-wrap {
|
| 25 |
+
min-height: 200px;
|
| 26 |
+
max-height: 200px;
|
| 27 |
+
}
|
| 28 |
+
footer{display:none !important}
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
js = """
|
| 33 |
+
function refresh() {
|
| 34 |
+
const url = new URL(window.location);
|
| 35 |
+
|
| 36 |
+
if (url.searchParams.get('__theme') !== 'dark') {
|
| 37 |
+
url.searchParams.set('__theme', 'dark');
|
| 38 |
+
window.location.href = url.href;
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
class Color(Enum):
|
| 44 |
+
RED = "#dd0000"
|
| 45 |
+
GREEN = "#00dd00"
|
| 46 |
+
YELLOW = "#dddd00"
|
| 47 |
+
BLUE = "#0000ee"
|
| 48 |
+
MAGENTA = "#aa00dd"
|
| 49 |
+
CYAN = "#00dddd"
|
| 50 |
+
WHITE = "#87CEEB"
|