You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

200 lines
8.7 KiB

#!/usr/bin/env python3
"""
Seed a Ghostfolio account with realistic demo portfolio data.
Usage:
# Create a brand-new user and seed it (prints the access token when done):
python seed_demo.py --base-url https://ghostfolio-production-01e0.up.railway.app
# Seed an existing account (supply its auth JWT):
python seed_demo.py --base-url https://... --auth-token eyJ...
The script creates:
- 1 brokerage account ("Demo Portfolio")
- 18 realistic BUY/SELL/DIVIDEND transactions spanning 2021-2024
covering AAPL, MSFT, NVDA, GOOGL, AMZN, VTI (ETF)
"""
import argparse
import json
import sys
import urllib.request
import urllib.error
from datetime import datetime, timezone
DEFAULT_BASE_URL = "https://ghostfolio-production-01e0.up.railway.app"
_base_url = DEFAULT_BASE_URL
# ---------------------------------------------------------------------------
# HTTP helpers
# ---------------------------------------------------------------------------
def _request(method: str, path: str, body: dict | None = None, token: str | None = None) -> dict:
url = _base_url.rstrip("/") + path
data = json.dumps(body).encode() if body is not None else None
headers = {"Content-Type": "application/json", "Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body_text = e.read().decode()
print(f" HTTP {e.code} on {method} {path}: {body_text}", file=sys.stderr)
return {"error": body_text, "statusCode": e.code}
# ---------------------------------------------------------------------------
# Step 1 – auth
# ---------------------------------------------------------------------------
def create_user() -> tuple[str, str]:
"""Create a new anonymous user. Returns (accessToken, authToken)."""
print("Creating new demo user …")
resp = _request("POST", "/api/v1/user", {})
if "authToken" not in resp:
print(f"Failed to create user: {resp}", file=sys.stderr)
sys.exit(1)
print(f" User created • accessToken: {resp['accessToken']}")
return resp["accessToken"], resp["authToken"]
def get_auth_token(access_token: str) -> str:
"""Exchange an access token for a JWT."""
resp = _request("GET", f"/api/v1/auth/anonymous/{access_token}")
if "authToken" not in resp:
print(f"Failed to authenticate: {resp}", file=sys.stderr)
sys.exit(1)
return resp["authToken"]
# ---------------------------------------------------------------------------
# Step 2 – create brokerage account
# ---------------------------------------------------------------------------
def create_account(jwt: str) -> str:
"""Create a brokerage account and return its ID."""
print("Creating brokerage account …")
resp = _request("POST", "/api/v1/account", {
"balance": 0,
"currency": "USD",
"isExcluded": False,
"name": "Demo Portfolio",
"platformId": None
}, token=jwt)
if "id" not in resp:
print(f"Failed to create account: {resp}", file=sys.stderr)
sys.exit(1)
print(f" Account ID: {resp['id']}")
return resp["id"]
# ---------------------------------------------------------------------------
# Step 3 – import activities
# ---------------------------------------------------------------------------
ACTIVITIES = [
# AAPL — built position over 2021-2022, partial sell in 2023
{"type": "BUY", "symbol": "AAPL", "quantity": 10, "unitPrice": 134.18, "fee": 0, "currency": "USD", "date": "2021-03-15"},
{"type": "BUY", "symbol": "AAPL", "quantity": 5, "unitPrice": 148.56, "fee": 0, "currency": "USD", "date": "2021-09-10"},
{"type": "DIVIDEND", "symbol": "AAPL", "quantity": 1, "unitPrice": 3.44, "fee": 0, "currency": "USD", "date": "2022-02-04"},
{"type": "SELL", "symbol": "AAPL", "quantity": 5, "unitPrice": 183.12, "fee": 0, "currency": "USD", "date": "2023-06-20"},
{"type": "DIVIDEND", "symbol": "AAPL", "quantity": 1, "unitPrice": 3.66, "fee": 0, "currency": "USD", "date": "2023-08-04"},
# MSFT — steady accumulation
{"type": "BUY", "symbol": "MSFT", "quantity": 8, "unitPrice": 242.15, "fee": 0, "currency": "USD", "date": "2021-05-20"},
{"type": "BUY", "symbol": "MSFT", "quantity": 4, "unitPrice": 299.35, "fee": 0, "currency": "USD", "date": "2022-01-18"},
{"type": "DIVIDEND", "symbol": "MSFT", "quantity": 1, "unitPrice": 9.68, "fee": 0, "currency": "USD", "date": "2022-06-09"},
{"type": "DIVIDEND", "symbol": "MSFT", "quantity": 1, "unitPrice": 10.40, "fee": 0, "currency": "USD", "date": "2023-06-08"},
# NVDA — bought cheap, rode the AI wave
{"type": "BUY", "symbol": "NVDA", "quantity": 6, "unitPrice": 143.25, "fee": 0, "currency": "USD", "date": "2021-11-05"},
{"type": "BUY", "symbol": "NVDA", "quantity": 4, "unitPrice": 166.88, "fee": 0, "currency": "USD", "date": "2022-07-12"},
# GOOGL
{"type": "BUY", "symbol": "GOOGL", "quantity": 3, "unitPrice": 2718.96,"fee": 0, "currency": "USD", "date": "2021-08-03"},
{"type": "BUY", "symbol": "GOOGL", "quantity": 5, "unitPrice": 102.30, "fee": 0, "currency": "USD", "date": "2022-08-15"},
# AMZN
{"type": "BUY", "symbol": "AMZN", "quantity": 4, "unitPrice": 168.54, "fee": 0, "currency": "USD", "date": "2023-02-08"},
# VTI — ETF core holding
{"type": "BUY", "symbol": "VTI", "quantity": 15, "unitPrice": 207.38, "fee": 0, "currency": "USD", "date": "2021-04-06"},
{"type": "BUY", "symbol": "VTI", "quantity": 10, "unitPrice": 183.52, "fee": 0, "currency": "USD", "date": "2022-10-14"},
{"type": "DIVIDEND", "symbol": "VTI", "quantity": 1, "unitPrice": 10.28, "fee": 0, "currency": "USD", "date": "2022-12-27"},
{"type": "DIVIDEND", "symbol": "VTI", "quantity": 1, "unitPrice": 11.42, "fee": 0, "currency": "USD", "date": "2023-12-27"},
]
def import_activities(jwt: str, account_id: str) -> None:
print(f"Importing {len(ACTIVITIES)} activities (YAHOO first, MANUAL fallback) …")
imported = 0
for a in ACTIVITIES:
for data_source in ("YAHOO", "MANUAL"):
payload = {
"accountId": account_id,
"currency": a["currency"],
"dataSource": data_source,
"date": f"{a['date']}T00:00:00.000Z",
"fee": a["fee"],
"quantity": a["quantity"],
"symbol": a["symbol"],
"type": a["type"],
"unitPrice": a["unitPrice"],
}
resp = _request("POST", "/api/v1/import", {"activities": [payload]}, token=jwt)
if not resp.get("error") and resp.get("statusCode", 200) < 400:
imported += 1
print(f"{a['type']:8} {a['symbol']:5} ({data_source})")
break
else:
print(f"{a['type']:8} {a['symbol']:5} — skipped (both sources failed)", file=sys.stderr)
print(f" Imported {imported}/{len(ACTIVITIES)} activities successfully")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Ghostfolio base URL")
parser.add_argument("--auth-token", default=None, help="Existing JWT (skip user creation)")
parser.add_argument("--access-token", default=None, help="Existing access token to exchange for JWT")
args = parser.parse_args()
global _base_url
_base_url = args.base_url.rstrip("/")
# Resolve JWT
if args.auth_token:
jwt = args.auth_token
access_token = "(provided)"
print(f"Using provided auth token.")
elif args.access_token:
print(f"Exchanging access token for JWT …")
jwt = get_auth_token(args.access_token)
access_token = args.access_token
else:
access_token, jwt = create_user()
account_id = create_account(jwt)
import_activities(jwt, account_id)
print()
print("=" * 60)
print(" Demo account seeded successfully!")
print("=" * 60)
print(f" Login URL : {_base_url}/en/register")
print(f" Access token: {access_token}")
print(f" Auth JWT : {jwt}")
print()
print(" To use with the agent, set:")
print(f" GHOSTFOLIO_BEARER_TOKEN={jwt}")
print("=" * 60)
if __name__ == "__main__":
main()