Compare commits

..

6 Commits

115 changed files with 29 additions and 4810 deletions

View File

@ -2,7 +2,8 @@ FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 PIP_NO_CACHE_DIR=1 \
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
WORKDIR /app WORKDIR /app

View File

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

View File

@ -263,7 +263,7 @@ If PDF fails, ensure `kaleido` is installed (already in `requirements.txt`).
### Port already in use ### Port already in use
```bash ```bash
streamlit run app.py --server.port 8502 streamlit run app.py --server.port 8501
``` ```
### Bad symbol/data error ### Bad symbol/data error

View File

@ -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/<timestamp>/<Scheme>.app`
Package as DMG:
```bash
APP_BUNDLE_PATH="dist-mac/<timestamp>/<Scheme>.app" ./scripts/create_installer_dmg.sh
```
Output: `build/dmg/<AppName>-<timestamp>.dmg`
## Optional Standalone Streamlit App
```bash
./scripts/build_standalone_app.sh
```
Output: `dist-standalone/<timestamp>/dist/<RepoName>.app`
## Notes
- Analysis-only app; no trade execution.
- Yahoo Finance interval availability depends on symbol/lookback.
- For broad distribution, use code signing + notarization.

View File

@ -1,5 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
./run.sh

View File

@ -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 <<EOF
$combined
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="${2:-}"
shift 2
;;
--env-file)
ENV_FILE="${2:-}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Error: env file not found: $ENV_FILE" >&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 <<EOF
$(find web/src -type f -mmin "-$minutes" ! -name ".DS_Store" | sort)
EOF
shift 2
;;
--git-hours)
hours="${2:-}"
if ! [[ "$hours" =~ ^[0-9]+$ ]]; then
echo "Error: --git-hours requires an integer." >&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=<your-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

26
docker-compose.yml Normal file
View File

@ -0,0 +1,26 @@
services:
maneshtrader:
image: maneshtrader:latest
pull_policy: build
build:
context: .
dockerfile: Dockerfile
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:
- maneshtrader_data:/root/.web_local_shell
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
volumes:
maneshtrader_data:

View File

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

View File

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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" : "Couldnt 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<Int32>.size))
var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.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<sockaddr_in>.stride)) == 0
}
}
}
}

View File

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

View File

@ -1,196 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Help & Quick Start</title>
<style>
:root {
color-scheme: light dark;
--bg: #0e1117;
--panel: #131925;
--text: #f3f6fb;
--muted: #a6b3c7;
--accent: #4db2ff;
--ok: #4bd37b;
--warn: #ff9f43;
--line: #2a3346;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f8fc;
--panel: #ffffff;
--text: #172033;
--muted: #5b6980;
--accent: #006fde;
--ok: #12834a;
--warn: #b85b00;
--line: #d7deea;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background: radial-gradient(circle at top right, rgba(77, 178, 255, 0.18), transparent 40%), var(--bg);
}
main {
max-width: 980px;
margin: 0 auto;
padding: 28px 24px 56px;
}
h1 {
margin: 0 0 8px;
font-size: 2rem;
}
.subtitle {
margin: 0 0 20px;
color: var(--muted);
}
section {
border: 1px solid var(--line);
background: color-mix(in oklab, var(--panel) 92%, transparent);
border-radius: 14px;
padding: 16px 18px;
margin: 0 0 14px;
}
h2 {
margin: 0 0 10px;
font-size: 1.05rem;
}
h3 {
margin: 12px 0 8px;
font-size: 0.95rem;
}
p,
li {
line-height: 1.45;
}
p {
margin: 0 0 8px;
}
ul,
ol {
margin: 0;
padding-left: 20px;
}
li {
margin-bottom: 6px;
}
code {
background: color-mix(in oklab, var(--panel) 80%, var(--line));
border: 1px solid var(--line);
border-radius: 6px;
padding: 1px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9em;
}
.tag {
display: inline-block;
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px 10px;
margin-right: 6px;
font-size: 0.85rem;
color: var(--muted);
}
.ok {
color: var(--ok);
font-weight: 600;
}
.warn {
color: var(--warn);
font-weight: 600;
}
</style>
</head>
<body>
<main>
<h1>Help & Quick Start</h1>
<p class="subtitle">A quick guide to reading signals, choosing settings, and troubleshooting.</p>
<section>
<h2>Start in 60 Seconds</h2>
<ol>
<li>Set a symbol like <code>AAPL</code> or <code>BTC-USD</code>.</li>
<li>Choose <code>Timeframe</code> (<code>1d</code> is a good default) and <code>Period</code> (<code>6mo</code>).</li>
<li>Keep <code>Ignore potentially live last bar</code> enabled.</li>
<li>Review trend status and chart markers.</li>
<li>Use Export to download CSV/PDF outputs.</li>
</ol>
</section>
<section>
<h2>Signal Rules</h2>
<p>
<span class="tag"><span class="ok">real_bull</span> close above previous high</span>
<span class="tag"><span class="warn">real_bear</span> close below previous low</span>
<span class="tag">fake close inside previous range</span>
</p>
<ul>
<li>Trend starts after <strong>2 consecutive real bars</strong> in the same direction.</li>
<li>Trend reverses only after <strong>2 consecutive opposite real bars</strong>.</li>
<li>Fake bars are noise and do not reverse trend.</li>
</ul>
</section>
<section>
<h2>Data Settings</h2>
<h3>Core fields</h3>
<ul>
<li><code>Symbol</code>: ticker or pair, e.g. <code>AAPL</code>, <code>MSFT</code>, <code>BTC-USD</code>.</li>
<li><code>Timeframe</code>: candle size. Start with <code>1d</code> for cleaner swings.</li>
<li><code>Period</code>: amount of history to load. Start with <code>6mo</code>.</li>
<li><code>Max bars</code>: limits loaded candles for speed and chart readability.</li>
</ul>
<h3>Optional filters</h3>
<ul>
<li><code>Use previous body range</code>: ignores wick-only breakouts.</li>
<li><code>Enable volume filter</code>: treats low-volume bars as fake.</li>
<li><code>Hide market-closed gaps</code>: recommended ON for stocks.</li>
<li><code>Enable auto-refresh</code>: useful for live monitoring only.</li>
</ul>
</section>
<section>
<h2>Chart Reading</h2>
<ul>
<li>Green triangle-up markers show <code>real_bull</code> bars.</li>
<li>Red triangle-down markers show <code>real_bear</code> bars.</li>
<li>Gray candles (if enabled) de-emphasize fake/noise bars.</li>
<li>Volume bars are color-coded by trend state.</li>
</ul>
</section>
<section>
<h2>Troubleshooting</h2>
<ul>
<li>If no data appears, verify ticker format (for example <code>BTC-USD</code>, not <code>BTCUSD</code>).</li>
<li>If results look noisy, switch to <code>1d</code> and reduce optional filters.</li>
<li>If trend seems delayed, remember trend transitions require two real bars.</li>
<li>This tool is analysis-only and does not place trades.</li>
</ul>
</section>
</main>
</body>
</html>

View File

@ -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")
}
}
}

View File

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,196 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Help & Quick Start</title>
<style>
:root {
color-scheme: light dark;
--bg: #0e1117;
--panel: #131925;
--text: #f3f6fb;
--muted: #a6b3c7;
--accent: #4db2ff;
--ok: #4bd37b;
--warn: #ff9f43;
--line: #2a3346;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f8fc;
--panel: #ffffff;
--text: #172033;
--muted: #5b6980;
--accent: #006fde;
--ok: #12834a;
--warn: #b85b00;
--line: #d7deea;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background: radial-gradient(circle at top right, rgba(77, 178, 255, 0.18), transparent 40%), var(--bg);
}
main {
max-width: 980px;
margin: 0 auto;
padding: 28px 24px 56px;
}
h1 {
margin: 0 0 8px;
font-size: 2rem;
}
.subtitle {
margin: 0 0 20px;
color: var(--muted);
}
section {
border: 1px solid var(--line);
background: color-mix(in oklab, var(--panel) 92%, transparent);
border-radius: 14px;
padding: 16px 18px;
margin: 0 0 14px;
}
h2 {
margin: 0 0 10px;
font-size: 1.05rem;
}
h3 {
margin: 12px 0 8px;
font-size: 0.95rem;
}
p,
li {
line-height: 1.45;
}
p {
margin: 0 0 8px;
}
ul,
ol {
margin: 0;
padding-left: 20px;
}
li {
margin-bottom: 6px;
}
code {
background: color-mix(in oklab, var(--panel) 80%, var(--line));
border: 1px solid var(--line);
border-radius: 6px;
padding: 1px 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9em;
}
.tag {
display: inline-block;
border: 1px solid var(--line);
border-radius: 999px;
padding: 2px 10px;
margin-right: 6px;
font-size: 0.85rem;
color: var(--muted);
}
.ok {
color: var(--ok);
font-weight: 600;
}
.warn {
color: var(--warn);
font-weight: 600;
}
</style>
</head>
<body>
<main>
<h1>Help & Quick Start</h1>
<p class="subtitle">A quick guide to reading signals, choosing settings, and troubleshooting.</p>
<section>
<h2>Start in 60 Seconds</h2>
<ol>
<li>Set a symbol like <code>AAPL</code> or <code>BTC-USD</code>.</li>
<li>Choose <code>Timeframe</code> (<code>1d</code> is a good default) and <code>Period</code> (<code>6mo</code>).</li>
<li>Keep <code>Ignore potentially live last bar</code> enabled.</li>
<li>Review trend status and chart markers.</li>
<li>Use Export to download CSV/PDF outputs.</li>
</ol>
</section>
<section>
<h2>Signal Rules</h2>
<p>
<span class="tag"><span class="ok">real_bull</span> close above previous high</span>
<span class="tag"><span class="warn">real_bear</span> close below previous low</span>
<span class="tag">fake close inside previous range</span>
</p>
<ul>
<li>Trend starts after <strong>2 consecutive real bars</strong> in the same direction.</li>
<li>Trend reverses only after <strong>2 consecutive opposite real bars</strong>.</li>
<li>Fake bars are noise and do not reverse trend.</li>
</ul>
</section>
<section>
<h2>Data Settings</h2>
<h3>Core fields</h3>
<ul>
<li><code>Symbol</code>: ticker or pair, e.g. <code>AAPL</code>, <code>MSFT</code>, <code>BTC-USD</code>.</li>
<li><code>Timeframe</code>: candle size. Start with <code>1d</code> for cleaner swings.</li>
<li><code>Period</code>: amount of history to load. Start with <code>6mo</code>.</li>
<li><code>Max bars</code>: limits loaded candles for speed and chart readability.</li>
</ul>
<h3>Optional filters</h3>
<ul>
<li><code>Use previous body range</code>: ignores wick-only breakouts.</li>
<li><code>Enable volume filter</code>: treats low-volume bars as fake.</li>
<li><code>Hide market-closed gaps</code>: recommended ON for stocks.</li>
<li><code>Enable auto-refresh</code>: useful for live monitoring only.</li>
</ul>
</section>
<section>
<h2>Chart Reading</h2>
<ul>
<li>Green triangle-up markers show <code>real_bull</code> bars.</li>
<li>Red triangle-down markers show <code>real_bear</code> bars.</li>
<li>Gray candles (if enabled) de-emphasize fake/noise bars.</li>
<li>Volume bars are color-coded by trend state.</li>
</ul>
</section>
<section>
<h2>Troubleshooting</h2>
<ul>
<li>If no data appears, verify ticker format (for example <code>BTC-USD</code>, not <code>BTCUSD</code>).</li>
<li>If results look noisy, switch to <code>1d</code> and reduce optional filters.</li>
<li>If trend seems delayed, remember trend transitions require two real bars.</li>
<li>This tool is analysis-only and does not place trades.</li>
</ul>
</section>
</main>
</body>
</html>

View File

@ -1,10 +0,0 @@
import SwiftUI
@main
struct ManeshTraderMobileApp: App {
var body: some Scene {
WindowGroup {
MobileContentView()
}
}
}

View File

@ -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()
}

View File

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

View File

@ -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 its 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()
}
}
}

View File

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

View File

@ -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 = "<group>";
};
EAB661012F3FD5C100ED41BA /* AppMobile */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppMobile;
sourceTree = "<group>";
};
EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppTests;
sourceTree = "<group>";
};
EAB660862F3FD5C100ED41BA /* AppUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppUITests;
sourceTree = "<group>";
};
/* 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 = "<group>";
};
EAB6606D2F3FD5C000ED41BA /* Products */ = {
isa = PBXGroup;
children = (
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */,
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
BuildableName = "ManeshTraderMobile.app"
BlueprintName = "ManeshTraderMobile"
ReferencedContainer = "container:MacShell.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
BuildableName = "ManeshTraderMobile.app"
BlueprintName = "ManeshTraderMobile"
ReferencedContainer = "container:MacShell.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EAB661022F3FD5C100ED41BA"
BuildableName = "ManeshTraderMobile.app"
BlueprintName = "ManeshTraderMobile"
ReferencedContainer = "container:MacShell.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -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/<timestamp>/<Scheme>.app`
3. Optional DMG packaging:
- `APP_BUNDLE_PATH="dist-mac/<timestamp>/<Scheme>.app" ./scripts/create_installer_dmg.sh`
- DMG output path: `build/dmg/<AppName>-<timestamp>.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://<LAN-IP>: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.

4
run.sh
View File

@ -1,4 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
exec "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/web/run.sh" "$@"

View File

@ -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"

View File

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

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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" <<EOF
on run
tell application "Terminal"
activate
do script "cd \"$ESCAPED_ROOT\" && ./run.sh"
end tell
end run
EOF
rm -rf "$APP_PATH"
osacompile -o "$APP_PATH" "$SCRIPT_FILE"
rm -f "$SCRIPT_FILE"
echo "Created: $APP_PATH"
echo "You can drag ${APP_NAME}.app into /Applications if desired."

View File

@ -1,129 +0,0 @@
from __future__ import annotations
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter
def lerp(a: float, b: float, t: float) -> 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()

View File

@ -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 `~/.<app>/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.

View File

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

View File

@ -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/<module_dir>:<module_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: `~/.<app>/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/<timestamp>/<AppName>Mac.app" ./scripts/create_installer_dmg.sh
```
Expected DMG output:
```text
build/dmg/<AppName>-<timestamp>.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.

View File

@ -1,72 +0,0 @@
# Layout And Naming
## Canonical Structure
```text
<repo-root>/
web/
run.sh
src/
app.py
<web modules>
requirements.txt
ONBOARDING.md
mac/
src/
<Project>.xcodeproj/
App/
ContentView.swift
<AppMain>.swift
EmbeddedBackend/
WebBackend
Help/
help.html
AppTests/
AppUITests/
scripts/
build_embedded_backend.sh
build_selfcontained_mac_app.sh
create_installer_dmg.sh
build/
dmg/
<AppName>-<timestamp>.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`

View File

@ -1,99 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <repo-root> [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" <<DOC
# ${APP_NAME} Architecture
- web backend source: ./web/src
- mac shell source: ./mac/src
- embedded backend binary: ./mac/src/App/EmbeddedBackend/WebBackend
- native help page: ./mac/src/App/Help/help.html
DOC
cat > "$REPO_ROOT/scripts/README.build.md" <<DOC
# Build Script Placeholders
Add:
- build_embedded_backend.sh
- build_selfcontained_mac_app.sh
- create_installer_dmg.sh
Suggested script behaviors:
- Discover \`*.xcodeproj\` under \`mac/src\` unless \`MAC_PROJECT_PATH\` is provided.
- Discover scheme via \`xcodebuild -list -json\` unless \`MAC_SCHEME\` is provided.
- Default backend binary name: \`WebBackend\` (override with \`BACKEND_BIN_NAME\`).
- Write DMG artifacts to \`build/dmg/\`.
DOC
cat > "$REPO_ROOT/web/src/ONBOARDING.md" <<DOC
# ${APP_NAME} Onboarding
Add your web help/onboarding content here.
DOC
cat > "$REPO_ROOT/mac/src/App/Help/help.html" <<DOC
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Help</title>
</head>
<body>
<h1>${APP_NAME} Help</h1>
<p>Replace this with your quick start and onboarding content.</p>
</body>
</html>
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)"

View File

@ -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"

View File

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

Some files were not shown because too many files have changed in this diff Show More