diff --git a/web/src/Dockerfile b/Dockerfile similarity index 100% rename from web/src/Dockerfile rename to Dockerfile diff --git a/Makefile b/Makefile deleted file mode 100644 index a5c9806..0000000 --- a/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -.PHONY: setup run test build-mac-selfcontained - -setup: - ./web/run.sh --setup-only - -run: - ./web/run.sh - -test: setup - . .venv/bin/activate && PYTHONPATH=web/src pytest -q web/src/tests - -build-mac-selfcontained: - ./scripts/build_selfcontained_mac_app.sh diff --git a/web/src/ONBOARDING.md b/ONBOARDING.md similarity index 100% rename from web/src/ONBOARDING.md rename to ONBOARDING.md diff --git a/web/src/PRD.md b/PRD.md similarity index 100% rename from web/src/PRD.md rename to PRD.md diff --git a/README.md b/README.md deleted file mode 100644 index f71263c..0000000 --- a/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Real Bars vs Fake Bars Trend Analyzer - -A Python web app wrapped by a native macOS shell. - -## Standardized Layout -- `web/src/`: web backend + Streamlit app source -- `mac/src/`: Xcode macOS shell app (`WKWebView` host) -- `scripts/`: build and packaging scripts -- `docs/`: architecture and supporting docs -- `skills/`: reusable project skills - -## Web Source -- `web/src/app.py`: Streamlit entrypoint and UI orchestration -- `web/src/web_core/`: strategy/data/chart/export modules -- `web/src/requirements.txt`: Python dependencies -- `web/src/ONBOARDING.md`: in-app onboarding guide content -- `web/src/PRD.md`: web product rules and behavior spec - -## macOS Shell -- Project location: `mac/src/` (`*.xcodeproj` auto-discovered by scripts) -- Uses `WKWebView` and launches embedded backend executable from app resources. -- No external browser required. - -See `mac/src/README.md` for shell details. - -## Setup -### Quick start -```bash -./run.sh -``` - -### Setup only -```bash -./run.sh --setup-only -``` - -### Tests -```bash -make test -``` - -## Build Self-Contained macOS App -```bash -./scripts/build_selfcontained_mac_app.sh -``` -Output: `dist-mac//.app` - -Package as DMG: -```bash -APP_BUNDLE_PATH="dist-mac//.app" ./scripts/create_installer_dmg.sh -``` -Output: `build/dmg/-.dmg` - -## Optional Standalone Streamlit App -```bash -./scripts/build_standalone_app.sh -``` -Output: `dist-standalone//dist/.app` - -## Notes -- Analysis-only app; no trade execution. -- Yahoo Finance interval availability depends on symbol/lookback. -- For broad distribution, use code signing + notarization. diff --git a/Run App.command b/Run App.command deleted file mode 100755 index 40b8168..0000000 --- a/Run App.command +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -cd "$(dirname "$0")" -./run.sh diff --git a/web/src/SYNOLOGY.md b/SYNOLOGY.md similarity index 100% rename from web/src/SYNOLOGY.md rename to SYNOLOGY.md diff --git a/web/src/app.py b/app.py similarity index 100% rename from web/src/app.py rename to app.py diff --git a/web/src/backend_embedded_launcher.py b/backend_embedded_launcher.py similarity index 100% rename from web/src/backend_embedded_launcher.py rename to backend_embedded_launcher.py diff --git a/deploy_synology.sh b/deploy_synology.sh deleted file mode 100755 index 56189a1..0000000 --- a/deploy_synology.sh +++ /dev/null @@ -1,391 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ -z "${BASH_VERSION:-}" ]]; then - echo "Error: run this script with bash (example: bash deploy_synology.sh)." >&2 - exit 1 -fi - -# ManeshTrader quick deploy helper for Synology Docker host. -# Defaults are set for the target provided by the user. - -ENV_FILE=".env.deploy_synology" -if [[ -f "$ENV_FILE" ]]; then - # shellcheck disable=SC1090 - set -a; source "$ENV_FILE"; set +a -fi - -REMOTE_USER="${REMOTE_USER:-mbrucedogs}" -REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}" -REMOTE_PORT="${REMOTE_PORT:-25}" -REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}" -LOCAL_BASE="${LOCAL_BASE:-web/src}" -SYNO_PASSWORD="${SYNO_PASSWORD:-}" -CONTAINER_NAME="${CONTAINER_NAME:-}" -SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}" -SUDO_MODE="${SUDO_MODE:-auto}" # auto|always|never -SSH_PASS_WARNING_SHOWN="0" - -MODE="bind" # bind: upload + restart; image: upload + rebuild -NO_RESTART="0" -DRY_RUN="0" - -DEFAULT_FILES=( - "web/src/web_core/ui/help_content.py" - "web/src/PRD.md" - "web/src/ONBOARDING.md" -) - -FILES=("${DEFAULT_FILES[@]}") - -usage() { - cat <<'EOF' -Usage: - ./deploy_synology.sh [options] - -Options: - --mode bind|image Deploy mode (default: bind) - --files "a b c" Space-separated file list to upload - --recent-minutes N Upload files under web/src modified in last N minutes - --git-hours N Upload files changed in last N hours (git history + working tree) - --env-file PATH Load environment variables from file (default: .env.deploy_synology if present) - --sudo-mode MODE Docker privilege mode: auto|always|never (default: auto) - --no-restart Upload only; skip remote docker restart/rebuild - --dry-run Print actions but do not execute - -h, --help Show help - -Environment overrides: - REMOTE_USER, REMOTE_HOST, REMOTE_PORT, REMOTE_BASE, LOCAL_BASE, SYNO_PASSWORD, CONTAINER_NAME, - SUDO_PASSWORD, SUDO_MODE - -Examples: - ./deploy_synology.sh - ./deploy_synology.sh --mode image - ./deploy_synology.sh --recent-minutes 120 - ./deploy_synology.sh --git-hours 2 - ./deploy_synology.sh --env-file .env.deploy_synology --git-hours 2 - ./deploy_synology.sh --git-hours 2 --sudo-mode always - ./deploy_synology.sh --files "web/src/web_core/ui/help_content.py" -EOF -} - -run_cmd() { - if [[ "$DRY_RUN" == "1" ]]; then - printf '[dry-run] %s\n' "$*" - else - eval "$@" - fi -} - -build_ssh_cmd() { - if [[ -n "${SYNO_PASSWORD:-}" ]]; then - if ! command -v sshpass >/dev/null 2>&1; then - if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then - echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2 - SSH_PASS_WARNING_SHOWN="1" - fi - printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" - return - fi - printf "SSHPASS='%s' sshpass -e ssh -o StrictHostKeyChecking=accept-new -p '%s' '%s@%s'" \ - "${SYNO_PASSWORD}" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" - else - printf "ssh -p '%s' '%s@%s'" "${REMOTE_PORT}" "${REMOTE_USER}" "${REMOTE_HOST}" - fi -} - -build_scp_cmd() { - if [[ -n "${SYNO_PASSWORD:-}" ]]; then - if ! command -v sshpass >/dev/null 2>&1; then - if [[ "$SSH_PASS_WARNING_SHOWN" == "0" ]]; then - echo "Warning: SYNO_PASSWORD is set but sshpass is not installed; falling back to interactive ssh/scp prompts." >&2 - SSH_PASS_WARNING_SHOWN="1" - fi - printf "scp -O -P '%s'" "${REMOTE_PORT}" - return - fi - printf "SSHPASS='%s' sshpass -e scp -O -o StrictHostKeyChecking=accept-new -P '%s'" \ - "${SYNO_PASSWORD}" "${REMOTE_PORT}" - else - printf "scp -O -P '%s'" "${REMOTE_PORT}" - fi -} - -to_remote_rel_path() { - local file="$1" - local prefix="${LOCAL_BASE%/}/" - if [[ "$file" == "$prefix"* ]]; then - printf "%s" "${file#$prefix}" - else - printf "%s" "$file" - fi -} - -collect_git_hours_files() { - local hours="$1" - local base_commit - local combined - - if ! command -v git >/dev/null 2>&1; then - echo "Error: git is required for --git-hours." >&2 - exit 1 - fi - if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - echo "Error: --git-hours must be run from inside a git repository." >&2 - exit 1 - fi - - base_commit="$(git rev-list -n 1 --before="${hours} hours ago" HEAD 2>/dev/null || true)" - - combined="$( - { - if [[ -n "$base_commit" ]]; then - git diff --name-only --diff-filter=ACMRTUXB "${base_commit}..HEAD" -- web/src - fi - git diff --name-only -- web/src - git diff --cached --name-only -- web/src - git ls-files -m -o --exclude-standard -- web/src - } | sed '/^$/d' | sort -u - )" - - FILES=() - while IFS= read -r candidate; do - [[ -z "$candidate" ]] && continue - FILES+=("$candidate") - done <&2 - exit 1 - fi - # shellcheck disable=SC1090 - set -a; source "$ENV_FILE"; set +a - REMOTE_USER="${REMOTE_USER:-mbrucedogs}" - REMOTE_HOST="${REMOTE_HOST:-192.168.1.128}" - REMOTE_PORT="${REMOTE_PORT:-25}" - REMOTE_BASE="${REMOTE_BASE:-/volume1/docker/maneshtrader}" - LOCAL_BASE="${LOCAL_BASE:-web/src}" - SYNO_PASSWORD="${SYNO_PASSWORD:-}" - CONTAINER_NAME="${CONTAINER_NAME:-}" - SUDO_PASSWORD="${SUDO_PASSWORD:-${SYNO_PASSWORD:-}}" - SUDO_MODE="${SUDO_MODE:-auto}" - shift 2 - ;; - --files) - read -r -a FILES <<< "${2:-}" - shift 2 - ;; - --recent-minutes) - minutes="${2:-}" - if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then - echo "Error: --recent-minutes requires an integer." >&2 - exit 1 - fi - FILES=() - while IFS= read -r candidate; do - [[ -z "$candidate" ]] && continue - FILES+=("$candidate") - done <&2 - exit 1 - fi - collect_git_hours_files "$hours" - shift 2 - ;; - --no-restart) - NO_RESTART="1" - shift - ;; - --sudo-mode) - SUDO_MODE="${2:-}" - shift 2 - ;; - --dry-run) - DRY_RUN="1" - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown option: $1" >&2 - usage - exit 1 - ;; - esac -done - -if [[ "$MODE" != "bind" && "$MODE" != "image" ]]; then - echo "Error: --mode must be 'bind' or 'image'." >&2 - exit 1 -fi -if [[ "$SUDO_MODE" != "auto" && "$SUDO_MODE" != "always" && "$SUDO_MODE" != "never" ]]; then - echo "Error: --sudo-mode must be auto|always|never." >&2 - exit 1 -fi - -if [[ ${#FILES[@]} -eq 0 ]]; then - echo "Error: No files selected for upload." >&2 - exit 1 -fi - -for file in "${FILES[@]}"; do - if [[ ! -f "$file" ]]; then - echo "Error: Local file not found: $file" >&2 - exit 1 - fi -done - -echo "Deploy target: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}" -echo "Remote base: ${REMOTE_BASE}" -echo "Mode: ${MODE}" -echo "Files:" -for file in "${FILES[@]}"; do - echo " - $file" -done - -for file in "${FILES[@]}"; do - remote_rel="$(to_remote_rel_path "$file")" - remote_path="${REMOTE_BASE}/${remote_rel}" - remote_dir="$(dirname "$remote_path")" - SSH_CMD="$(build_ssh_cmd)" - SCP_CMD="$(build_scp_cmd)" - run_cmd "${SSH_CMD} \"mkdir -p '${remote_dir}'\"" - run_cmd "${SCP_CMD} '${file}' '${REMOTE_USER}@${REMOTE_HOST}:${remote_path}'" -done - -if [[ "$NO_RESTART" == "1" ]]; then - echo "Upload complete (restart/rebuild skipped)." - exit 0 -fi - -if [[ "$MODE" == "bind" ]]; then - SSH_CMD="$(build_ssh_cmd)" - run_cmd "${SSH_CMD} ' -set -e -cd \"${REMOTE_BASE}\" -SUDO_MODE=\"${SUDO_MODE}\" -SUDO_PASSWORD=\"${SUDO_PASSWORD}\" -DOCKER_BIN=\"\" -if command -v docker >/dev/null 2>&1; then - DOCKER_BIN=\"\$(command -v docker)\" -elif [[ -x /usr/local/bin/docker ]]; then - DOCKER_BIN=\"/usr/local/bin/docker\" -elif [[ -x /usr/bin/docker ]]; then - DOCKER_BIN=\"/usr/bin/docker\" -elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then - DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\" -fi - -docker_cmd() { - if [[ \"\$USE_SUDO\" == \"1\" ]]; then - if [[ -n \"\$SUDO_PASSWORD\" ]]; then - printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\" - else - sudo \"\$DOCKER_BIN\" \"\$@\" - fi - else - \"\$DOCKER_BIN\" \"\$@\" - fi -} - -USE_SUDO=\"0\" -if [[ \"\$SUDO_MODE\" == \"always\" ]]; then - USE_SUDO=\"1\" -elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then - if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then - USE_SUDO=\"1\" - fi -fi - -if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then - docker_cmd compose restart -elif command -v docker-compose >/dev/null 2>&1; then - docker-compose restart -elif [[ -n \"\$DOCKER_BIN\" ]]; then - if [[ -n \"${CONTAINER_NAME:-}\" ]]; then - docker_cmd restart \"${CONTAINER_NAME}\" - else - ids=\$(docker_cmd ps --filter \"name=maneshtrader\" -q) - if [[ -z \"\$ids\" ]]; then - echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2 - echo \"Set CONTAINER_NAME= and rerun.\" >&2 - exit 1 - fi - docker_cmd restart \$ids - fi -else - echo \"No docker or docker compose command found on remote host.\" >&2 - exit 1 -fi -'" - echo "Upload + container restart complete." -else - SSH_CMD="$(build_ssh_cmd)" - run_cmd "${SSH_CMD} ' -set -e -cd \"${REMOTE_BASE}\" -SUDO_MODE=\"${SUDO_MODE}\" -SUDO_PASSWORD=\"${SUDO_PASSWORD}\" -DOCKER_BIN=\"\" -if command -v docker >/dev/null 2>&1; then - DOCKER_BIN=\"\$(command -v docker)\" -elif [[ -x /usr/local/bin/docker ]]; then - DOCKER_BIN=\"/usr/local/bin/docker\" -elif [[ -x /usr/bin/docker ]]; then - DOCKER_BIN=\"/usr/bin/docker\" -elif [[ -x /var/packages/ContainerManager/target/usr/bin/docker ]]; then - DOCKER_BIN=\"/var/packages/ContainerManager/target/usr/bin/docker\" -fi - -docker_cmd() { - if [[ \"\$USE_SUDO\" == \"1\" ]]; then - if [[ -n \"\$SUDO_PASSWORD\" ]]; then - printf \"%s\\n\" \"\$SUDO_PASSWORD\" | sudo -S -p \"\" \"\$DOCKER_BIN\" \"\$@\" - else - sudo \"\$DOCKER_BIN\" \"\$@\" - fi - else - \"\$DOCKER_BIN\" \"\$@\" - fi -} - -USE_SUDO=\"0\" -if [[ \"\$SUDO_MODE\" == \"always\" ]]; then - USE_SUDO=\"1\" -elif [[ \"\$SUDO_MODE\" == \"auto\" ]]; then - if ! \"\$DOCKER_BIN\" info >/dev/null 2>&1; then - USE_SUDO=\"1\" - fi -fi - -if [[ -n \"\$DOCKER_BIN\" ]] && docker_cmd compose version >/dev/null 2>&1; then - docker_cmd compose up -d --build -elif command -v docker-compose >/dev/null 2>&1; then - docker-compose up -d --build -else - echo \"No docker compose command found on remote host.\" >&2 - exit 1 -fi -'" - echo "Upload + image rebuild complete." -fi diff --git a/web/src/desktop_launcher.py b/desktop_launcher.py similarity index 100% rename from web/src/desktop_launcher.py rename to desktop_launcher.py diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e0a2519 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + maneshtrader: + image: python:3.11-slim + container_name: maneshtrader + working_dir: /app + restart: unless-stopped + ports: + - "${APP_PORT:-8501}:8501" + environment: + PYTHONDONTWRITEBYTECODE: "1" + PYTHONUNBUFFERED: "1" + PIP_NO_CACHE_DIR: "1" + STREAMLIT_BROWSER_GATHER_USAGE_STATS: "false" + volumes: + - ./:/app + - ./data:/root/.web_local_shell + command: > + sh -c "pip install -r requirements.txt && + streamlit run app.py --server.address=0.0.0.0 --server.port=8501 --server.headless=true" + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8501/_stcore/health', timeout=3)\" || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 45s diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 9ddf5ca..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,345 +0,0 @@ -# ManeshTrader - Architecture Plan - -## Executive Summary -ManeshTrader is an analysis-only trading intelligence system that classifies OHLC bars into real/fake signals, derives trend state from real bars, and delivers visual insights, exports, and optional monitoring alerts. The architecture refactor moves from a single-process UI-first design to a modular, service-oriented design that improves testability, scalability, observability, and operational resilience. - -## System Context -```mermaid -flowchart LR - Trader[Trader / Analyst] -->|Configure symbol, timeframe, filters| ManeshTrader[ManeshTrader Analysis Platform] - ManeshTrader -->|Fetch OHLC| Yahoo[Yahoo Finance Data API] - ManeshTrader -->|Optional notifications| Notify[Email / Push / Webhook] - ManeshTrader -->|Export CSV/PDF| Storage[(User Downloads / Object Storage)] - Admin[Operator] -->|Monitor health, logs, metrics| ManeshTrader -``` - -Overview: Defines external actors and dependencies around ManeshTrader. - -Key Components: Trader, Operator, data provider, notification endpoints, export targets. - -Relationships: Traders submit analysis requests; platform retrieves market data, computes classifications/trends, and returns charted/exported output. - -Design Decisions: Keep execution out-of-scope (analysis-only boundary). External market source decoupled behind adapter interface. - -NFR Considerations: -- Scalability: Stateless analysis workloads can scale horizontally. -- Performance: Cached market data and precomputed features reduce latency. -- Security: Read-only market data; no brokerage keys for execution. -- Reliability: Retry/fallback on upstream data issues. -- Maintainability: Clear system boundary reduces coupling. - -Trade-offs: Dependency on third-party data availability/quality. - -Risks and Mitigations: -- Risk: API throttling/outage. Mitigation: caching, backoff, alternate provider adapter. - -## Architecture Overview -The target architecture uses modular services with explicit boundaries: -- Presentation: Web UI / API consumers -- Application: Strategy orchestration and workflow -- Domain: Bar classification and trend state machine -- Data: Provider adapters, cache, persistence -- Platform: auth, observability, notifications, exports - -## Component Architecture -```mermaid -flowchart TB - UI[Web UI / Streamlit Frontend] - API[Analysis API Gateway] - ORCH[Analysis Orchestrator] - STRAT[Strategy Engine -(Real/Fake Classifier + Trend State Machine)] - BT[Backtest Evaluator] - ALERT[Alerting Service] - EXPORT[Export Service -(CSV/PDF)] - MKT[Market Data Adapter] - CACHE[(Market Cache)] - DB[(Analysis Store)] - OBS[Observability -Logs/Metrics/Traces] - - UI --> API - API --> ORCH - ORCH --> STRAT - ORCH --> BT - ORCH --> ALERT - ORCH --> EXPORT - ORCH --> MKT - MKT --> CACHE - MKT --> ORCH - ORCH --> DB - API --> DB - API --> OBS - ORCH --> OBS - STRAT --> OBS - ALERT --> OBS -``` - -Overview: Internal modular decomposition for the refactored system. - -Key Components: -- Analysis API Gateway: request validation and rate limiting. -- Analysis Orchestrator: coordinates data fetch, strategy execution, and response assembly. -- Strategy Engine: deterministic classification and trend transitions. -- Backtest Evaluator: lightweight historical scoring. -- Alerting/Export Services: asynchronous side effects. -- Market Data Adapter + Cache: provider abstraction and performance buffer. - -Relationships: API delegates to orchestrator; orchestrator composes domain and side-effect services; observability is cross-cutting. - -Design Decisions: Separate deterministic core logic from IO-heavy integrations. - -NFR Considerations: -- Scalability: independent scaling for API, strategy workers, and alert/export processors. -- Performance: cache first, compute second, persist last. -- Security: centralized input validation and API policy. -- Reliability: queue-based side effects isolate failures. -- Maintainability: single-responsibility services and clear contracts. - -Trade-offs: More infrastructure and operational complexity than monolith. - -Risks and Mitigations: -- Risk: distributed debugging complexity. Mitigation: trace IDs and structured logs. - -## Deployment Architecture -```mermaid -flowchart TB - subgraph Internet - U[User Browser] - end - - subgraph Cloud[VPC] - LB[HTTPS Load Balancer / WAF] - subgraph AppSubnet[Private App Subnet] - UI[UI Service] - API[API Service] - WRK[Worker Service] - end - subgraph DataSubnet[Private Data Subnet] - REDIS[(Redis Cache)] - PG[(PostgreSQL)] - MQ[(Message Queue)] - OBJ[(Object Storage)] - end - OBS[Managed Monitoring] - SECRET[Secrets Manager] - end - - EXT[Yahoo Finance / Alt Provider] - - U --> LB --> UI - UI --> API - API --> REDIS - API --> PG - API --> MQ - WRK --> MQ - WRK --> PG - WRK --> OBJ - API --> EXT - WRK --> EXT - API --> OBS - WRK --> OBS - API --> SECRET - WRK --> SECRET -``` - -Overview: Logical production deployment with secure segmentation. - -Key Components: WAF/LB edge, stateless app services, queue-driven workers, managed data stores. - -Relationships: Request path stays synchronous through UI/API; heavy export/alert tasks handled asynchronously by workers. - -Design Decisions: Network segmentation and managed services for resilience and lower ops overhead. - -NFR Considerations: -- Scalability: autoscaling app and worker tiers. -- Performance: Redis cache and async task offload. -- Security: private subnets, secret manager, TLS at edge. -- Reliability: managed DB backup, queue durability. -- Maintainability: environment parity across dev/staging/prod. - -Trade-offs: Managed services cost vs operational simplicity. - -Risks and Mitigations: -- Risk: queue backlog under spikes. Mitigation: autoscaling workers and dead-letter queues. - -## Data Flow -```mermaid -flowchart LR - R[User Request -symbol/timeframe/filters] --> V[Input Validation] - V --> C1{Cache Hit?} - C1 -->|Yes| D1[Load OHLC from Cache] - C1 -->|No| D2[Fetch OHLC from Provider] - D2 --> D3[Normalize + Time Alignment] - D3 --> D4[Persist to Cache] - D1 --> P1[Classify Bars -real_bull/real_bear/fake] - D4 --> P1 - P1 --> P2[Trend State Machine -2-bar confirmation] - P2 --> P3[Backtest Snapshot] - P3 --> P4[Build Response Model] - P4 --> S1[(Analysis Store)] - P4 --> O[UI Chart + Metrics + Events] - P4 --> E[Export Job CSV/PDF] - P4 --> A[Alert Job] -``` - -Overview: End-to-end data processing lifecycle for each analysis request. - -Key Components: validation, cache/provider ingestion, classification/trend processing, output assembly. - -Relationships: deterministic analytics pipeline with optional async exports/alerts. - -Design Decisions: Normalize data before strategy logic for deterministic outcomes. - -NFR Considerations: -- Scalability: compute pipeline can be parallelized per request. -- Performance: cache avoids repeated provider calls. -- Security: strict input schema and output sanitization. -- Reliability: idempotent processing and persisted analysis snapshots. -- Maintainability: explicit stage boundaries simplify test coverage. - -Trade-offs: Additional persistence adds write latency. - -Risks and Mitigations: -- Risk: inconsistent timestamps across providers. Mitigation: canonical UTC normalization. - -## Key Workflows -```mermaid -sequenceDiagram - participant User - participant UI - participant API - participant Data as Market Adapter - participant Engine as Strategy Engine - participant Alert as Alert Service - participant Export as Export Service - - User->>UI: Submit symbol/timeframe/filters - UI->>API: Analyze request - API->>Data: Get closed OHLC bars - Data-->>API: Normalized bars - API->>Engine: Classify + detect trend - Engine-->>API: trend state, events, metrics - API-->>UI: chart model + events + backtest - alt New trend/reversal event - API->>Alert: Publish notification task - end - opt User requests export - UI->>API: Export CSV/PDF - API->>Export: Generate artifact - Export-->>UI: Download link/blob - end -``` - -Overview: Critical user flow from request to insight/alert/export. - -Key Components: UI, API, data adapter, strategy engine, alert/export services. - -Relationships: synchronous analysis, asynchronous side effects. - -Design Decisions: Keep analytical response fast; move non-critical tasks to background. - -NFR Considerations: -- Scalability: alert/export can scale separately. -- Performance: response prioritizes analysis payload. -- Security: permission checks around export and notification endpoints. -- Reliability: retries for failed async tasks. -- Maintainability: workflow contracts versioned via API schema. - -Trade-offs: eventual consistency for async outputs. - -Risks and Mitigations: -- Risk: duplicate alerts after retries. Mitigation: idempotency keys by event hash. - -## Additional Diagram: Domain State Model -```mermaid -stateDiagram-v2 - [*] --> Neutral - Neutral --> Bullish: 2 consecutive real_bull - Neutral --> Bearish: 2 consecutive real_bear - Bullish --> Bullish: fake OR single real_bear fluke - Bearish --> Bearish: fake OR single real_bull fluke - Bullish --> Bearish: 2 consecutive real_bear - Bearish --> Bullish: 2 consecutive real_bull -``` - -Overview: Strategy state transitions based on confirmed real-bar sequences. - -Key Components: Neutral, Bullish, Bearish states; confirmation conditions. - -Relationships: fake bars never reverse state; opposite single bar is non-reversal noise. - -Design Decisions: enforce confirmation to reduce whipsaw. - -NFR Considerations: -- Scalability: pure function state machine enables easy horizontal compute. -- Performance: O(n) per bar sequence. -- Security: deterministic logic reduces ambiguity and operator error. -- Reliability: explicit transitions avoid hidden side effects. -- Maintainability: state model is test-friendly and auditable. - -Trade-offs: delayed reversals in fast inflection markets. - -Risks and Mitigations: -- Risk: late entries due to confirmation lag. Mitigation: optional “early warning” non-trading signal. - -## Phased Development - -### Phase 1: Initial Implementation -- Single deployable web app with embedded analysis module. -- Basic data adapter, core strategy engine, charting, CSV export. -- Local logs and lightweight metrics. - -### Phase 2+: Final Architecture -- Split API, workers, and dedicated alert/export services. -- Add cache + persistent analysis store + queue-driven async tasks. -- Multi-provider market adapter and hardened observability. - -### Migration Path -1. Extract strategy logic into standalone domain module with unit tests. -2. Introduce API boundary and typed request/response contracts. -3. Externalize side effects (alerts/exports) into worker queue. -4. Add Redis caching and persistent analysis snapshots. -5. Enable multi-environment CI/CD and infrastructure-as-code. - -## Non-Functional Requirements Analysis - -### Scalability -Stateless API/services with autoscaling; async workers for bursty jobs; provider/caching abstraction to reduce upstream load. - -### Performance -Cache-first ingestion, bounded bar windows, O(n) classification/state processing, deferred heavy exports. - -### Security -WAF/TLS, secrets manager, strict request validation, RBAC for admin controls, audit logs for alert/export actions. - -### Reliability -Queue retry policies, dead-letter queues, health probes, circuit breakers for upstream data sources, backup/restore for persistent stores. - -### Maintainability -Layered architecture, clear contracts, domain isolation, test pyramid (unit/contract/integration), observability-first operations. - -## Risks and Mitigations -- Upstream data inconsistency: normalize timestamps and schema at adapter boundary. -- Alert noise: debounce and idempotency keyed by symbol/timeframe/event timestamp. -- Cost growth with scale: autoscaling guardrails, TTL caches, export retention policy. -- Strategy misinterpretation: publish explicit strategy rules and state transition docs in product UI. - -## Technology Stack Recommendations -- Frontend: Streamlit (MVP) then React/Next.js for multi-user production UX. -- API: FastAPI with pydantic contracts. -- Workers: Celery/RQ with Redis or managed queue. -- Storage: PostgreSQL for analysis metadata; object storage for export artifacts. -- Observability: OpenTelemetry + managed logging/metrics dashboards. -- Deployment: Containerized services on managed Kubernetes or serverless containers. - -## Next Steps -1. Approve the phased architecture and target operating model. -2. Define API contracts for analysis request/response and event schema. -3. Implement Phase 1 module boundaries (domain/application/infrastructure). -4. Add core test suite for classification and trend state transitions. -5. Plan Phase 2 service split and infrastructure rollout. diff --git a/mac/src/App/Assets.xcassets/AccentColor.colorset/Contents.json b/mac/src/App/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/mac/src/App/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/mac/src/App/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 64dc11e..0000000 --- a/mac/src/App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon_16x16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "icon_16x16@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "icon_32x32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "icon_32x32@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "icon_128x128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "icon_128x128@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "icon_256x256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "icon_256x256@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "icon_512x512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "icon_512x512@2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128.png deleted file mode 100644 index 2a84246..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png deleted file mode 100644 index bb89624..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16.png deleted file mode 100644 index 57b5d06..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png deleted file mode 100644 index 2ad2af9..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256.png deleted file mode 100644 index bb89624..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png deleted file mode 100644 index 52e1d6b..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32.png deleted file mode 100644 index 2ad2af9..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png deleted file mode 100644 index b8642b7..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512.png deleted file mode 100644 index 52e1d6b..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png deleted file mode 100644 index bb9b5b9..0000000 Binary files a/mac/src/App/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and /dev/null differ diff --git a/mac/src/App/Assets.xcassets/Contents.json b/mac/src/App/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/mac/src/App/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/App/ContentView.swift b/mac/src/App/ContentView.swift deleted file mode 100644 index 3b282d6..0000000 --- a/mac/src/App/ContentView.swift +++ /dev/null @@ -1,867 +0,0 @@ -import SwiftUI -import WebKit -import Foundation -import Observation - -struct ContentView: View { - @State private var host = TraderHost() - @State private var didAutostart = false - @AppStorage("mt_setup_completed") private var setupCompleted = false - @AppStorage("mt_symbol") private var storedSymbol = "AAPL" - @AppStorage("mt_interval") private var storedInterval = "1d" - @AppStorage("mt_period") private var storedPeriod = "6mo" - @AppStorage("mt_max_bars") private var storedMaxBars = 500 - @AppStorage("mt_drop_live") private var storedDropLive = true - @AppStorage("mt_use_body_range") private var storedUseBodyRange = false - @AppStorage("mt_volume_filter_enabled") private var storedVolumeFilterEnabled = false - @AppStorage("mt_volume_sma_window") private var storedVolumeSMAWindow = 20 - @AppStorage("mt_volume_multiplier") private var storedVolumeMultiplier = 1.0 - @AppStorage("mt_gray_fake") private var storedGrayFake = true - @AppStorage("mt_hide_market_closed_gaps") private var storedHideMarketClosedGaps = true - @AppStorage("mt_enable_auto_refresh") private var storedEnableAutoRefresh = false - @AppStorage("mt_refresh_sec") private var storedRefreshSeconds = 60 - @State private var showSetupSheet = false - @State private var showHelpSheet = false - @State private var setupDraft = UserSetupPreferences.default -#if DEBUG - @State private var showDebugPanel = false -#endif - - var body: some View { - VStack(spacing: 0) { - if host.isRunning { - LocalWebView(url: host.serverURL, reloadToken: host.reloadToken) - } else { - launchView - } - -#if DEBUG - debugPanel -#endif - } - .frame(minWidth: 1100, minHeight: 760) - .onAppear { - guard !didAutostart else { return } - didAutostart = true - let normalized = syncHostPreferencesFromSharedSettings() - persistSharedSettingsFile(normalized) - host.start() - - if !setupCompleted { - setupDraft = normalized.setupDefaults - showSetupSheet = true - } - } - .onDisappear { host.stop() } - .toolbar { - ToolbarItemGroup(placement: .primaryAction) { - Button("Help") { - showHelpSheet = true - } - Button("Setup") { - syncStateFromSharedSettingsFileIfAvailable() - setupDraft = storedWebPreferences.normalized().setupDefaults - showSetupSheet = true - } - Button("Reload") { - _ = syncHostPreferencesFromSharedSettings() - host.reloadWebView() - } - .disabled(!host.isRunning) - Button("Restart") { - _ = syncHostPreferencesFromSharedSettings() - host.restart() - } - .disabled(host.isStarting) - } - } - .sheet(isPresented: $showSetupSheet) { - setupSheet - } - .sheet(isPresented: $showHelpSheet) { - HelpSheetView() - } - } - - private var sharedSettingsURL: URL { - FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".manesh_trader", isDirectory: true) - .appendingPathComponent("settings.json", isDirectory: false) - } - - private var storedWebPreferences: WebPreferences { - WebPreferences( - symbol: storedSymbol, - interval: storedInterval, - period: storedPeriod, - maxBars: storedMaxBars, - dropLive: storedDropLive, - useBodyRange: storedUseBodyRange, - volumeFilterEnabled: storedVolumeFilterEnabled, - volumeSMAWindow: storedVolumeSMAWindow, - volumeMultiplier: storedVolumeMultiplier, - grayFake: storedGrayFake, - hideMarketClosedGaps: storedHideMarketClosedGaps, - enableAutoRefresh: storedEnableAutoRefresh, - refreshSeconds: storedRefreshSeconds - ) - } - - private var setupSheet: some View { - NavigationStack { - Form { - Section { - Text("Choose your default market settings so you see useful data immediately on launch.") - .foregroundStyle(.secondary) - } - - Section("Data Defaults") { - TextField("Symbol", text: $setupDraft.symbol) - Text("Ticker or pair, e.g. AAPL, MSFT, BTC-USD.") - .font(.caption) - .foregroundStyle(.secondary) - - Picker("Timeframe", selection: $setupDraft.timeframe) { - ForEach(UserSetupPreferences.timeframeOptions, id: \.self) { option in - Text(option).tag(option) - } - } - Text("Bar size for each candle. `1d` is a good starting point.") - .font(.caption) - .foregroundStyle(.secondary) - - Picker("Period", selection: $setupDraft.period) { - ForEach(UserSetupPreferences.periodOptions, id: \.self) { option in - Text(option).tag(option) - } - } - Text("How much history to load for analysis.") - .font(.caption) - .foregroundStyle(.secondary) - - Stepper(value: $setupDraft.maxBars, in: 20...5000, step: 10) { - Text("Max bars: \(setupDraft.maxBars)") - } - Text("Limits candles loaded for speed and readability.") - .font(.caption) - .foregroundStyle(.secondary) - } - } - .formStyle(.grouped) - .navigationTitle("Welcome to ManeshTrader") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Use Defaults") { - let normalized = setupDraft.normalized() - applySetup(normalized, markCompleted: true) - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Save and Continue") { - let normalized = setupDraft.normalized() - applySetup(normalized, markCompleted: true) - } - } - } - } - .frame(minWidth: 560, minHeight: 460) - } - - private func applySetup(_ preferences: UserSetupPreferences, markCompleted: Bool) { - let normalizedSetup = preferences.normalized() - let mergedPreferences = storedWebPreferences - .applyingSetup(normalizedSetup) - .normalized() - writeWebPreferencesToStorage(mergedPreferences) - persistSharedSettingsFile(mergedPreferences) - if markCompleted { - setupCompleted = true - } - host.applyPreferences(mergedPreferences) - host.reloadWebView() - showSetupSheet = false - } - - private func syncStateFromSharedSettingsFileIfAvailable() { - guard - let data = try? Data(contentsOf: sharedSettingsURL), - let decoded = try? JSONDecoder().decode(WebPreferences.self, from: data) - else { - return - } - - writeWebPreferencesToStorage(decoded.normalized()) - } - - @discardableResult - private func syncHostPreferencesFromSharedSettings() -> WebPreferences { - syncStateFromSharedSettingsFileIfAvailable() - let normalized = storedWebPreferences.normalized() - writeWebPreferencesToStorage(normalized) - host.applyPreferences(normalized) - return normalized - } - - private func writeWebPreferencesToStorage(_ preferences: WebPreferences) { - storedSymbol = preferences.symbol - storedInterval = preferences.interval - storedPeriod = preferences.period - storedMaxBars = preferences.maxBars - storedDropLive = preferences.dropLive - storedUseBodyRange = preferences.useBodyRange - storedVolumeFilterEnabled = preferences.volumeFilterEnabled - storedVolumeSMAWindow = preferences.volumeSMAWindow - storedVolumeMultiplier = preferences.volumeMultiplier - storedGrayFake = preferences.grayFake - storedHideMarketClosedGaps = preferences.hideMarketClosedGaps - storedEnableAutoRefresh = preferences.enableAutoRefresh - storedRefreshSeconds = preferences.refreshSeconds - } - - private func persistSharedSettingsFile(_ preferences: WebPreferences) { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - - do { - let normalized = preferences.normalized() - let directory = sharedSettingsURL.deletingLastPathComponent() - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - let data = try encoder.encode(normalized) - try data.write(to: sharedSettingsURL, options: .atomic) - } catch { - #if DEBUG - print("Failed to persist shared settings: \(error.localizedDescription)") - #endif - } - } - - private var launchView: some View { - VStack(spacing: 14) { - launchCard - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background( - LinearGradient( - colors: [ - Color(nsColor: .windowBackgroundColor), - Color(nsColor: .underPageBackgroundColor), - ], - startPoint: .top, - endPoint: .bottom - ) - ) - } - - @ViewBuilder - private var launchCard: some View { - let card = VStack(spacing: 14) { - Image(systemName: host.launchError == nil ? "chart.line.uptrend.xyaxis.circle.fill" : "exclamationmark.triangle.fill") - .font(.system(size: 42)) - .foregroundStyle(host.launchError == nil ? .green : .orange) - - Text(host.launchError == nil ? "Starting ManeshTrader" : "Couldn’t Start ManeshTrader") - .font(.title3.weight(.semibold)) - - if let launchError = host.launchError { - Text(launchError) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 700) - - if #available(macOS 26.0, *) { - Button("Try Again") { host.start() } - .buttonStyle(.glassProminent) - } else { - Button("Try Again") { host.start() } - .buttonStyle(.borderedProminent) - } - } else { - ProgressView() - .controlSize(.large) - - Text("Loading local engine...") - .foregroundStyle(.secondary) - } - } - .padding(.horizontal, 26) - .padding(.vertical, 22) - - if #available(macOS 26.0, *) { - card.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 24)) - } else { - card.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous)) - } - } - -#if DEBUG - private var debugPanel: some View { - VStack(spacing: 0) { - Divider() - DisclosureGroup("Developer Tools", isExpanded: $showDebugPanel) { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 8) { - Button("Start") { - _ = syncHostPreferencesFromSharedSettings() - host.start() - } - .disabled(host.isRunning || host.isStarting) - Button("Stop") { host.stop() } - .disabled(!host.isRunning && !host.isStarting) - Button("Reload") { - _ = syncHostPreferencesFromSharedSettings() - host.reloadWebView() - } - .disabled(!host.isRunning) - } - - Text(host.status) - .font(.callout) - .foregroundStyle(.secondary) - - Text("Local URL: \(host.serverURL.absoluteString)") - .font(.caption) - .foregroundStyle(.secondary) - - Text("Embedded backend: \(host.backendExecutablePath)") - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - .padding(.top, 6) - } - .padding(.horizontal, 12) - .padding(.vertical, 10) - } - } -#endif -} - -private struct HelpSheetView: View { - @Environment(\.dismiss) private var dismiss - - private var helpFileURL: URL? { - let fm = FileManager.default - let candidates = [ - Bundle.main.url(forResource: "help", withExtension: "html", subdirectory: "Help"), - Bundle.main.url(forResource: "help", withExtension: "html"), - Bundle.main.resourceURL?.appendingPathComponent("Help/help.html"), - Bundle.main.resourceURL?.appendingPathComponent("help.html"), - ].compactMap { $0 } - return candidates.first(where: { fm.fileExists(atPath: $0.path) }) - } - - var body: some View { - NavigationStack { - Group { - if let helpFileURL { - HelpDocumentWebView(fileURL: helpFileURL) - } else { - VStack(alignment: .leading, spacing: 8) { - Text("Help file not found.") - .font(.title3.weight(.semibold)) - Text("Expected bundled file: help.html") - .foregroundStyle(.secondary) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(24) - } - } - .navigationTitle("Help & Quick Start") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } - } - } - } - .frame(minWidth: 860, minHeight: 640) - } -} - -private struct HelpDocumentWebView: NSViewRepresentable { - let fileURL: URL - - func makeNSView(context: Context) -> WKWebView { - let webView = WKWebView(frame: .zero) - webView.allowsBackForwardNavigationGestures = true - webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent()) - return webView - } - - func updateNSView(_ webView: WKWebView, context: Context) { - if webView.url != fileURL { - webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent()) - } - } -} - -#Preview { - ContentView() -} - -private struct UserSetupPreferences { - static let timeframeOptions = ["1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo"] - static let periodOptions = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "max"] - - static let `default` = UserSetupPreferences(symbol: "AAPL", timeframe: "1d", period: "6mo", maxBars: 500) - - var symbol: String - var timeframe: String - var period: String - var maxBars: Int - - func normalized() -> UserSetupPreferences { - let normalizedSymbol = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - let safeSymbol = normalizedSymbol.isEmpty ? "AAPL" : normalizedSymbol - - let safeTimeframe = Self.timeframeOptions.contains(timeframe) ? timeframe : "1d" - let safePeriod = Self.periodOptions.contains(period) ? period : "6mo" - let safeMaxBars = min(5000, max(20, maxBars)) - - return UserSetupPreferences( - symbol: safeSymbol, - timeframe: safeTimeframe, - period: safePeriod, - maxBars: safeMaxBars - ) - } -} - -private struct WebPreferences: Codable { - var symbol: String - var interval: String - var period: String - var maxBars: Int - var dropLive: Bool - var useBodyRange: Bool - var volumeFilterEnabled: Bool - var volumeSMAWindow: Int - var volumeMultiplier: Double - var grayFake: Bool - var hideMarketClosedGaps: Bool - var enableAutoRefresh: Bool - var refreshSeconds: Int - - init( - symbol: String, - interval: String, - period: String, - maxBars: Int, - dropLive: Bool, - useBodyRange: Bool, - volumeFilterEnabled: Bool, - volumeSMAWindow: Int, - volumeMultiplier: Double, - grayFake: Bool, - hideMarketClosedGaps: Bool, - enableAutoRefresh: Bool, - refreshSeconds: Int - ) { - self.symbol = symbol - self.interval = interval - self.period = period - self.maxBars = maxBars - self.dropLive = dropLive - self.useBodyRange = useBodyRange - self.volumeFilterEnabled = volumeFilterEnabled - self.volumeSMAWindow = volumeSMAWindow - self.volumeMultiplier = volumeMultiplier - self.grayFake = grayFake - self.hideMarketClosedGaps = hideMarketClosedGaps - self.enableAutoRefresh = enableAutoRefresh - self.refreshSeconds = refreshSeconds - } - - static let `default` = WebPreferences( - symbol: "AAPL", - interval: "1d", - period: "6mo", - maxBars: 500, - dropLive: true, - useBodyRange: false, - volumeFilterEnabled: false, - volumeSMAWindow: 20, - volumeMultiplier: 1.0, - grayFake: true, - hideMarketClosedGaps: true, - enableAutoRefresh: false, - refreshSeconds: 60 - ) - - private enum CodingKeys: String, CodingKey { - case symbol - case interval - case period - case maxBars = "max_bars" - case dropLive = "drop_live" - case useBodyRange = "use_body_range" - case volumeFilterEnabled = "volume_filter_enabled" - case volumeSMAWindow = "volume_sma_window" - case volumeMultiplier = "volume_multiplier" - case grayFake = "gray_fake" - case hideMarketClosedGaps = "hide_market_closed_gaps" - case enableAutoRefresh = "enable_auto_refresh" - case refreshSeconds = "refresh_sec" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let defaults = Self.default - - symbol = try container.decodeIfPresent(String.self, forKey: .symbol) ?? defaults.symbol - interval = try container.decodeIfPresent(String.self, forKey: .interval) ?? defaults.interval - period = try container.decodeIfPresent(String.self, forKey: .period) ?? defaults.period - maxBars = try container.decodeIfPresent(Int.self, forKey: .maxBars) ?? defaults.maxBars - dropLive = try container.decodeIfPresent(Bool.self, forKey: .dropLive) ?? defaults.dropLive - useBodyRange = try container.decodeIfPresent(Bool.self, forKey: .useBodyRange) ?? defaults.useBodyRange - volumeFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: .volumeFilterEnabled) ?? defaults.volumeFilterEnabled - volumeSMAWindow = try container.decodeIfPresent(Int.self, forKey: .volumeSMAWindow) ?? defaults.volumeSMAWindow - volumeMultiplier = try container.decodeIfPresent(Double.self, forKey: .volumeMultiplier) ?? defaults.volumeMultiplier - grayFake = try container.decodeIfPresent(Bool.self, forKey: .grayFake) ?? defaults.grayFake - hideMarketClosedGaps = try container.decodeIfPresent(Bool.self, forKey: .hideMarketClosedGaps) ?? defaults.hideMarketClosedGaps - enableAutoRefresh = try container.decodeIfPresent(Bool.self, forKey: .enableAutoRefresh) ?? defaults.enableAutoRefresh - refreshSeconds = try container.decodeIfPresent(Int.self, forKey: .refreshSeconds) ?? defaults.refreshSeconds - } - - func normalized() -> WebPreferences { - let safeSymbol = { - let candidate = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - return candidate.isEmpty ? "AAPL" : candidate - }() - - let safeInterval = UserSetupPreferences.timeframeOptions.contains(interval) ? interval : "1d" - let safePeriod = UserSetupPreferences.periodOptions.contains(period) ? period : "6mo" - let safeMaxBars = min(5000, max(20, maxBars)) - let safeVolumeSMAWindow = min(100, max(2, volumeSMAWindow)) - let clampedMultiplier = min(3.0, max(0.1, volumeMultiplier)) - let safeVolumeMultiplier = (clampedMultiplier * 10).rounded() / 10 - let safeRefreshSeconds = min(600, max(10, refreshSeconds)) - - return WebPreferences( - symbol: safeSymbol, - interval: safeInterval, - period: safePeriod, - maxBars: safeMaxBars, - dropLive: dropLive, - useBodyRange: useBodyRange, - volumeFilterEnabled: volumeFilterEnabled, - volumeSMAWindow: safeVolumeSMAWindow, - volumeMultiplier: safeVolumeMultiplier, - grayFake: grayFake, - hideMarketClosedGaps: hideMarketClosedGaps, - enableAutoRefresh: enableAutoRefresh, - refreshSeconds: safeRefreshSeconds - ) - } - - var setupDefaults: UserSetupPreferences { - UserSetupPreferences( - symbol: symbol, - timeframe: interval, - period: period, - maxBars: maxBars - ).normalized() - } - - func applyingSetup(_ setup: UserSetupPreferences) -> WebPreferences { - let normalizedSetup = setup.normalized() - return WebPreferences( - symbol: normalizedSetup.symbol, - interval: normalizedSetup.timeframe, - period: normalizedSetup.period, - maxBars: normalizedSetup.maxBars, - dropLive: dropLive, - useBodyRange: useBodyRange, - volumeFilterEnabled: volumeFilterEnabled, - volumeSMAWindow: volumeSMAWindow, - volumeMultiplier: volumeMultiplier, - grayFake: grayFake, - hideMarketClosedGaps: hideMarketClosedGaps, - enableAutoRefresh: enableAutoRefresh, - refreshSeconds: refreshSeconds - ) - } - - var queryItems: [URLQueryItem] { - [ - URLQueryItem(name: "symbol", value: symbol), - URLQueryItem(name: "interval", value: interval), - URLQueryItem(name: "period", value: period), - URLQueryItem(name: "max_bars", value: String(maxBars)), - URLQueryItem(name: "drop_live", value: String(dropLive)), - URLQueryItem(name: "use_body_range", value: String(useBodyRange)), - URLQueryItem(name: "volume_filter_enabled", value: String(volumeFilterEnabled)), - URLQueryItem(name: "volume_sma_window", value: String(volumeSMAWindow)), - URLQueryItem(name: "volume_multiplier", value: String(volumeMultiplier)), - URLQueryItem(name: "gray_fake", value: String(grayFake)), - URLQueryItem(name: "hide_market_closed_gaps", value: String(hideMarketClosedGaps)), - URLQueryItem(name: "enable_auto_refresh", value: String(enableAutoRefresh)), - URLQueryItem(name: "refresh_sec", value: String(refreshSeconds)), - ] - } -} - -private struct LocalWebView: NSViewRepresentable { - let url: URL - let reloadToken: Int - - func makeNSView(context: Context) -> WKWebView { - let webView = WKWebView(frame: .zero) - webView.customUserAgent = "ManeshTraderMac" - webView.allowsBackForwardNavigationGestures = false - webView.navigationDelegate = context.coordinator - context.coordinator.attach(webView) - webView.load(URLRequest(url: url)) - return webView - } - - func updateNSView(_ webView: WKWebView, context: Context) { - if context.coordinator.lastReloadToken != reloadToken || webView.url != url { - context.coordinator.lastReloadToken = reloadToken - webView.load(URLRequest(url: url)) - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(url: url, reloadToken: reloadToken) - } - - final class Coordinator: NSObject, WKNavigationDelegate { - private let url: URL - private weak var webView: WKWebView? - private var pendingRetry: DispatchWorkItem? - var lastReloadToken: Int - - init(url: URL, reloadToken: Int) { - self.url = url - self.lastReloadToken = reloadToken - } - - func attach(_ webView: WKWebView) { - self.webView = webView - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - pendingRetry?.cancel() - pendingRetry = nil - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - scheduleRetry() - } - - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - scheduleRetry() - } - - private func scheduleRetry() { - pendingRetry?.cancel() - let retry = DispatchWorkItem { [weak self] in - guard let self, let webView = self.webView else { return } - webView.load(URLRequest(url: self.url)) - } - pendingRetry = retry - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: retry) - } - } -} - -@Observable -@MainActor -private final class TraderHost { - var isRunning = false - var isStarting = false - var launchError: String? - var status = "Preparing backend..." - var backendExecutablePath = "" - var reloadToken = 0 - var serverPort = 8501 - var webQueryItems: [URLQueryItem] = [] - var serverURL: URL { - var components = URLComponents() - components.scheme = "http" - components.host = "127.0.0.1" - components.port = serverPort - components.queryItems = webQueryItems.isEmpty ? nil : webQueryItems - return components.url! - } - - private var process: Process? - private var outputPipe: Pipe? - private var latestBackendLogLine = "" - private var lastSignificantBackendLogLine = "" - private var didRequestStop = false - - func start() { - guard process == nil else { return } - - isStarting = true - launchError = nil - status = "Starting local engine..." - didRequestStop = false - - guard let executableURL = bundledBackendExecutableURL() else { - isStarting = false - launchError = "Required backend files were not found in the app bundle." - status = "Bundled backend executable not found." - return - } - backendExecutablePath = executableURL.path - - do { - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executableURL.path) - } catch { - status = "Could not set executable permissions: \(error.localizedDescription)" - } - - let p = Process() - latestBackendLogLine = "" - lastSignificantBackendLogLine = "" - serverPort = nextAvailablePort(preferred: 8501) - p.currentDirectoryURL = executableURL.deletingLastPathComponent() - p.executableURL = executableURL - var environment = ProcessInfo.processInfo.environment - environment["MANESH_TRADER_PORT"] = "\(serverPort)" - p.environment = environment - - let pipe = Pipe() - outputPipe = pipe - p.standardOutput = pipe - p.standardError = pipe - pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in - let data = handle.availableData - guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return } - let lastLine = text - .split(whereSeparator: \.isNewline) - .last - .map(String.init)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !lastLine.isEmpty else { return } - DispatchQueue.main.async { - self?.latestBackendLogLine = lastLine - if !lastLine.contains("LOADER: failed to destroy sync semaphore") { - self?.lastSignificantBackendLogLine = lastLine - } - } - } - - p.terminationHandler = { [weak self] proc in - DispatchQueue.main.async { - guard let self else { return } - self.isRunning = false - self.isStarting = false - self.process = nil - self.outputPipe?.fileHandleForReading.readabilityHandler = nil - self.outputPipe = nil - - if self.didRequestStop { - self.status = "Stopped." - return - } - - if let line = self.lastSignificantBackendLogLine.isEmpty ? nil : self.lastSignificantBackendLogLine { - self.status = "Stopped (exit \(proc.terminationStatus)): \(line)" - } else if let line = self.latestBackendLogLine.isEmpty ? nil : self.latestBackendLogLine { - self.status = "Stopped (exit \(proc.terminationStatus)): \(line)" - } else { - self.status = "Stopped (exit \(proc.terminationStatus))." - } - - if proc.terminationStatus != 0 { - self.launchError = "The local engine stopped unexpectedly. Please try again." - } - } - } - - do { - try p.run() - process = p - isRunning = true - isStarting = false - reloadToken += 1 - status = "Running locally in-app at \(serverURL.absoluteString)" - } catch { - status = "Failed to start backend: \(error.localizedDescription)" - process = nil - outputPipe?.fileHandleForReading.readabilityHandler = nil - outputPipe = nil - isRunning = false - isStarting = false - launchError = "Unable to start the local engine. Please try again." - } - } - - func stop() { - guard let process else { return } - didRequestStop = true - isStarting = false - if process.isRunning { - process.terminate() - } - outputPipe?.fileHandleForReading.readabilityHandler = nil - outputPipe = nil - self.process = nil - isRunning = false - status = "Stopped." - } - - func reloadWebView() { - guard isRunning else { return } - reloadToken += 1 - } - - func applyPreferences(_ preferences: WebPreferences) { - webQueryItems = preferences.normalized().queryItems - } - - func restart() { - stop() - start() - } - - private func bundledBackendExecutableURL() -> URL? { - let fm = FileManager.default - let candidates = [ - Bundle.main.url(forResource: "WebBackend", withExtension: nil, subdirectory: "EmbeddedBackend"), - Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/WebBackend"), - Bundle.main.url(forResource: "WebBackend", withExtension: nil), - Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil, subdirectory: "EmbeddedBackend"), - Bundle.main.resourceURL?.appendingPathComponent("EmbeddedBackend/ManeshTraderBackend"), - Bundle.main.url(forResource: "ManeshTraderBackend", withExtension: nil), - ].compactMap { $0 } - - return candidates.first(where: { fm.fileExists(atPath: $0.path) }) - } - - private func nextAvailablePort(preferred: Int) -> Int { - if canBindToLocalPort(preferred) { - return preferred - } - - for candidate in 8502...8600 where canBindToLocalPort(candidate) { - return candidate - } - return preferred - } - - private func canBindToLocalPort(_ port: Int) -> Bool { - let fd = socket(AF_INET, SOCK_STREAM, 0) - guard fd >= 0 else { return false } - defer { close(fd) } - - var value: Int32 = 1 - _ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout.size)) - - var addr = sockaddr_in() - addr.sin_len = UInt8(MemoryLayout.stride) - addr.sin_family = sa_family_t(AF_INET) - addr.sin_port = in_port_t(UInt16(port).bigEndian) - addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1")) - - return withUnsafePointer(to: &addr) { pointer in - pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in - bind(fd, sockPtr, socklen_t(MemoryLayout.stride)) == 0 - } - } - } -} diff --git a/mac/src/App/EmbeddedBackend/README.md b/mac/src/App/EmbeddedBackend/README.md deleted file mode 100644 index c011b39..0000000 --- a/mac/src/App/EmbeddedBackend/README.md +++ /dev/null @@ -1,6 +0,0 @@ -This folder is populated by `scripts/build_embedded_backend.sh`. - -- Output binary: `WebBackend` -- Source of truth for backend code: `web/src/` (`app.py`, `web_core/`) - -Do not hand-edit generated binaries in this folder. diff --git a/mac/src/App/EmbeddedBackend/WebBackend b/mac/src/App/EmbeddedBackend/WebBackend deleted file mode 100755 index fa56c7e..0000000 Binary files a/mac/src/App/EmbeddedBackend/WebBackend and /dev/null differ diff --git a/mac/src/App/Help/help.html b/mac/src/App/Help/help.html deleted file mode 100644 index 027daa5..0000000 --- a/mac/src/App/Help/help.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - Help & Quick Start - - - -
-

Help & Quick Start

-

A quick guide to reading signals, choosing settings, and troubleshooting.

- -
-

Start in 60 Seconds

-
    -
  1. Set a symbol like AAPL or BTC-USD.
  2. -
  3. Choose Timeframe (1d is a good default) and Period (6mo).
  4. -
  5. Keep Ignore potentially live last bar enabled.
  6. -
  7. Review trend status and chart markers.
  8. -
  9. Use Export to download CSV/PDF outputs.
  10. -
-
- -
-

Signal Rules

-

- real_bull close above previous high - real_bear close below previous low - fake close inside previous range -

-
    -
  • Trend starts after 2 consecutive real bars in the same direction.
  • -
  • Trend reverses only after 2 consecutive opposite real bars.
  • -
  • Fake bars are noise and do not reverse trend.
  • -
-
- -
-

Data Settings

-

Core fields

-
    -
  • Symbol: ticker or pair, e.g. AAPL, MSFT, BTC-USD.
  • -
  • Timeframe: candle size. Start with 1d for cleaner swings.
  • -
  • Period: amount of history to load. Start with 6mo.
  • -
  • Max bars: limits loaded candles for speed and chart readability.
  • -
-

Optional filters

-
    -
  • Use previous body range: ignores wick-only breakouts.
  • -
  • Enable volume filter: treats low-volume bars as fake.
  • -
  • Hide market-closed gaps: recommended ON for stocks.
  • -
  • Enable auto-refresh: useful for live monitoring only.
  • -
-
- -
-

Chart Reading

-
    -
  • Green triangle-up markers show real_bull bars.
  • -
  • Red triangle-down markers show real_bear bars.
  • -
  • Gray candles (if enabled) de-emphasize fake/noise bars.
  • -
  • Volume bars are color-coded by trend state.
  • -
-
- -
-

Troubleshooting

-
    -
  • If no data appears, verify ticker format (for example BTC-USD, not BTCUSD).
  • -
  • If results look noisy, switch to 1d and reduce optional filters.
  • -
  • If trend seems delayed, remember trend transitions require two real bars.
  • -
  • This tool is analysis-only and does not place trades.
  • -
-
-
- - diff --git a/mac/src/App/ManeshTraderMacApp.swift b/mac/src/App/ManeshTraderMacApp.swift deleted file mode 100644 index 935bc22..0000000 --- a/mac/src/App/ManeshTraderMacApp.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ManeshTraderMacApp.swift -// ManeshTraderMac -// -// Created by Matt Bruce on 2/13/26. -// - -import SwiftUI - -@main -struct ManeshTraderMacApp: App { - var body: some Scene { - WindowGroup { - ContentView() - .navigationTitle("ManeshTrader") - } - } -} diff --git a/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json b/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index dfa108c..0000000 --- a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon-20@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon-20@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "icon-29@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon-29@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "icon-40@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon-40@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "icon-60@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "icon-60@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "icon-20@1x-ipad.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "icon-20@2x-ipad.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon-29@1x-ipad.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "icon-29@2x-ipad.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon-40@1x-ipad.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "icon-40@2x-ipad.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon-76@2x-ipad.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "icon-83.5@2x-ipad.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "icon-1024-marketing.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png deleted file mode 100644 index bb9b5b9..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png deleted file mode 100644 index 466520a..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png deleted file mode 100644 index e40a4f1..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png deleted file mode 100644 index e40a4f1..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png deleted file mode 100644 index 2b8117a..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png deleted file mode 100644 index b988d74..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png deleted file mode 100644 index f25b929..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png deleted file mode 100644 index f25b929..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png deleted file mode 100644 index 8230a77..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png deleted file mode 100644 index e40a4f1..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png deleted file mode 100644 index 649c20b..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png deleted file mode 100644 index 649c20b..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png deleted file mode 100644 index df31be1..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png deleted file mode 100644 index df31be1..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png deleted file mode 100644 index 2f58cc3..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png deleted file mode 100644 index 7f79f23..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png deleted file mode 100644 index b824e4e..0000000 Binary files a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png and /dev/null differ diff --git a/mac/src/AppMobile/Assets.xcassets/Contents.json b/mac/src/AppMobile/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/mac/src/AppMobile/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mac/src/AppMobile/Help/help.html b/mac/src/AppMobile/Help/help.html deleted file mode 100644 index 027daa5..0000000 --- a/mac/src/AppMobile/Help/help.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - Help & Quick Start - - - -
-

Help & Quick Start

-

A quick guide to reading signals, choosing settings, and troubleshooting.

- -
-

Start in 60 Seconds

-
    -
  1. Set a symbol like AAPL or BTC-USD.
  2. -
  3. Choose Timeframe (1d is a good default) and Period (6mo).
  4. -
  5. Keep Ignore potentially live last bar enabled.
  6. -
  7. Review trend status and chart markers.
  8. -
  9. Use Export to download CSV/PDF outputs.
  10. -
-
- -
-

Signal Rules

-

- real_bull close above previous high - real_bear close below previous low - fake close inside previous range -

-
    -
  • Trend starts after 2 consecutive real bars in the same direction.
  • -
  • Trend reverses only after 2 consecutive opposite real bars.
  • -
  • Fake bars are noise and do not reverse trend.
  • -
-
- -
-

Data Settings

-

Core fields

-
    -
  • Symbol: ticker or pair, e.g. AAPL, MSFT, BTC-USD.
  • -
  • Timeframe: candle size. Start with 1d for cleaner swings.
  • -
  • Period: amount of history to load. Start with 6mo.
  • -
  • Max bars: limits loaded candles for speed and chart readability.
  • -
-

Optional filters

-
    -
  • Use previous body range: ignores wick-only breakouts.
  • -
  • Enable volume filter: treats low-volume bars as fake.
  • -
  • Hide market-closed gaps: recommended ON for stocks.
  • -
  • Enable auto-refresh: useful for live monitoring only.
  • -
-
- -
-

Chart Reading

-
    -
  • Green triangle-up markers show real_bull bars.
  • -
  • Red triangle-down markers show real_bear bars.
  • -
  • Gray candles (if enabled) de-emphasize fake/noise bars.
  • -
  • Volume bars are color-coded by trend state.
  • -
-
- -
-

Troubleshooting

-
    -
  • If no data appears, verify ticker format (for example BTC-USD, not BTCUSD).
  • -
  • If results look noisy, switch to 1d and reduce optional filters.
  • -
  • If trend seems delayed, remember trend transitions require two real bars.
  • -
  • This tool is analysis-only and does not place trades.
  • -
-
-
- - diff --git a/mac/src/AppMobile/ManeshTraderMobileApp.swift b/mac/src/AppMobile/ManeshTraderMobileApp.swift deleted file mode 100644 index 153c164..0000000 --- a/mac/src/AppMobile/ManeshTraderMobileApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct ManeshTraderMobileApp: App { - var body: some Scene { - WindowGroup { - MobileContentView() - } - } -} diff --git a/mac/src/AppMobile/MobileContentView.swift b/mac/src/AppMobile/MobileContentView.swift deleted file mode 100644 index 2578b00..0000000 --- a/mac/src/AppMobile/MobileContentView.swift +++ /dev/null @@ -1,615 +0,0 @@ -import SwiftUI -import WebKit -import Foundation -import Observation - -struct MobileContentView: View { - @State private var host = MobileTraderHost() - @State private var didAutostart = false - @AppStorage("mt_mobile_setup_completed") private var setupCompleted = false - @AppStorage("mt_mobile_backend_url") private var storedBackendURL = "http://127.0.0.1:8501" - @AppStorage("mt_mobile_symbol") private var storedSymbol = "AAPL" - @AppStorage("mt_mobile_interval") private var storedInterval = "1d" - @AppStorage("mt_mobile_period") private var storedPeriod = "6mo" - @AppStorage("mt_mobile_max_bars") private var storedMaxBars = 500 - @AppStorage("mt_mobile_drop_live") private var storedDropLive = true - @AppStorage("mt_mobile_use_body_range") private var storedUseBodyRange = false - @AppStorage("mt_mobile_volume_filter_enabled") private var storedVolumeFilterEnabled = false - @AppStorage("mt_mobile_volume_sma_window") private var storedVolumeSMAWindow = 20 - @AppStorage("mt_mobile_volume_multiplier") private var storedVolumeMultiplier = 1.0 - @AppStorage("mt_mobile_gray_fake") private var storedGrayFake = true - @AppStorage("mt_mobile_hide_market_closed_gaps") private var storedHideMarketClosedGaps = true - @AppStorage("mt_mobile_enable_auto_refresh") private var storedEnableAutoRefresh = false - @AppStorage("mt_mobile_refresh_sec") private var storedRefreshSeconds = 60 - - @State private var showSetupSheet = false - @State private var showHelpSheet = false - @State private var setupDraft = MobileUserSetupPreferences.default - @State private var setupBackendURL = "http://127.0.0.1:8501" - - var body: some View { - NavigationStack { - Group { - if let url = host.serverURL { - MobileWebView(url: url, reloadToken: host.reloadToken) - } else { - launchView - } - } - .navigationTitle("ManeshTrader") - .toolbar { - ToolbarItemGroup(placement: .topBarTrailing) { - Button("Help") { - showHelpSheet = true - } - - Button("Setup") { - setupDraft = storedWebPreferences.normalized().setupDefaults - setupBackendURL = storedBackendURL - showSetupSheet = true - } - - Button("Reload") { - _ = syncHostPreferencesFromStorage() - host.reloadWebView() - } - .disabled(host.serverURL == nil) - } - } - } - .onAppear { - guard !didAutostart else { return } - didAutostart = true - - let normalized = syncHostPreferencesFromStorage() - if !setupCompleted { - setupDraft = normalized.setupDefaults - setupBackendURL = storedBackendURL - showSetupSheet = true - } - } - .sheet(isPresented: $showSetupSheet) { - setupSheet - } - .sheet(isPresented: $showHelpSheet) { - MobileHelpSheetView() - } - } - - private var storedWebPreferences: MobileWebPreferences { - MobileWebPreferences( - symbol: storedSymbol, - interval: storedInterval, - period: storedPeriod, - maxBars: storedMaxBars, - dropLive: storedDropLive, - useBodyRange: storedUseBodyRange, - volumeFilterEnabled: storedVolumeFilterEnabled, - volumeSMAWindow: storedVolumeSMAWindow, - volumeMultiplier: storedVolumeMultiplier, - grayFake: storedGrayFake, - hideMarketClosedGaps: storedHideMarketClosedGaps, - enableAutoRefresh: storedEnableAutoRefresh, - refreshSeconds: storedRefreshSeconds - ) - } - - private var setupSheet: some View { - NavigationStack { - Form { - Section { - Text("Connect this app to a reachable ManeshTrader web backend. iOS cannot run the embedded backend executable directly.") - .foregroundStyle(.secondary) - } - - Section("Backend") { - TextField("Base URL", text: $setupBackendURL) - .textInputAutocapitalization(.never) - .autocorrectionDisabled(true) - .keyboardType(.URL) - Text("Examples: http://192.168.1.10:8501 or https://your-host") - .font(.caption) - .foregroundStyle(.secondary) - } - - Section("Data Defaults") { - TextField("Symbol", text: $setupDraft.symbol) - .textInputAutocapitalization(.characters) - .autocorrectionDisabled(true) - - Picker("Timeframe", selection: $setupDraft.timeframe) { - ForEach(MobileUserSetupPreferences.timeframeOptions, id: \.self) { option in - Text(option).tag(option) - } - } - - Picker("Period", selection: $setupDraft.period) { - ForEach(MobileUserSetupPreferences.periodOptions, id: \.self) { option in - Text(option).tag(option) - } - } - - Stepper(value: $setupDraft.maxBars, in: 20...5000, step: 10) { - Text("Max bars: \(setupDraft.maxBars)") - } - } - - if let validationMessage = host.validationMessage { - Section { - Text(validationMessage) - .foregroundStyle(.red) - .font(.footnote) - } - } - } - .navigationTitle("Setup") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Use Defaults") { - applySetup(MobileUserSetupPreferences.default, backendURL: setupBackendURL, markCompleted: true) - } - } - - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - applySetup(setupDraft.normalized(), backendURL: setupBackendURL, markCompleted: true) - } - } - } - } - } - - private func applySetup(_ preferences: MobileUserSetupPreferences, backendURL: String, markCompleted: Bool) { - let normalizedSetup = preferences.normalized() - let mergedPreferences = storedWebPreferences - .applyingSetup(normalizedSetup) - .normalized() - - storedBackendURL = backendURL.trimmingCharacters(in: .whitespacesAndNewlines) - writeWebPreferencesToStorage(mergedPreferences) - - host.applyPreferences(mergedPreferences, backendURLString: storedBackendURL) - host.reloadWebView() - - if markCompleted { - setupCompleted = true - } - - if host.serverURL != nil { - showSetupSheet = false - } - } - - @discardableResult - private func syncHostPreferencesFromStorage() -> MobileWebPreferences { - let normalized = storedWebPreferences.normalized() - writeWebPreferencesToStorage(normalized) - host.applyPreferences(normalized, backendURLString: storedBackendURL) - return normalized - } - - private func writeWebPreferencesToStorage(_ preferences: MobileWebPreferences) { - storedSymbol = preferences.symbol - storedInterval = preferences.interval - storedPeriod = preferences.period - storedMaxBars = preferences.maxBars - storedDropLive = preferences.dropLive - storedUseBodyRange = preferences.useBodyRange - storedVolumeFilterEnabled = preferences.volumeFilterEnabled - storedVolumeSMAWindow = preferences.volumeSMAWindow - storedVolumeMultiplier = preferences.volumeMultiplier - storedGrayFake = preferences.grayFake - storedHideMarketClosedGaps = preferences.hideMarketClosedGaps - storedEnableAutoRefresh = preferences.enableAutoRefresh - storedRefreshSeconds = preferences.refreshSeconds - } - - private var launchView: some View { - VStack(spacing: 14) { - Image(systemName: "network.badge.shield.half.filled") - .font(.system(size: 42)) - .foregroundStyle(.blue) - - Text("Connect ManeshTrader") - .font(.title3.weight(.semibold)) - - Text("Set a backend URL reachable from this device, then load the web app in place.") - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 460) - - Button("Open Setup") { - setupDraft = storedWebPreferences.normalized().setupDefaults - setupBackendURL = storedBackendURL - showSetupSheet = true - } - .buttonStyle(.borderedProminent) - - if let validationMessage = host.validationMessage { - Text(validationMessage) - .font(.footnote) - .foregroundStyle(.red) - .multilineTextAlignment(.center) - .frame(maxWidth: 460) - } - } - .padding(20) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background( - LinearGradient( - colors: [ - Color(uiColor: .systemBackground), - Color(uiColor: .secondarySystemBackground), - ], - startPoint: .top, - endPoint: .bottom - ) - ) - } -} - -private struct MobileHelpSheetView: View { - @Environment(\.dismiss) private var dismiss - - private var helpFileURL: URL? { - let fm = FileManager.default - let candidates = [ - Bundle.main.url(forResource: "help", withExtension: "html", subdirectory: "Help"), - Bundle.main.url(forResource: "help", withExtension: "html"), - Bundle.main.resourceURL?.appendingPathComponent("Help/help.html"), - Bundle.main.resourceURL?.appendingPathComponent("help.html"), - ].compactMap { $0 } - - return candidates.first(where: { fm.fileExists(atPath: $0.path) }) - } - - var body: some View { - NavigationStack { - Group { - if let helpFileURL { - MobileHelpDocumentWebView(fileURL: helpFileURL) - } else { - VStack(alignment: .leading, spacing: 8) { - Text("Help file not found.") - .font(.title3.weight(.semibold)) - Text("Expected bundled file: help.html") - .foregroundStyle(.secondary) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(24) - } - } - .navigationTitle("Help & Quick Start") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { dismiss() } - } - } - } - } -} - -private struct MobileHelpDocumentWebView: UIViewRepresentable { - let fileURL: URL - - func makeUIView(context: Context) -> WKWebView { - let webView = WKWebView(frame: .zero) - webView.allowsBackForwardNavigationGestures = true - webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent()) - return webView - } - - func updateUIView(_ webView: WKWebView, context: Context) { - if webView.url != fileURL { - webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent()) - } - } -} - -private struct MobileWebView: UIViewRepresentable { - let url: URL - let reloadToken: Int - - func makeUIView(context: Context) -> WKWebView { - let webView = WKWebView(frame: .zero) - webView.customUserAgent = "ManeshTraderMobile" - webView.allowsBackForwardNavigationGestures = true - webView.navigationDelegate = context.coordinator - context.coordinator.attach(webView) - webView.load(URLRequest(url: url)) - return webView - } - - func updateUIView(_ webView: WKWebView, context: Context) { - if context.coordinator.lastReloadToken != reloadToken || webView.url != url { - context.coordinator.lastReloadToken = reloadToken - webView.load(URLRequest(url: url)) - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(url: url, reloadToken: reloadToken) - } - - final class Coordinator: NSObject, WKNavigationDelegate { - private let url: URL - private weak var webView: WKWebView? - private var pendingRetry: DispatchWorkItem? - var lastReloadToken: Int - - init(url: URL, reloadToken: Int) { - self.url = url - self.lastReloadToken = reloadToken - } - - func attach(_ webView: WKWebView) { - self.webView = webView - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - pendingRetry?.cancel() - pendingRetry = nil - } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - scheduleRetry() - } - - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - scheduleRetry() - } - - private func scheduleRetry() { - pendingRetry?.cancel() - let retry = DispatchWorkItem { [weak self] in - guard let self, let webView = self.webView else { return } - webView.load(URLRequest(url: self.url)) - } - pendingRetry = retry - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: retry) - } - } -} - -@Observable -@MainActor -private final class MobileTraderHost { - var reloadToken = 0 - var validationMessage: String? - var webQueryItems: [URLQueryItem] = [] - var backendURLString = "" - - var serverURL: URL? { - guard var components = URLComponents(string: backendURLString), - let scheme = components.scheme?.lowercased(), - ["http", "https"].contains(scheme), - components.host != nil else { - return nil - } - - components.queryItems = webQueryItems.isEmpty ? nil : webQueryItems - return components.url - } - - func applyPreferences(_ preferences: MobileWebPreferences, backendURLString: String) { - self.backendURLString = backendURLString.trimmingCharacters(in: .whitespacesAndNewlines) - webQueryItems = preferences.normalized().queryItems - - if serverURL == nil { - validationMessage = "Enter a valid backend URL with http:// or https:// and a host." - } else { - validationMessage = nil - } - } - - func reloadWebView() { - guard serverURL != nil else { return } - reloadToken += 1 - } -} - -private struct MobileUserSetupPreferences { - static let timeframeOptions = ["1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo"] - static let periodOptions = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "max"] - - static let `default` = MobileUserSetupPreferences(symbol: "AAPL", timeframe: "1d", period: "6mo", maxBars: 500) - - var symbol: String - var timeframe: String - var period: String - var maxBars: Int - - func normalized() -> MobileUserSetupPreferences { - let normalizedSymbol = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - let safeSymbol = normalizedSymbol.isEmpty ? "AAPL" : normalizedSymbol - - let safeTimeframe = Self.timeframeOptions.contains(timeframe) ? timeframe : "1d" - let safePeriod = Self.periodOptions.contains(period) ? period : "6mo" - let safeMaxBars = min(5000, max(20, maxBars)) - - return MobileUserSetupPreferences( - symbol: safeSymbol, - timeframe: safeTimeframe, - period: safePeriod, - maxBars: safeMaxBars - ) - } -} - -private struct MobileWebPreferences: Codable { - var symbol: String - var interval: String - var period: String - var maxBars: Int - var dropLive: Bool - var useBodyRange: Bool - var volumeFilterEnabled: Bool - var volumeSMAWindow: Int - var volumeMultiplier: Double - var grayFake: Bool - var hideMarketClosedGaps: Bool - var enableAutoRefresh: Bool - var refreshSeconds: Int - - init( - symbol: String, - interval: String, - period: String, - maxBars: Int, - dropLive: Bool, - useBodyRange: Bool, - volumeFilterEnabled: Bool, - volumeSMAWindow: Int, - volumeMultiplier: Double, - grayFake: Bool, - hideMarketClosedGaps: Bool, - enableAutoRefresh: Bool, - refreshSeconds: Int - ) { - self.symbol = symbol - self.interval = interval - self.period = period - self.maxBars = maxBars - self.dropLive = dropLive - self.useBodyRange = useBodyRange - self.volumeFilterEnabled = volumeFilterEnabled - self.volumeSMAWindow = volumeSMAWindow - self.volumeMultiplier = volumeMultiplier - self.grayFake = grayFake - self.hideMarketClosedGaps = hideMarketClosedGaps - self.enableAutoRefresh = enableAutoRefresh - self.refreshSeconds = refreshSeconds - } - - static let `default` = MobileWebPreferences( - symbol: "AAPL", - interval: "1d", - period: "6mo", - maxBars: 500, - dropLive: true, - useBodyRange: false, - volumeFilterEnabled: false, - volumeSMAWindow: 20, - volumeMultiplier: 1.0, - grayFake: true, - hideMarketClosedGaps: true, - enableAutoRefresh: false, - refreshSeconds: 60 - ) - - private enum CodingKeys: String, CodingKey { - case symbol - case interval - case period - case maxBars = "max_bars" - case dropLive = "drop_live" - case useBodyRange = "use_body_range" - case volumeFilterEnabled = "volume_filter_enabled" - case volumeSMAWindow = "volume_sma_window" - case volumeMultiplier = "volume_multiplier" - case grayFake = "gray_fake" - case hideMarketClosedGaps = "hide_market_closed_gaps" - case enableAutoRefresh = "enable_auto_refresh" - case refreshSeconds = "refresh_sec" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let defaults = Self.default - - symbol = try container.decodeIfPresent(String.self, forKey: .symbol) ?? defaults.symbol - interval = try container.decodeIfPresent(String.self, forKey: .interval) ?? defaults.interval - period = try container.decodeIfPresent(String.self, forKey: .period) ?? defaults.period - maxBars = try container.decodeIfPresent(Int.self, forKey: .maxBars) ?? defaults.maxBars - dropLive = try container.decodeIfPresent(Bool.self, forKey: .dropLive) ?? defaults.dropLive - useBodyRange = try container.decodeIfPresent(Bool.self, forKey: .useBodyRange) ?? defaults.useBodyRange - volumeFilterEnabled = try container.decodeIfPresent(Bool.self, forKey: .volumeFilterEnabled) ?? defaults.volumeFilterEnabled - volumeSMAWindow = try container.decodeIfPresent(Int.self, forKey: .volumeSMAWindow) ?? defaults.volumeSMAWindow - volumeMultiplier = try container.decodeIfPresent(Double.self, forKey: .volumeMultiplier) ?? defaults.volumeMultiplier - grayFake = try container.decodeIfPresent(Bool.self, forKey: .grayFake) ?? defaults.grayFake - hideMarketClosedGaps = try container.decodeIfPresent(Bool.self, forKey: .hideMarketClosedGaps) ?? defaults.hideMarketClosedGaps - enableAutoRefresh = try container.decodeIfPresent(Bool.self, forKey: .enableAutoRefresh) ?? defaults.enableAutoRefresh - refreshSeconds = try container.decodeIfPresent(Int.self, forKey: .refreshSeconds) ?? defaults.refreshSeconds - } - - func normalized() -> MobileWebPreferences { - let safeSymbol = { - let candidate = symbol.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - return candidate.isEmpty ? "AAPL" : candidate - }() - - let safeInterval = MobileUserSetupPreferences.timeframeOptions.contains(interval) ? interval : "1d" - let safePeriod = MobileUserSetupPreferences.periodOptions.contains(period) ? period : "6mo" - let safeMaxBars = min(5000, max(20, maxBars)) - let safeVolumeSMAWindow = min(100, max(2, volumeSMAWindow)) - let clampedMultiplier = min(3.0, max(0.1, volumeMultiplier)) - let safeVolumeMultiplier = (clampedMultiplier * 10).rounded() / 10 - let safeRefreshSeconds = min(600, max(10, refreshSeconds)) - - return MobileWebPreferences( - symbol: safeSymbol, - interval: safeInterval, - period: safePeriod, - maxBars: safeMaxBars, - dropLive: dropLive, - useBodyRange: useBodyRange, - volumeFilterEnabled: volumeFilterEnabled, - volumeSMAWindow: safeVolumeSMAWindow, - volumeMultiplier: safeVolumeMultiplier, - grayFake: grayFake, - hideMarketClosedGaps: hideMarketClosedGaps, - enableAutoRefresh: enableAutoRefresh, - refreshSeconds: safeRefreshSeconds - ) - } - - var setupDefaults: MobileUserSetupPreferences { - MobileUserSetupPreferences( - symbol: symbol, - timeframe: interval, - period: period, - maxBars: maxBars - ).normalized() - } - - func applyingSetup(_ setup: MobileUserSetupPreferences) -> MobileWebPreferences { - let normalizedSetup = setup.normalized() - return MobileWebPreferences( - symbol: normalizedSetup.symbol, - interval: normalizedSetup.timeframe, - period: normalizedSetup.period, - maxBars: normalizedSetup.maxBars, - dropLive: dropLive, - useBodyRange: useBodyRange, - volumeFilterEnabled: volumeFilterEnabled, - volumeSMAWindow: volumeSMAWindow, - volumeMultiplier: volumeMultiplier, - grayFake: grayFake, - hideMarketClosedGaps: hideMarketClosedGaps, - enableAutoRefresh: enableAutoRefresh, - refreshSeconds: refreshSeconds - ) - } - - var queryItems: [URLQueryItem] { - [ - URLQueryItem(name: "symbol", value: symbol), - URLQueryItem(name: "interval", value: interval), - URLQueryItem(name: "period", value: period), - URLQueryItem(name: "max_bars", value: String(maxBars)), - URLQueryItem(name: "drop_live", value: String(dropLive)), - URLQueryItem(name: "use_body_range", value: String(useBodyRange)), - URLQueryItem(name: "volume_filter_enabled", value: String(volumeFilterEnabled)), - URLQueryItem(name: "volume_sma_window", value: String(volumeSMAWindow)), - URLQueryItem(name: "volume_multiplier", value: String(volumeMultiplier)), - URLQueryItem(name: "gray_fake", value: String(grayFake)), - URLQueryItem(name: "hide_market_closed_gaps", value: String(hideMarketClosedGaps)), - URLQueryItem(name: "enable_auto_refresh", value: String(enableAutoRefresh)), - URLQueryItem(name: "refresh_sec", value: String(refreshSeconds)), - ] - } -} - -#Preview { - MobileContentView() -} diff --git a/mac/src/AppTests/ManeshTraderMacTests.swift b/mac/src/AppTests/ManeshTraderMacTests.swift deleted file mode 100644 index ca3183f..0000000 --- a/mac/src/AppTests/ManeshTraderMacTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ManeshTraderMacTests -// -// Created by Matt Bruce on 2/13/26. -// - -import Testing -@testable import ManeshTraderMac - -struct ManeshTraderMacTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/mac/src/AppUITests/ManeshTraderMacUITests.swift b/mac/src/AppUITests/ManeshTraderMacUITests.swift deleted file mode 100644 index 5b204b0..0000000 --- a/mac/src/AppUITests/ManeshTraderMacUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// ManeshTraderMacUITests.swift -// ManeshTraderMacUITests -// -// Created by Matt Bruce on 2/13/26. -// - -import XCTest - -final class ManeshTraderMacUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } -} diff --git a/mac/src/AppUITests/ManeshTraderMacUITestsLaunchTests.swift b/mac/src/AppUITests/ManeshTraderMacUITestsLaunchTests.swift deleted file mode 100644 index 849ace0..0000000 --- a/mac/src/AppUITests/ManeshTraderMacUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ManeshTraderMacUITestsLaunchTests.swift -// ManeshTraderMacUITests -// -// Created by Matt Bruce on 2/13/26. -// - -import XCTest - -final class ManeshTraderMacUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/mac/src/MacShell.xcodeproj/project.pbxproj b/mac/src/MacShell.xcodeproj/project.pbxproj deleted file mode 100644 index 16d097e..0000000 --- a/mac/src/MacShell.xcodeproj/project.pbxproj +++ /dev/null @@ -1,704 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXContainerItemProxy section */ - EAB6607A2F3FD5C100ED41BA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = EAB660642F3FD5C000ED41BA /* Project object */; - proxyType = 1; - remoteGlobalIDString = EAB6606B2F3FD5C000ED41BA; - remoteInfo = ManeshTraderMac; - }; - EAB660842F3FD5C100ED41BA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = EAB660642F3FD5C000ED41BA /* Project object */; - proxyType = 1; - remoteGlobalIDString = EAB6606B2F3FD5C000ED41BA; - remoteInfo = ManeshTraderMac; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; - EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; - EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - EAB6606E2F3FD5C000ED41BA /* App */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = App; - sourceTree = ""; - }; - EAB661012F3FD5C100ED41BA /* AppMobile */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = AppMobile; - sourceTree = ""; - }; - EAB6607C2F3FD5C100ED41BA /* AppTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = AppTests; - sourceTree = ""; - }; - EAB660862F3FD5C100ED41BA /* AppUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = AppUITests; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - EAB660692F3FD5C000ED41BA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB661032F3FD5C100ED41BA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB660762F3FD5C100ED41BA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB660802F3FD5C100ED41BA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - EAB660632F3FD5C000ED41BA = { - isa = PBXGroup; - children = ( - EAB6606E2F3FD5C000ED41BA /* App */, - EAB661012F3FD5C100ED41BA /* AppMobile */, - EAB6607C2F3FD5C100ED41BA /* AppTests */, - EAB660862F3FD5C100ED41BA /* AppUITests */, - EAB6606D2F3FD5C000ED41BA /* Products */, - ); - sourceTree = ""; - }; - EAB6606D2F3FD5C000ED41BA /* Products */ = { - isa = PBXGroup; - children = ( - EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */, - EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */, - EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */, - EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */ = { - isa = PBXNativeTarget; - buildConfigurationList = EAB6608D2F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMac" */; - buildPhases = ( - EAB660682F3FD5C000ED41BA /* Sources */, - EAB660692F3FD5C000ED41BA /* Frameworks */, - EAB6606A2F3FD5C000ED41BA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - EAB6606E2F3FD5C000ED41BA /* App */, - ); - name = ManeshTraderMac; - packageProductDependencies = ( - ); - productName = ManeshTraderMac; - productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */; - productType = "com.apple.product-type.application"; - }; - EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */ = { - isa = PBXNativeTarget; - buildConfigurationList = EAB661082F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMobile" */; - buildPhases = ( - EAB661052F3FD5C100ED41BA /* Sources */, - EAB661032F3FD5C100ED41BA /* Frameworks */, - EAB661042F3FD5C100ED41BA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - EAB661012F3FD5C100ED41BA /* AppMobile */, - ); - name = ManeshTraderMobile; - packageProductDependencies = ( - ); - productName = ManeshTraderMobile; - productReference = EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */; - productType = "com.apple.product-type.application"; - }; - EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */; - buildPhases = ( - EAB660752F3FD5C100ED41BA /* Sources */, - EAB660762F3FD5C100ED41BA /* Frameworks */, - EAB660772F3FD5C100ED41BA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - EAB6607B2F3FD5C100ED41BA /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - EAB6607C2F3FD5C100ED41BA /* AppTests */, - ); - name = ManeshTraderMacTests; - packageProductDependencies = ( - ); - productName = ManeshTraderMacTests; - productReference = EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = EAB660932F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacUITests" */; - buildPhases = ( - EAB6607F2F3FD5C100ED41BA /* Sources */, - EAB660802F3FD5C100ED41BA /* Frameworks */, - EAB660812F3FD5C100ED41BA /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - EAB660852F3FD5C100ED41BA /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - EAB660862F3FD5C100ED41BA /* AppUITests */, - ); - name = ManeshTraderMacUITests; - packageProductDependencies = ( - ); - productName = ManeshTraderMacUITests; - productReference = EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - EAB660642F3FD5C000ED41BA /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2630; - LastUpgradeCheck = 2630; - TargetAttributes = { - EAB6606B2F3FD5C000ED41BA = { - CreatedOnToolsVersion = 26.3; - }; - EAB661022F3FD5C100ED41BA = { - CreatedOnToolsVersion = 26.3; - }; - EAB660782F3FD5C100ED41BA = { - CreatedOnToolsVersion = 26.3; - TestTargetID = EAB6606B2F3FD5C000ED41BA; - }; - EAB660822F3FD5C100ED41BA = { - CreatedOnToolsVersion = 26.3; - TestTargetID = EAB6606B2F3FD5C000ED41BA; - }; - }; - }; - buildConfigurationList = EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "MacShell" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = EAB660632F3FD5C000ED41BA; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 77; - productRefGroup = EAB6606D2F3FD5C000ED41BA /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */, - EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */, - EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */, - EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - EAB6606A2F3FD5C000ED41BA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB661042F3FD5C100ED41BA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB660772F3FD5C100ED41BA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB660812F3FD5C100ED41BA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - EAB660682F3FD5C000ED41BA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB661052F3FD5C100ED41BA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB660752F3FD5C100ED41BA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EAB6607F2F3FD5C100ED41BA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - EAB6607B2F3FD5C100ED41BA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */; - targetProxy = EAB6607A2F3FD5C100ED41BA /* PBXContainerItemProxy */; - }; - EAB660852F3FD5C100ED41BA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */; - targetProxy = EAB660842F3FD5C100ED41BA /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - EAB6608B2F3FD5C100ED41BA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.2; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - EAB6608C2F3FD5C100ED41BA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.2; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - }; - name = Release; - }; - EAB6608E2F3FD5C100ED41BA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMac; - PRODUCT_NAME = "$(TARGET_NAME)"; - REGISTER_APP_GROUPS = YES; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - EAB6608F2F3FD5C100ED41BA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - ENABLE_APP_SANDBOX = NO; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SELECTED_FILES = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMac; - PRODUCT_NAME = "$(TARGET_NAME)"; - REGISTER_APP_GROUPS = YES; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - EAB661062F3FD5C100ED41BA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - EAB661072F3FD5C100ED41BA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMobile; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - EAB660912F3FD5C100ED41BA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ManeshTraderMac.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ManeshTraderMac"; - }; - name = Debug; - }; - EAB660922F3FD5C100ED41BA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ManeshTraderMac.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ManeshTraderMac"; - }; - name = Release; - }; - EAB660942F3FD5C100ED41BA /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = ManeshTraderMac; - }; - name = Debug; - }; - EAB660952F3FD5C100ED41BA /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 6R7KLBPBLZ; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.ManeshTraderMacUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - STRING_CATALOG_GENERATE_SYMBOLS = NO; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; - TEST_TARGET_NAME = ManeshTraderMac; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "MacShell" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EAB6608B2F3FD5C100ED41BA /* Debug */, - EAB6608C2F3FD5C100ED41BA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - EAB6608D2F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMac" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EAB6608E2F3FD5C100ED41BA /* Debug */, - EAB6608F2F3FD5C100ED41BA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - EAB661082F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMobile" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EAB661062F3FD5C100ED41BA /* Debug */, - EAB661072F3FD5C100ED41BA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EAB660912F3FD5C100ED41BA /* Debug */, - EAB660922F3FD5C100ED41BA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - EAB660932F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - EAB660942F3FD5C100ED41BA /* Debug */, - EAB660952F3FD5C100ED41BA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = EAB660642F3FD5C000ED41BA /* Project object */; -} diff --git a/mac/src/MacShell.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mac/src/MacShell.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/mac/src/MacShell.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme b/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme deleted file mode 100644 index 6f4fa4a..0000000 --- a/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mac/src/README.md b/mac/src/README.md deleted file mode 100644 index e2cf7b0..0000000 --- a/mac/src/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# macOS Shell App - -Native macOS shell around the web app in `web/src/`. - -## What It Does -- Starts/stops bundled backend executable from app resources -- Hosts UI in `WKWebView` at local `127.0.0.1` URL -- Keeps user inside app window (no external browser) -- Provides a native toolbar `Help` button that opens bundled quick-start docs in-app - -## Build Self-Contained App -From repo root: -1. Build embedded backend + macOS app: - - `./scripts/build_selfcontained_mac_app.sh` -2. Output app bundle: - - `dist-mac//.app` -3. Optional DMG packaging: - - `APP_BUNDLE_PATH="dist-mac//.app" ./scripts/create_installer_dmg.sh` - - DMG output path: `build/dmg/-.dmg` - -## Run In Xcode -From repo root: -1. Generate embedded backend binary: - - `./scripts/build_embedded_backend.sh` -2. Open project: - - `mac/src/*.xcodeproj` -3. Build/Run scheme: - - default: project name (or set `MAC_SCHEME` in scripts) - -## iOS/iPadOS Target -- New target: `ManeshTraderMobile` (same Xcode project: `mac/src/MacShell.xcodeproj`). -- The mobile target is a web wrapper and does **not** launch the embedded backend executable. -- In the app, open `Setup` and provide a backend URL reachable from the device/simulator (for example `http://:8501` or an HTTPS host). -- The same symbol/timeframe/period preferences are passed as query params to the backend URL. - -## Notes -- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`). -- Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory. diff --git a/web/src/requirements.txt b/requirements.txt similarity index 100% rename from web/src/requirements.txt rename to requirements.txt diff --git a/run.sh b/run.sh deleted file mode 100755 index 01138eb..0000000 --- a/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/web/run.sh" "$@" diff --git a/scripts/build_embedded_backend.sh b/scripts/build_embedded_backend.sh deleted file mode 100755 index e8b3114..0000000 --- a/scripts/build_embedded_backend.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WEB_SRC_DIR="$ROOT_DIR/web/src" -MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}" -PYTHON_BIN="$ROOT_DIR/.venv/bin/python" -LAUNCHER="$WEB_SRC_DIR/backend_embedded_launcher.py" -BACKEND_BIN_NAME="${BACKEND_BIN_NAME:-WebBackend}" -BUILD_ROOT="$ROOT_DIR/dist-backend-build" -DIST_PATH="$BUILD_ROOT/dist" -WORK_PATH="$BUILD_ROOT/build" -SPEC_PATH="$BUILD_ROOT/spec" -PROJECT_PATH="${MAC_PROJECT_PATH:-}" -SCHEME="${MAC_SCHEME:-}" -TARGET_DIR="${EMBEDDED_BACKEND_DIR:-}" - -discover_scheme() { - local project_path="$1" - python3 - "$project_path" <<'PY' -import json -import subprocess -import sys - -project = sys.argv[1] -try: - output = subprocess.check_output( - ["xcodebuild", "-list", "-project", project, "-json"], - text=True, - stderr=subprocess.DEVNULL, - ) - payload = json.loads(output) -except Exception: - print("") - raise SystemExit(0) - -project_data = payload.get("project", {}) -schemes = project_data.get("schemes") or [] -print(schemes[0] if schemes else "") -PY -} - -if [[ ! -x "$PYTHON_BIN" ]]; then - echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2 - exit 1 -fi - -if [[ ! -f "$LAUNCHER" ]]; then - echo "Missing launcher file: $LAUNCHER" >&2 - exit 1 -fi - -if [[ -z "$PROJECT_PATH" ]]; then - PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)" -fi -if [[ -z "$PROJECT_PATH" ]]; then - echo "No .xcodeproj found under: $MAC_SRC_DIR" >&2 - echo "Set MAC_PROJECT_PATH to the project you want to build." >&2 - exit 1 -fi - -PROJECT_DIR="$(dirname "$PROJECT_PATH")" -if [[ -z "$SCHEME" ]]; then - SCHEME="$(discover_scheme "$PROJECT_PATH")" -fi -if [[ -z "$SCHEME" ]]; then - SCHEME="$(basename "$PROJECT_PATH" .xcodeproj)" -fi - -if [[ -z "$TARGET_DIR" ]]; then - DEFAULT_TARGET_DIR="$PROJECT_DIR/$SCHEME/EmbeddedBackend" - if [[ -d "$DEFAULT_TARGET_DIR" ]]; then - TARGET_DIR="$DEFAULT_TARGET_DIR" - else - TARGET_DIR="$(find "$PROJECT_DIR" -maxdepth 4 -type d -name EmbeddedBackend | sort | head -n 1)" - fi -fi -if [[ -z "$TARGET_DIR" ]]; then - TARGET_DIR="$PROJECT_DIR/$SCHEME/EmbeddedBackend" -fi - -mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR" - -"$PYTHON_BIN" -m pip install -q pyinstaller - -PYI_ARGS=( - --noconfirm - --clean - --onefile - --name "$BACKEND_BIN_NAME" - --distpath "$DIST_PATH" - --workpath "$WORK_PATH" - --specpath "$SPEC_PATH" - --add-data "$WEB_SRC_DIR/app.py:." - --collect-all streamlit - --collect-all streamlit_autorefresh - --hidden-import yfinance - --hidden-import pandas - --collect-all plotly - --collect-all kaleido -) - -for source_dir in "$WEB_SRC_DIR"/*; do - source_name="$(basename "$source_dir")" - if [[ ! -d "$source_dir" ]]; then - continue - fi - if [[ "$source_name" == "tests" ]] || [[ "$source_name" == "__pycache__" ]]; then - continue - fi - PYI_ARGS+=(--add-data "$source_dir:$source_name") -done - -if [[ -f "$WEB_SRC_DIR/ONBOARDING.md" ]]; then - PYI_ARGS+=(--add-data "$WEB_SRC_DIR/ONBOARDING.md:.") -fi - -"$PYTHON_BIN" -m PyInstaller "${PYI_ARGS[@]}" "$LAUNCHER" - -cp "$DIST_PATH/$BACKEND_BIN_NAME" "$TARGET_DIR/$BACKEND_BIN_NAME" -chmod +x "$TARGET_DIR/$BACKEND_BIN_NAME" - -echo "Embedded backend updated: $TARGET_DIR/$BACKEND_BIN_NAME" diff --git a/scripts/build_fresh_installer_dmg.sh b/scripts/build_fresh_installer_dmg.sh deleted file mode 100755 index 2ce397a..0000000 --- a/scripts/build_fresh_installer_dmg.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SETUP_SCRIPT="$ROOT_DIR/run.sh" -BUILD_APP_SCRIPT="$ROOT_DIR/scripts/build_selfcontained_mac_app.sh" -BUILD_DMG_SCRIPT="$ROOT_DIR/scripts/create_installer_dmg.sh" -VENV_PYTHON="$ROOT_DIR/.venv/bin/python" -BUILD_DIR="${BUILD_DIR:-$ROOT_DIR/build}" -DMG_DIR="${DMG_DIR:-$BUILD_DIR/dmg}" - -if [[ ! -x "$VENV_PYTHON" ]]; then - echo "Virtual environment missing. Running setup..." - "$SETUP_SCRIPT" --setup-only -fi - -if ! command -v create-dmg >/dev/null 2>&1; then - echo "create-dmg not found. Install with: brew install create-dmg" >&2 - exit 1 -fi - -app_marker="$(mktemp)" -touch "$app_marker" - -echo "Building fresh self-contained app (includes embedded backend rebuild)..." -"$BUILD_APP_SCRIPT" - -APP_BUNDLE_PATH="$(find "$ROOT_DIR/dist-mac" -type d -name "*.app" -newer "$app_marker" | sort | tail -n 1 || true)" -rm -f "$app_marker" - -if [[ -z "$APP_BUNDLE_PATH" || ! -d "$APP_BUNDLE_PATH" ]]; then - echo "Build succeeded but no freshly built app bundle was found in dist-mac." >&2 - exit 1 -fi - -dmg_marker="$(mktemp)" -touch "$dmg_marker" - -echo "Packaging DMG from freshly built app:" -echo " $APP_BUNDLE_PATH" -APP_BUNDLE_PATH="$APP_BUNDLE_PATH" "$BUILD_DMG_SCRIPT" - -DMG_PATH="$(find "$DMG_DIR" -type f -name "*.dmg" -newer "$dmg_marker" | sort | tail -n 1 || true)" -rm -f "$dmg_marker" - -if [[ -n "$DMG_PATH" && -f "$DMG_PATH" ]]; then - echo "Fresh installer ready:" - echo " $DMG_PATH" -else - echo "DMG packaging step completed, but new DMG path could not be auto-detected." >&2 - echo "Check: $DMG_DIR" -fi diff --git a/scripts/build_selfcontained_mac_app.sh b/scripts/build_selfcontained_mac_app.sh deleted file mode 100755 index 2809753..0000000 --- a/scripts/build_selfcontained_mac_app.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}" -PROJECT_PATH="${MAC_PROJECT_PATH:-}" -SCHEME="${MAC_SCHEME:-}" -CONFIGURATION="${CONFIGURATION:-Release}" -DERIVED_DATA_PATH="$ROOT_DIR/dist-mac/derived-data" -TIMESTAMP="$(date +%Y%m%d-%H%M%S)" -OUTPUT_DIR="$ROOT_DIR/dist-mac/$TIMESTAMP" - -discover_scheme() { - local project_path="$1" - python3 - "$project_path" <<'PY' -import json -import subprocess -import sys - -project = sys.argv[1] -try: - output = subprocess.check_output( - ["xcodebuild", "-list", "-project", project, "-json"], - text=True, - stderr=subprocess.DEVNULL, - ) - payload = json.loads(output) -except Exception: - print("") - raise SystemExit(0) - -project_data = payload.get("project", {}) -schemes = project_data.get("schemes") or [] -print(schemes[0] if schemes else "") -PY -} - -"$ROOT_DIR/scripts/build_embedded_backend.sh" - -if [[ -z "$PROJECT_PATH" ]]; then - PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)" -fi -if [[ -z "$PROJECT_PATH" ]]; then - echo "No .xcodeproj found under: $MAC_SRC_DIR" >&2 - echo "Set MAC_PROJECT_PATH to the project you want to build." >&2 - exit 1 -fi -if [[ -z "$SCHEME" ]]; then - SCHEME="$(discover_scheme "$PROJECT_PATH")" -fi -if [[ -z "$SCHEME" ]]; then - SCHEME="$(basename "$PROJECT_PATH" .xcodeproj)" -fi - -xcodebuild \ - -project "$PROJECT_PATH" \ - -scheme "$SCHEME" \ - -configuration "$CONFIGURATION" \ - -derivedDataPath "$DERIVED_DATA_PATH" \ - build - -APP_PATH="$(find "$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION" -maxdepth 2 -name "${SCHEME}.app" | head -n 1)" -if [[ -z "${APP_PATH:-}" ]]; then - echo "Build failed: ${SCHEME}.app not found in build products." >&2 - exit 1 -fi - -mkdir -p "$OUTPUT_DIR" -cp -R "$APP_PATH" "$OUTPUT_DIR/" - -echo "Self-contained app created: $OUTPUT_DIR/${SCHEME}.app" -echo "To package DMG:" -echo "APP_BUNDLE_PATH=\"$OUTPUT_DIR/${SCHEME}.app\" ./scripts/create_installer_dmg.sh" diff --git a/scripts/build_standalone_app.sh b/scripts/build_standalone_app.sh deleted file mode 100755 index 3166250..0000000 --- a/scripts/build_standalone_app.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WEB_SRC_DIR="$ROOT_DIR/web/src" -PYTHON_BIN="$ROOT_DIR/.venv/bin/python" -APP_NAME="${APP_NAME:-$(basename "$ROOT_DIR")}" - -if [[ ! -x "$PYTHON_BIN" ]]; then - echo "Missing virtual environment. Run ./run.sh --setup-only first." >&2 - exit 1 -fi - -"$PYTHON_BIN" -m pip install -q pyinstaller - -TS="$(date +%Y%m%d-%H%M%S)" -OUT_ROOT="$ROOT_DIR/dist-standalone/$TS" -DIST_PATH="$OUT_ROOT/dist" -WORK_PATH="$OUT_ROOT/build" -SPEC_PATH="$OUT_ROOT/spec" - -mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" - -PYI_ARGS=( - --noconfirm - --windowed - --name "$APP_NAME" - --distpath "$DIST_PATH" - --workpath "$WORK_PATH" - --specpath "$SPEC_PATH" - --add-data "$WEB_SRC_DIR/app.py:." - --collect-all streamlit - --collect-all streamlit_autorefresh - --hidden-import yfinance - --hidden-import pandas - --collect-all plotly -) - -for source_dir in "$WEB_SRC_DIR"/*; do - source_name="$(basename "$source_dir")" - if [[ ! -d "$source_dir" ]]; then - continue - fi - if [[ "$source_name" == "tests" ]] || [[ "$source_name" == "__pycache__" ]]; then - continue - fi - PYI_ARGS+=(--add-data "$source_dir:$source_name") -done - -if [[ -f "$WEB_SRC_DIR/ONBOARDING.md" ]]; then - PYI_ARGS+=(--add-data "$WEB_SRC_DIR/ONBOARDING.md:.") -fi - -"$PYTHON_BIN" -m PyInstaller "${PYI_ARGS[@]}" "$WEB_SRC_DIR/desktop_launcher.py" - -APP_BUNDLE="$DIST_PATH/${APP_NAME}.app" -if [[ ! -d "$APP_BUNDLE" ]]; then - echo "Build failed: ${APP_BUNDLE} not found" >&2 - exit 1 -fi - -echo "Standalone app created: $APP_BUNDLE" -echo "To build DMG from this app:" -echo "APP_BUNDLE_PATH=\"$APP_BUNDLE\" ./scripts/create_installer_dmg.sh" diff --git a/scripts/create_installer_dmg.sh b/scripts/create_installer_dmg.sh deleted file mode 100755 index 2f65172..0000000 --- a/scripts/create_installer_dmg.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -find_latest_app_bundle() { - local candidate - - candidate="$(find "$ROOT_DIR/dist-mac" -type d -name "*.app" 2>/dev/null | sort | tail -n 1 || true)" - if [[ -n "$candidate" ]]; then - echo "$candidate" - return 0 - fi - - candidate="$(find "$ROOT_DIR/dist-standalone" -type d -name "*.app" 2>/dev/null | sort | tail -n 1 || true)" - if [[ -n "$candidate" ]]; then - echo "$candidate" - return 0 - fi - - candidate="$(find "$ROOT_DIR" -maxdepth 1 -type d -name "*.app" | sort | head -n 1 || true)" - if [[ -n "$candidate" ]]; then - echo "$candidate" - return 0 - fi - - return 1 -} - -APP_BUNDLE="${APP_BUNDLE_PATH:-}" -if [[ -z "$APP_BUNDLE" ]]; then - APP_BUNDLE="$(find_latest_app_bundle || true)" -fi - -if ! command -v create-dmg >/dev/null 2>&1; then - echo "create-dmg not found. Install with: brew install create-dmg" >&2 - exit 1 -fi - -if [[ ! -d "$APP_BUNDLE" ]]; then - echo "App bundle not found." >&2 - echo "Set APP_BUNDLE_PATH to a built .app bundle or build one first." >&2 - exit 1 -fi - -APP_FILENAME="$(basename "$APP_BUNDLE")" -APP_NAME="${APP_FILENAME%.app}" -TS="$(date +%Y%m%d-%H%M%S)" -BUILD_DIR="${BUILD_DIR:-$ROOT_DIR/build}" -DMG_DIR="${DMG_DIR:-$BUILD_DIR/dmg}" -STAGE_DIR="$DMG_DIR/stage/$TS" -OUT_DMG="$DMG_DIR/${APP_NAME}-$TS.dmg" - -mkdir -p "$DMG_DIR" "$STAGE_DIR" -cp -R "$APP_BUNDLE" "$STAGE_DIR/" - -create-dmg \ - --volname "${APP_NAME} Installer" \ - --window-size 600 400 \ - --icon-size 120 \ - --icon "$APP_FILENAME" 175 190 \ - --icon "Applications" 425 190 \ - --hide-extension "$APP_FILENAME" \ - --app-drop-link 425 190 \ - "$OUT_DMG" \ - "$STAGE_DIR" - -rm -rf "$STAGE_DIR" -rmdir "$DMG_DIR/stage" 2>/dev/null || true - -echo "Created installer: $OUT_DMG" diff --git a/scripts/create_mac_app.sh b/scripts/create_mac_app.sh deleted file mode 100755 index c2b3d10..0000000 --- a/scripts/create_mac_app.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -APP_NAME="${APP_NAME:-$(basename "$ROOT_DIR")}" -APP_PATH="$ROOT_DIR/${APP_NAME}.app" - -if ! command -v osacompile >/dev/null 2>&1; then - echo "Error: osacompile is not available on this macOS installation." >&2 - exit 1 -fi - -ESCAPED_ROOT="${ROOT_DIR//\"/\\\"}" -SCRIPT_FILE="$(mktemp)" - -cat > "$SCRIPT_FILE" < float: - return a + (b - a) * t - - -def make_gradient(size: int) -> Image.Image: - img = Image.new("RGBA", (size, size), (0, 0, 0, 255)) - px = img.load() - top = (8, 17, 40) - bottom = (17, 54, 95) - for y in range(size): - t = y / (size - 1) - color = tuple(int(lerp(top[i], bottom[i], t)) for i in range(3)) + (255,) - for x in range(size): - px[x, y] = color - return img - - -def rounded_rect_mask(size: int, radius: int) -> Image.Image: - m = Image.new("L", (size, size), 0) - d = ImageDraw.Draw(m) - d.rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255) - return m - - -def draw_icon(size: int = 1024) -> Image.Image: - base = make_gradient(size) - draw = ImageDraw.Draw(base) - - # Soft vignette - vignette = Image.new("RGBA", (size, size), (0, 0, 0, 0)) - vd = ImageDraw.Draw(vignette) - vd.ellipse((-size * 0.25, -size * 0.15, size * 1.25, size * 1.15), fill=(255, 255, 255, 26)) - vd.ellipse((-size * 0.1, size * 0.55, size * 1.1, size * 1.5), fill=(0, 0, 0, 70)) - vignette = vignette.filter(ImageFilter.GaussianBlur(radius=size * 0.06)) - base = Image.alpha_composite(base, vignette) - draw = ImageDraw.Draw(base) - - # Grid lines - grid_color = (190, 215, 255, 34) - margin = int(size * 0.16) - for i in range(1, 5): - y = int(lerp(margin, size - margin, i / 5)) - draw.line((margin, y, size - margin, y), fill=grid_color, width=max(1, size // 512)) - - # Candlestick bodies/wicks - center_x = size // 2 - widths = int(size * 0.09) - gap = int(size * 0.05) - - candles = [ - (center_x - widths - gap, 0.62, 0.33, 0.57, 0.36, (18, 214, 130, 255)), - (center_x, 0.72, 0.40, 0.45, 0.65, (255, 88, 88, 255)), - (center_x + widths + gap, 0.58, 0.30, 0.52, 0.34, (18, 214, 130, 255)), - ] - - for x, low, high, body_top, body_bottom, color in candles: - x = int(x) - y_low = int(size * low) - y_high = int(size * high) - y_a = int(size * body_top) - y_b = int(size * body_bottom) - y_top = min(y_a, y_b) - y_bottom = max(y_a, y_b) - wick_w = max(3, size // 180) - draw.line((x, y_high, x, y_low), fill=(220, 235, 255, 220), width=wick_w) - bw = widths - draw.rounded_rectangle( - (x - bw // 2, y_top, x + bw // 2, y_bottom), - radius=max(6, size // 64), - fill=color, - ) - - # Trend arrows - arrow_green = (45, 237, 147, 255) - arrow_red = (255, 77, 77, 255) - up = [ - (int(size * 0.20), int(size * 0.70)), - (int(size * 0.29), int(size * 0.61)), - (int(size * 0.25), int(size * 0.61)), - (int(size * 0.25), int(size * 0.52)), - (int(size * 0.15), int(size * 0.52)), - (int(size * 0.15), int(size * 0.61)), - (int(size * 0.11), int(size * 0.61)), - ] - down = [ - (int(size * 0.80), int(size * 0.34)), - (int(size * 0.71), int(size * 0.43)), - (int(size * 0.75), int(size * 0.43)), - (int(size * 0.75), int(size * 0.52)), - (int(size * 0.85), int(size * 0.52)), - (int(size * 0.85), int(size * 0.43)), - (int(size * 0.89), int(size * 0.43)), - ] - draw.polygon(up, fill=arrow_green) - draw.polygon(down, fill=arrow_red) - - # Rounded-square icon mask - mask = rounded_rect_mask(size, radius=int(size * 0.23)) - out = Image.new("RGBA", (size, size), (0, 0, 0, 0)) - out.paste(base, (0, 0), mask) - - # Subtle border - bd = ImageDraw.Draw(out) - bd.rounded_rectangle( - (2, 2, size - 3, size - 3), - radius=int(size * 0.23), - outline=(255, 255, 255, 44), - width=max(2, size // 256), - ) - return out - - -def main() -> None: - out_path = Path("assets/icon/ManeshTrader.png") - out_path.parent.mkdir(parents=True, exist_ok=True) - img = draw_icon(1024) - img.save(out_path) - print(f"Wrote {out_path}") - - -if __name__ == "__main__": - main() diff --git a/skills/macos-selfcontained-webapp/SKILL.md b/skills/macos-selfcontained-webapp/SKILL.md deleted file mode 100644 index f27fc68..0000000 --- a/skills/macos-selfcontained-webapp/SKILL.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -name: macos-selfcontained-webapp -description: "Build or refactor a project so a web app runs as a self-contained macOS app: embedded local backend binary + SwiftUI/WKWebView shell + packaging scripts. Use when a user has web logic and wants no external browser, no separate web-code install, local execution from inside .app, and repeatable DMG packaging." ---- - -# macOS Self-Contained Web App - -## Outcome -Deliver one installable macOS app that: -- launches an embedded backend executable from app resources -- opens the web UI only inside `WKWebView` -- persists settings locally and supports native-to-web sync -- provides native app-level help/onboarding (not only in the web UI) -- can be packaged as a DMG - -## Layout Standard -Use this layout for new projects: -- `web/src/` for web backend source (`app.py`, modules, requirements) -- `mac/src/` for Xcode project and SwiftUI shell sources -- `scripts/` for build and packaging automation -- `docs/` for architecture and onboarding docs - -For existing repos, avoid destructive renames unless explicitly requested. Add compatibility paths or migration commits in small steps. - -Detailed naming guidance: `references/layout-and-naming.md` - -## Workflow -1. Inventory existing architecture. -- Identify backend entrypoint, runtime dependencies, and static/resource files. -- Confirm current launch mode and where browser auto-open is happening. - -2. Create embedded backend launcher. -- Add a thin launcher (`backend_embedded_launcher.py` or equivalent) that starts the web server on `127.0.0.1` and reads port from env (for example `MANESH_TRADER_PORT`). -- Ensure browser auto-open is disabled. - -3. Build backend into a single executable. -- Add `scripts/build_embedded_backend.sh` that compiles backend into a one-file executable and copies it into the mac target resource folder. -- Use generic discovery: - - discover `*.xcodeproj` under `mac/src` - - discover scheme via `xcodebuild -list -json` (fallback to project name) - - discover/create `EmbeddedBackend` folder under selected project sources -- Include all required web files/modules in build inputs. -- Default backend name should be stable (for example `WebBackend`) and configurable via env. - -4. Implement SwiftUI host shell. -- Use `@Observable` host state (no `ObservableObject`/Combine). -- Start/stop backend process, detect available local port, and handle retries. -- Render URL in `WKWebView` only. -- Keep app responsive when backend fails and surface actionable status. -- Resolve backend executable by checking both current and legacy names during migration. -- Add a native toolbar `Help` button that opens bundled help content in-app. - -5. Define settings sync contract. -- Use shared settings file (for example `~/./settings.json`) as source of truth. -- Normalize settings both in web app and native app. -- Pass effective settings via URL query params on launch/reload/restart. -- Keep onboarding limited to starter fields; preserve advanced fields. -- Add web-only fallback access to help/onboarding (for users not using the mac wrapper). - -6. Automate packaging. -- Add `scripts/build_selfcontained_mac_app.sh` to build embedded backend then Xcode app. -- Add `scripts/create_installer_dmg.sh` for distributable DMG. -- Use generic app bundle discovery for DMG defaults where possible. -- Standardize installer artifacts under `build/dmg/` (not repo root). -- Ensure `.gitignore` excludes generated artifacts (`build/`, `*.dmg`, temp `rw.*.dmg`). - -7. Validate. -- Python syntax: `python -m py_compile` on web entrypoint. -- Tests: `PYTHONPATH=web/src pytest -q web/src/tests`. -- Xcode build: `xcodebuild ... build`. -- Runtime check: no external browser opens, webview loads locally, settings persist across relaunch. -- Verify help works in both modes: - - native toolbar help popup in mac app - - sidebar help fallback in web-only mode - -## Required Deliverables -- Embedded backend build script -- macOS app host that launches backend + WKWebView -- Shared settings sync path -- Native help/onboarding popup + web fallback help entry -- README section for local build + DMG workflow -- Verified build commands and final artifact locations -- `.gitignore` updated for build/installer outputs - -## References -- Layout and migration rules: `references/layout-and-naming.md` -- Implementation blueprint and command templates: `references/implementation-blueprint.md` - -## Bundled Script -Use `scripts/scaffold_web_mac_layout.sh` to create a new standardized folder skeleton for fresh projects. diff --git a/skills/macos-selfcontained-webapp/agents/openai.yaml b/skills/macos-selfcontained-webapp/agents/openai.yaml deleted file mode 100644 index a90c050..0000000 --- a/skills/macos-selfcontained-webapp/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "macOS Self-Contained Web App" - short_description: "Embed a local web backend in a self-contained macOS WKWebView app." - default_prompt: "Create or refactor this project into a self-contained macOS app wrapper with web/src + mac/src layout, embedded backend binary, native Help popup, DMG output in build/dmg, and proper .gitignore rules." diff --git a/skills/macos-selfcontained-webapp/references/implementation-blueprint.md b/skills/macos-selfcontained-webapp/references/implementation-blueprint.md deleted file mode 100644 index 508d921..0000000 --- a/skills/macos-selfcontained-webapp/references/implementation-blueprint.md +++ /dev/null @@ -1,99 +0,0 @@ -# Implementation Blueprint - -## Backend Build Script (Python Example) - -```bash -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WEB_SRC_DIR="$ROOT_DIR/web/src" -MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}" -PYTHON_BIN="$ROOT_DIR/.venv/bin/python" -BACKEND_BIN_NAME="${BACKEND_BIN_NAME:-WebBackend}" -BUILD_ROOT="$ROOT_DIR/dist-backend-build" -DIST_PATH="$BUILD_ROOT/dist" -WORK_PATH="$BUILD_ROOT/build" -SPEC_PATH="$BUILD_ROOT/spec" -PROJECT_PATH="${MAC_PROJECT_PATH:-}" -SCHEME="${MAC_SCHEME:-}" -TARGET_DIR="${EMBEDDED_BACKEND_DIR:-}" - -if [[ -z "$PROJECT_PATH" ]]; then - PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)" -fi -PROJECT_DIR="$(dirname "$PROJECT_PATH")" -if [[ -z "$TARGET_DIR" ]]; then - TARGET_DIR="$(find "$PROJECT_DIR" -maxdepth 4 -type d -name EmbeddedBackend | head -n 1)" -fi -mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR" - -"$PYTHON_BIN" -m pip install -q pyinstaller - -"$PYTHON_BIN" -m PyInstaller \ - --noconfirm --clean --onefile \ - --name "$BACKEND_BIN_NAME" \ - --distpath "$DIST_PATH" \ - --workpath "$WORK_PATH" \ - --specpath "$SPEC_PATH" \ - --add-data "$WEB_SRC_DIR/app.py:." \ - --add-data "$WEB_SRC_DIR/:" \ - "$WEB_SRC_DIR/backend_embedded_launcher.py" - -cp "$DIST_PATH/$BACKEND_BIN_NAME" "$TARGET_DIR/$BACKEND_BIN_NAME" -chmod +x "$TARGET_DIR/$BACKEND_BIN_NAME" -``` - -## SwiftUI Host Requirements -- Use `@Observable` host object. -- Compute `serverURL` from host/port + query items. -- Start backend once on appear; stop on disappear. -- Render backend URL in `WKWebView`. -- Retry provisional load failure after short delay. -- Keep debug controls behind `#if DEBUG`. -- Add toolbar Help action that opens a bundled local `help.html` in a sheet/webview. - -## Settings Sync Contract -- Shared path: `~/./settings.json` -- Normalize every field on web and native sides. -- Load shared file before app launch. -- Push normalized fields as query params when launching/reloading webview. -- Persist setup-sheet changes back to shared file. -- Keep a legacy fallback path for migration when needed. - -## Packaging Commands -- Build app: - -```bash -./scripts/build_selfcontained_mac_app.sh -``` - -- Create DMG: - -```bash -APP_BUNDLE_PATH="dist-mac//Mac.app" ./scripts/create_installer_dmg.sh -``` - -Expected DMG output: - -```text -build/dmg/-.dmg -``` - -## Git Ignore Baseline - -```gitignore -build/ -*.dmg -rw.*.dmg -``` - -## Verification Checklist -- No external browser window opens. -- App starts with embedded backend from app resources. -- `WKWebView` loads local URL. -- Settings persist across relaunch and remain in sync. -- Native Help popup renders bundled content. -- Web-only run has sidebar help fallback. -- DMG installs and runs on a second machine. -- DMG is produced under `build/dmg/` and repo root stays clean. diff --git a/skills/macos-selfcontained-webapp/references/layout-and-naming.md b/skills/macos-selfcontained-webapp/references/layout-and-naming.md deleted file mode 100644 index 3969810..0000000 --- a/skills/macos-selfcontained-webapp/references/layout-and-naming.md +++ /dev/null @@ -1,72 +0,0 @@ -# Layout And Naming - -## Canonical Structure - -```text -/ - web/ - run.sh - src/ - app.py - - requirements.txt - ONBOARDING.md - mac/ - src/ - .xcodeproj/ - App/ - ContentView.swift - .swift - EmbeddedBackend/ - WebBackend - Help/ - help.html - AppTests/ - AppUITests/ - scripts/ - build_embedded_backend.sh - build_selfcontained_mac_app.sh - create_installer_dmg.sh - build/ - dmg/ - -.dmg - docs/ - architecture.md -``` - -## Naming Rules -- Repo: lowercase hyphenated (`trader-desktop-shell`). -- macOS target/scheme: PascalCase (`TraderMac` or project-defined), but source folders should stay generic (`App`, `AppTests`, `AppUITests`). -- Embedded backend binary: stable generic default (`WebBackend`) with env override support. -- Settings directory: lowercase snake or kebab (`~/.trader_app`). -- Environment port var: uppercase snake (keep legacy fallbacks during migration). - -## Script Discovery Rules -- Build scripts should discover the first `*.xcodeproj` under `mac/src` unless `MAC_PROJECT_PATH` is provided. -- Build scripts should discover scheme via `xcodebuild -list -json` unless `MAC_SCHEME` is provided. -- Embedded backend target dir should be derived from selected project sources or `EMBEDDED_BACKEND_DIR`. -- Python tests should run with `PYTHONPATH=web/src`. -- DMG scripts should write to `build/dmg/` and clean temporary staging folders. - -## Ignore Rules -Add these to root `.gitignore`: -- `build/` -- `*.dmg` -- `rw.*.dmg` - -## Migration Rules For Existing Projects -1. Do not rename everything in one commit. -2. First, add new folders and compatibility references. -3. Move scripts and docs next. -4. Move web source only after build scripts are updated. -5. Move mac source into `mac/src/App*` before optional target/project renames. -6. Add compatibility lookup for old backend binary names and old settings paths during transition. - -## Commit Strategy -1. `chore(layout): add canonical folders` -2. `build(backend): add embedded binary build` -3. `feat(mac-shell): host local backend in webview` -4. `feat(sync): add shared settings contract` -5. `feat(help): add native help popup + web fallback` -6. `build(packaging): add self-contained app + dmg scripts` -7. `chore(rename): finalize naming migration` diff --git a/skills/macos-selfcontained-webapp/scripts/scaffold_web_mac_layout.sh b/skills/macos-selfcontained-webapp/scripts/scaffold_web_mac_layout.sh deleted file mode 100755 index 3cfa003..0000000 --- a/skills/macos-selfcontained-webapp/scripts/scaffold_web_mac_layout.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 [app-name]" - echo "Example: $0 ~/Code/trader-app Trader" - exit 1 -fi - -REPO_ROOT="$1" -APP_NAME="${2:-WebShellApp}" - -mkdir -p "$REPO_ROOT/web/src" -mkdir -p "$REPO_ROOT/web/src/web_core" -mkdir -p "$REPO_ROOT/web/src/tests" -mkdir -p "$REPO_ROOT/mac/src/App/EmbeddedBackend" -mkdir -p "$REPO_ROOT/mac/src/App/Help" -mkdir -p "$REPO_ROOT/mac/src/AppTests" -mkdir -p "$REPO_ROOT/mac/src/AppUITests" -mkdir -p "$REPO_ROOT/scripts" -mkdir -p "$REPO_ROOT/docs" - -cat > "$REPO_ROOT/docs/architecture.md" < "$REPO_ROOT/scripts/README.build.md" < "$REPO_ROOT/web/src/ONBOARDING.md" < "$REPO_ROOT/mac/src/App/Help/help.html" < - - - - - Help - - -

${APP_NAME} Help

-

Replace this with your quick start and onboarding content.

- - -DOC - -cat > "$REPO_ROOT/web/run.sh" <<'DOC' -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -VENV_DIR="$ROOT_DIR/.venv" -WEB_SRC_DIR="$ROOT_DIR/web/src" - -if [[ ! -d "$VENV_DIR" ]]; then - python3 -m venv "$VENV_DIR" -fi - -# shellcheck disable=SC1091 -source "$VENV_DIR/bin/activate" -pip install -r "$WEB_SRC_DIR/requirements.txt" -exec streamlit run "$WEB_SRC_DIR/app.py" -DOC -chmod +x "$REPO_ROOT/web/run.sh" - -touch "$REPO_ROOT/.gitignore" -for line in "build/" "*.dmg" "rw.*.dmg"; do - if ! grep -Fxq "$line" "$REPO_ROOT/.gitignore"; then - printf "%s\n" "$line" >> "$REPO_ROOT/.gitignore" - fi -done - -echo "Created layout for ${APP_NAME}:" -echo "- $REPO_ROOT/web/src" -echo "- $REPO_ROOT/mac/src" -echo "- $REPO_ROOT/scripts" -echo "- $REPO_ROOT/docs" -echo "- $REPO_ROOT/.gitignore (updated with build/DMG ignore rules)" diff --git a/web/src/tests/conftest.py b/tests/conftest.py similarity index 100% rename from web/src/tests/conftest.py rename to tests/conftest.py diff --git a/web/src/tests/test_analytics.py b/tests/test_analytics.py similarity index 100% rename from web/src/tests/test_analytics.py rename to tests/test_analytics.py diff --git a/web/src/tests/test_app_settings.py b/tests/test_app_settings.py similarity index 100% rename from web/src/tests/test_app_settings.py rename to tests/test_app_settings.py diff --git a/web/src/tests/test_backtest_controls.py b/tests/test_backtest_controls.py similarity index 100% rename from web/src/tests/test_backtest_controls.py rename to tests/test_backtest_controls.py diff --git a/web/src/tests/test_charting.py b/tests/test_charting.py similarity index 100% rename from web/src/tests/test_charting.py rename to tests/test_charting.py diff --git a/web/src/tests/test_data.py b/tests/test_data.py similarity index 100% rename from web/src/tests/test_data.py rename to tests/test_data.py diff --git a/web/src/tests/test_exporting.py b/tests/test_exporting.py similarity index 100% rename from web/src/tests/test_exporting.py rename to tests/test_exporting.py diff --git a/web/src/tests/test_insights.py b/tests/test_insights.py similarity index 100% rename from web/src/tests/test_insights.py rename to tests/test_insights.py diff --git a/web/src/tests/test_settings_schema.py b/tests/test_settings_schema.py similarity index 100% rename from web/src/tests/test_settings_schema.py rename to tests/test_settings_schema.py diff --git a/web/src/tests/test_strategy.py b/tests/test_strategy.py similarity index 100% rename from web/src/tests/test_strategy.py rename to tests/test_strategy.py diff --git a/web/src/tests/test_time_display.py b/tests/test_time_display.py similarity index 100% rename from web/src/tests/test_time_display.py rename to tests/test_time_display.py diff --git a/web/src/tests/test_training_ui.py b/tests/test_training_ui.py similarity index 100% rename from web/src/tests/test_training_ui.py rename to tests/test_training_ui.py diff --git a/web/run.sh b/web/run.sh deleted file mode 100755 index 8601f85..0000000 --- a/web/run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WEB_SRC_DIR="$ROOT_DIR/web/src" -VENV_DIR="$ROOT_DIR/.venv" -SETUP_ONLY=false - -if [[ "${1:-}" == "--setup-only" ]]; then - SETUP_ONLY=true -fi - -if [[ ! -d "$VENV_DIR" ]]; then - echo "Creating virtual environment..." - python3 -m venv "$VENV_DIR" -fi - -# shellcheck disable=SC1091 -source "$VENV_DIR/bin/activate" - -if [[ ! -f "$VENV_DIR/.deps_installed" ]] || [[ "$WEB_SRC_DIR/requirements.txt" -nt "$VENV_DIR/.deps_installed" ]]; then - echo "Installing dependencies..." - pip install -r "$WEB_SRC_DIR/requirements.txt" - touch "$VENV_DIR/.deps_installed" -fi - -if [[ "$SETUP_ONLY" == "true" ]]; then - echo "Setup complete." - exit 0 -fi - -echo "Starting Streamlit app..." -exec streamlit run "$WEB_SRC_DIR/app.py" diff --git a/web/src/docker-compose.yml b/web/src/docker-compose.yml deleted file mode 100644 index 0129459..0000000 --- a/web/src/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: - maneshtrader: - build: - context: . - dockerfile: Dockerfile - container_name: maneshtrader - restart: unless-stopped - ports: - - "8501:8501" - volumes: - - ./data:/root/.web_local_shell diff --git a/web/src/web_core/__init__.py b/web_core/__init__.py similarity index 100% rename from web/src/web_core/__init__.py rename to web_core/__init__.py diff --git a/web/src/web_core/analytics.py b/web_core/analytics.py similarity index 100% rename from web/src/web_core/analytics.py rename to web_core/analytics.py diff --git a/web/src/web_core/auth/__init__.py b/web_core/auth/__init__.py similarity index 100% rename from web/src/web_core/auth/__init__.py rename to web_core/auth/__init__.py diff --git a/web/src/web_core/auth/profile_auth.py b/web_core/auth/profile_auth.py similarity index 100% rename from web/src/web_core/auth/profile_auth.py rename to web_core/auth/profile_auth.py diff --git a/web/src/web_core/auth/profile_store.py b/web_core/auth/profile_store.py similarity index 100% rename from web/src/web_core/auth/profile_store.py rename to web_core/auth/profile_store.py diff --git a/web/src/web_core/chart_overlays.py b/web_core/chart_overlays.py similarity index 100% rename from web/src/web_core/chart_overlays.py rename to web_core/chart_overlays.py diff --git a/web/src/web_core/charting.py b/web_core/charting.py similarity index 100% rename from web/src/web_core/charting.py rename to web_core/charting.py diff --git a/web/src/web_core/constants.py b/web_core/constants.py similarity index 100% rename from web/src/web_core/constants.py rename to web_core/constants.py diff --git a/web/src/web_core/data.py b/web_core/data.py similarity index 100% rename from web/src/web_core/data.py rename to web_core/data.py diff --git a/web/src/web_core/exporting.py b/web_core/exporting.py similarity index 100% rename from web/src/web_core/exporting.py rename to web_core/exporting.py diff --git a/web/src/web_core/help.html b/web_core/help.html similarity index 100% rename from web/src/web_core/help.html rename to web_core/help.html diff --git a/web/src/web_core/insights.py b/web_core/insights.py similarity index 100% rename from web/src/web_core/insights.py rename to web_core/insights.py diff --git a/web/src/web_core/live_guide.py b/web_core/live_guide.py similarity index 100% rename from web/src/web_core/live_guide.py rename to web_core/live_guide.py diff --git a/web/src/web_core/market/__init__.py b/web_core/market/__init__.py similarity index 100% rename from web/src/web_core/market/__init__.py rename to web_core/market/__init__.py diff --git a/web/src/web_core/market/presets.py b/web_core/market/presets.py similarity index 100% rename from web/src/web_core/market/presets.py rename to web_core/market/presets.py diff --git a/web/src/web_core/market/symbols.py b/web_core/market/symbols.py similarity index 100% rename from web/src/web_core/market/symbols.py rename to web_core/market/symbols.py diff --git a/web/src/web_core/models.py b/web_core/models.py similarity index 100% rename from web/src/web_core/models.py rename to web_core/models.py diff --git a/web/src/web_core/settings/__init__.py b/web_core/settings/__init__.py similarity index 100% rename from web/src/web_core/settings/__init__.py rename to web_core/settings/__init__.py diff --git a/web/src/web_core/settings/settings_schema.py b/web_core/settings/settings_schema.py similarity index 100% rename from web/src/web_core/settings/settings_schema.py rename to web_core/settings/settings_schema.py diff --git a/web/src/web_core/strategy.py b/web_core/strategy.py similarity index 100% rename from web/src/web_core/strategy.py rename to web_core/strategy.py diff --git a/web/src/web_core/time_display.py b/web_core/time_display.py similarity index 100% rename from web/src/web_core/time_display.py rename to web_core/time_display.py diff --git a/web/src/web_core/ui/__init__.py b/web_core/ui/__init__.py similarity index 100% rename from web/src/web_core/ui/__init__.py rename to web_core/ui/__init__.py diff --git a/web/src/web_core/ui/help_content.py b/web_core/ui/help_content.py similarity index 100% rename from web/src/web_core/ui/help_content.py rename to web_core/ui/help_content.py diff --git a/web/src/web_core/ui/login_ui.py b/web_core/ui/login_ui.py similarity index 100% rename from web/src/web_core/ui/login_ui.py rename to web_core/ui/login_ui.py diff --git a/web/src/web_core/ui/sidebar_ui.py b/web_core/ui/sidebar_ui.py similarity index 100% rename from web/src/web_core/ui/sidebar_ui.py rename to web_core/ui/sidebar_ui.py diff --git a/web/src/web_core/ui/training_ui.py b/web_core/ui/training_ui.py similarity index 100% rename from web/src/web_core/ui/training_ui.py rename to web_core/ui/training_ui.py