diff --git a/deploy_synology.sh b/deploy_synology.sh new file mode 100755 index 0000000..899ea80 --- /dev/null +++ b/deploy_synology.sh @@ -0,0 +1,236 @@ +#!/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. + +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}" +SYNO_PASSWORD="${SYNO_PASSWORD:-}" + +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 + --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, SYNO_PASSWORD, CONTAINER_NAME + +Examples: + ./deploy_synology.sh + ./deploy_synology.sh --mode image + ./deploy_synology.sh --recent-minutes 120 + ./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 + echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2 + exit 1 + 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 + echo "Error: SYNO_PASSWORD is set but sshpass is not installed." >&2 + exit 1 + 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 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + MODE="${2:-}" + shift 2 + ;; + --files) + read -r -a FILES <<< "${2:-}" + shift 2 + ;; + --recent-minutes) + minutes="${2:-}" + if ! [[ "$minutes" =~ ^[0-9]+$ ]]; then + echo "Error: --recent-minutes requires an integer." >&2 + exit 1 + fi + FILES=() + while IFS= read -r candidate; do + [[ -z "$candidate" ]] && continue + FILES+=("$candidate") + done <&2 + usage + exit 1 + ;; + esac +done + +if [[ "$MODE" != "bind" && "$MODE" != "image" ]]; then + echo "Error: --mode must be 'bind' or 'image'." >&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_path="${REMOTE_BASE}/${file}" + 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}\" +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 + +if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then + \"\$DOCKER_BIN\" 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_BIN\" restart \"${CONTAINER_NAME}\" + else + ids=\$(\"\$DOCKER_BIN\" ps --filter \"name=maneshtrader\" -q) + if [[ -z \"\$ids\" ]]; then + echo \"No docker compose command found, and no running containers matched name=maneshtrader.\" >&2 + echo \"Set CONTAINER_NAME= and rerun.\" >&2 + exit 1 + fi + \"\$DOCKER_BIN\" 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}\" +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 + +if [[ -n \"\$DOCKER_BIN\" ]] && \"\$DOCKER_BIN\" compose version >/dev/null 2>&1; then + \"\$DOCKER_BIN\" compose up -d --build +elif command -v docker-compose >/dev/null 2>&1; then + docker-compose up -d --build +else + echo \"No docker compose command found on remote host.\" >&2 + exit 1 +fi +'" + echo "Upload + image rebuild complete." +fi diff --git a/web/src/ONBOARDING.md b/web/src/ONBOARDING.md index 0708978..4846023 100644 --- a/web/src/ONBOARDING.md +++ b/web/src/ONBOARDING.md @@ -12,9 +12,10 @@ Trend logic: - Reversal requires 2 consecutive opposite real bars - Fake bars do not reverse trend -In-app help popup source: -- Primary: `web/src/web_core/help.html` -- Fallback: this `web/src/ONBOARDING.md` +In-app help popup: +- Open from sidebar `Help / Quick Start` +- Uses a multi-screen, child-friendly guide implemented in `web/src/web_core/ui/help_content.py` +- Includes dedicated screens for core setup, filters, training mode, backtest controls, and advanced panels ## 2) Quick Start (Recommended) From project root: diff --git a/web/src/PRD.md b/web/src/PRD.md index ec4961d..aa64a60 100644 --- a/web/src/PRD.md +++ b/web/src/PRD.md @@ -164,10 +164,11 @@ Gap handling (`hide_market_closed_gaps`): - For daily interval, weekend break removal is applied. ## 8. Help and Onboarding Behavior -- Web-only fallback help entry exists in sidebar: +- Web-only help entry exists in sidebar: - `Help / Quick Start` -- Content source: `web/src/web_core/help.html` (primary), `web/src/ONBOARDING.md` (fallback) -- Help appears in a dialog. +- Help appears in a dialog with multiple navigable screens (screen picker + previous/next). +- Help copy is intentionally beginner-friendly and explains each major sidebar control group, including detailed backtest controls and why each setting matters. +- The onboarding markdown remains project documentation; in-app help content is rendered from `web/src/web_core/ui/help_content.py`. ## 9. Outputs - Metrics: diff --git a/web/src/web_core/ui/help_content.py b/web/src/web_core/ui/help_content.py index 816810a..2c97043 100644 --- a/web/src/web_core/ui/help_content.py +++ b/web/src/web_core/ui/help_content.py @@ -1,28 +1,237 @@ from __future__ import annotations -from pathlib import Path - import streamlit as st +_HELP_SCREENS: list[tuple[str, str]] = [ + ( + "Start Here", + """ +### What this app is for +Think of ManeshTrader like a **weather app for price candles**. +It does not place trades. It helps you answer: +- Is the market acting strong up, strong down, or messy? +- Did we get a real signal or just noise? -@st.cache_data(show_spinner=False) -def load_help_content() -> tuple[str, bool]: - app_path = Path(__file__).resolve().parents[1] / "app.py" - help_html_paths = [ - app_path.with_name("web_core").joinpath("help.html"), - app_path.with_name("help.html"), - ] - for help_html_path in help_html_paths: - if help_html_path.exists(): - return help_html_path.read_text(encoding="utf-8"), True +### The 10-second idea +Each candle gets a label: +- `real_bull`: a strong up break +- `real_bear`: a strong down break +- `fake`: noise inside the old range - onboarding_path = app_path.with_name("ONBOARDING.md") - if onboarding_path.exists(): - return onboarding_path.read_text(encoding="utf-8"), False - return "Help content not found.", False +Trend rule: +- 2 real up candles in a row => bullish trend +- 2 real down candles in a row => bearish trend +- Fake candles alone do not flip trend + +### Why you should care +This turns a messy chart into simple states: +- **Go with up trend** +- **Go with down trend** +- **Wait when unclear** +""", + ), + ( + "How To Use", + """ +### A simple 5-step routine +1. Pick one symbol. +2. Use `1d` timeframe and `6mo` period. +3. Turn on `Show live decision guide` and `Show past behavior examples`. +4. Read trend + check latest examples. +5. Decide: follow trend, or stand aside if choppy. + +### Important safety notes +- This app is **analysis-only**. +- It does **not** execute orders. +- Backtests and examples are educational. +- Never risk money you cannot afford to lose. +""", + ), + ( + "Main Setup", + """ +### Core controls (left sidebar) +These are your main knobs. + +- `Symbol`: what you are looking at (`AAPL`, `BTC-USD`). + Why care: wrong symbol = wrong answer. +- `Timeframe`: candle size (`1d`, `4h`, `1h`, etc.). + Why care: smaller = noisier, bigger = smoother. +- `Period`: how much history to load. + Why care: too short can miss context, too long can feel slow. +- `Max bars`: cap on candles loaded. + Why care: helps performance and keeps chart readable. +- `Ignore potentially live last bar`: ignores the newest unfinished candle. + Why care: unfinished candles can lie before they close. + +### Child-friendly default +Start with: +- `Timeframe = 1d` +- `Period = 6mo` +- `Ignore potentially live last bar = ON` +""", + ), + ( + "Filters", + """ +### Classification Filters +These help you reduce fake signals. + +- `Use previous body range (ignore wicks)` + Means: compare against candle body, not long spikes. + Why care: reduces wick tricks. + +- `Enable volume filter` + Means: low-volume moves are treated as fake. + Why care: weak moves often fail. + +- `Volume SMA window` + Means: how many candles used for average volume. + Why care: bigger window = steadier average. + +- `Min volume / SMA multiplier` + Means: how much volume is required vs average. + Why care: higher value = stricter filter. + +- `Gray out fake bars` + Means: noise bars fade visually. + Why care: easier to focus on important candles. + +- `Hide market-closed gaps (stocks)` + Means: removes weekend/closed-market blank spaces. + Why care: cleaner chart shape. +""", + ), + ( + "Training Mode", + """ +### Training & Guidance controls +Use this section to learn the model. + +- `Show live decision guide` + Why care: tells bias (long/short/neutral) in plain English. + +- `Show past behavior examples` + Why care: shows historical examples of how signals played out. + +- `Overlay example entries/exits on chart` + Why care: lets you see examples directly on candles. + +- `Focus chart on selected example` + Why care: zooms to one example so you can study it. + +- `Max training examples` + Why care: more examples = more history, but busier table. + +- `Replay mode (hide future bars)` + `Replay bars shown` + Why care: practice without seeing the future first. + +### Kid version +Replay mode is like covering answers on a worksheet, +then checking if your guess was right. +""", + ), + ( + "Monitoring + Advanced", + """ +### Monitoring +- `Auto-refresh` + Why care: keeps data fresh while you watch. +- `Refresh interval (seconds)` + Why care: lower is faster updates, but more load. + +### Advanced Signals +- `Auto-run advanced panels (slower)` + Why care: always-updated extras, but heavier. +- `Run advanced panels now` + Why care: one-time run when you want speed normally. +- `Show multi-timeframe confirmation` + Why care: checks if 1h/4h/1d agree. +- `Regime filter` + Why care: warns when market is choppy (messy). + +### Compare Symbols + Alerts +- `Enable compare symbols panel` +- `Compare symbols (comma separated)` +Why care: quick side-by-side trend check. + +- `Enable alert rules` +- `Alert on bullish confirmations` +- `Alert on bearish confirmations` +- `Webhook URL (optional)` +Why care: get notified instead of watching charts all day. +""", + ), + ( + "Backtest Controls", + """ +### What a backtest is +A backtest asks: "If we used these rules in the past, what happened?" +It is practice data, not a promise. + +### Every backtest control explained +- `Slippage (bps per side)` + Meaning: extra price loss when entering/exiting. + Why care: real fills are rarely perfect. + +- `Fee (bps per side)` + Meaning: trading cost paid on entry and exit. + Why care: costs can erase small wins. + +- `Stop loss (%)` + Meaning: emergency exit if price moves against you by this percent. + Why care: limits damage. + Note: `0` means OFF. + +- `Take profit (%)` + Meaning: lock gains once profit reaches this percent. + Why care: prevents giving back wins. + Note: `0` means OFF. + +- `Min hold bars` + Meaning: shortest time a trade must stay open. + Why care: avoids instant in-and-out noise trades. + +- `Max hold bars` + Meaning: longest time a trade can stay open. + Why care: forces an exit if nothing happens. + +### Simple example +If `Stop loss = 2%`, you are saying: +"If I am down 2%, I am out." +If `Take profit = 4%`, you are saying: +"If I am up 4%, I will lock it." +""", + ), +] @st.dialog("Help & Quick Start", width="large") def help_dialog() -> None: - content, is_html = load_help_content() - st.markdown(content, unsafe_allow_html=is_html) + total = len(_HELP_SCREENS) + if "help_screen_index" not in st.session_state: + st.session_state["help_screen_index"] = 0 + + options = [title for title, _ in _HELP_SCREENS] + safe_index = min(max(int(st.session_state["help_screen_index"]), 0), total - 1) + st.session_state["help_screen_index"] = safe_index + + picker_col, _ = st.columns([3, 2]) + with picker_col: + selected_title = st.selectbox("Help Screen", options, index=safe_index) + selected_index = options.index(selected_title) + if selected_index != st.session_state["help_screen_index"]: + st.session_state["help_screen_index"] = selected_index + safe_index = selected_index + + st.caption(f"Screen {safe_index + 1} of {total}") + st.markdown(_HELP_SCREENS[safe_index][1]) + + nav_col, _ = st.columns([2, 5]) + prev_col, next_col = nav_col.columns(2) + with prev_col: + if st.button("Previous", disabled=safe_index == 0): + st.session_state["help_screen_index"] = max(safe_index - 1, 0) + with next_col: + if st.button("Next", disabled=safe_index >= total - 1): + st.session_state["help_screen_index"] = min(safe_index + 1, total - 1)