mirror of https://github.com/ghostfolio/ghostfolio
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
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()
|
|
|