Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-14 11:30:55 -06:00
parent 5bbc6a0111
commit a9f30f0fa4
6 changed files with 290 additions and 47 deletions

View File

@ -21,6 +21,7 @@ struct ContentView: View {
@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
@ -54,6 +55,9 @@ struct ContentView: View {
.onDisappear { host.stop() }
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Help") {
showHelpSheet = true
}
Button("Setup") {
syncStateFromSharedSettingsFileIfAvailable()
setupDraft = storedWebPreferences.normalized().setupDefaults
@ -74,6 +78,9 @@ struct ContentView: View {
.sheet(isPresented: $showSetupSheet) {
setupSheet
}
.sheet(isPresented: $showHelpSheet) {
HelpSheetView()
}
}
private var sharedSettingsURL: URL {
@ -330,6 +337,64 @@ struct ContentView: View {
#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()
}

196
mac/src/App/Help/help.html Normal file
View File

@ -0,0 +1,196 @@
<!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

@ -30,17 +30,17 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */ = {
EAB6606E2F3FD5C000ED41BA /* App */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = App;
sourceTree = "<group>";
};
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppTests;
sourceTree = "<group>";
};
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */ = {
EAB660862F3FD5C100ED41BA /* AppUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppUITests;
sourceTree = "<group>";
@ -75,9 +75,9 @@
EAB660632F3FD5C000ED41BA = {
isa = PBXGroup;
children = (
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */,
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */,
EAB6606E2F3FD5C000ED41BA /* App */,
EAB6607C2F3FD5C100ED41BA /* AppTests */,
EAB660862F3FD5C100ED41BA /* AppUITests */,
EAB6606D2F3FD5C000ED41BA /* Products */,
);
sourceTree = "<group>";
@ -108,7 +108,7 @@
dependencies = (
);
fileSystemSynchronizedGroups = (
EAB6606E2F3FD5C000ED41BA /* ManeshTraderMac */,
EAB6606E2F3FD5C000ED41BA /* App */,
);
name = ManeshTraderMac;
packageProductDependencies = (
@ -131,7 +131,7 @@
EAB6607B2F3FD5C100ED41BA /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EAB6607C2F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB6607C2F3FD5C100ED41BA /* AppTests */,
);
name = ManeshTraderMacTests;
packageProductDependencies = (
@ -154,7 +154,7 @@
EAB660852F3FD5C100ED41BA /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EAB660862F3FD5C100ED41BA /* ManeshTraderMacUITests */,
EAB660862F3FD5C100ED41BA /* AppUITests */,
);
name = ManeshTraderMacUITests;
packageProductDependencies = (
@ -186,7 +186,7 @@
};
};
};
buildConfigurationList = EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "ManeshTraderMac" */;
buildConfigurationList = EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "MacShell" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@ -540,7 +540,7 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "ManeshTraderMac" */ = {
EAB660672F3FD5C000ED41BA /* Build configuration list for PBXProject "MacShell" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EAB6608B2F3FD5C100ED41BA /* Debug */,

View File

@ -6,6 +6,7 @@ Native macOS shell around the web app in `web/src/`.
- 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:

View File

@ -155,6 +155,19 @@ def save_web_settings(settings: dict[str, Any]) -> None:
SETTINGS_PATH.write_text(json.dumps(normalize_web_settings(settings), indent=2), encoding="utf-8")
@st.cache_data(show_spinner=False)
def load_help_markdown() -> str:
onboarding_path = Path(__file__).with_name("ONBOARDING.md")
if onboarding_path.exists():
return onboarding_path.read_text(encoding="utf-8")
return "Help content not found."
@st.dialog("Help & Quick Start", width="large")
def help_dialog() -> None:
st.markdown(load_help_markdown())
@st.cache_data(show_spinner=False, ttl=3600)
def lookup_symbol_candidates(query: str, max_results: int = 10) -> list[dict[str, str]]:
cleaned = query.strip()
@ -226,51 +239,19 @@ def resolve_symbol_identity(symbol: str) -> dict[str, str]:
return {"symbol": normalized_symbol, "name": "", "exchange": ""}
@st.cache_data(show_spinner=False)
def load_onboarding_markdown() -> str:
onboarding_path = Path(__file__).with_name("ONBOARDING.md")
if onboarding_path.exists():
return onboarding_path.read_text(encoding="utf-8")
return "ONBOARDING.md not found in project root."
@st.dialog("Onboarding Guide", width="large")
def onboarding_dialog() -> None:
st.markdown(load_onboarding_markdown())
def main() -> None:
st.set_page_config(page_title="Real Bars vs Fake Bars Analyzer", layout="wide")
st.title("Real Bars vs Fake Bars Trend Analyzer")
st.caption(
"Price-action tool that classifies closed bars, filters fake bars, and tracks trend persistence using only real bars."
)
if st.button("Open ONBOARDING.md", type="tertiary"):
onboarding_dialog()
with st.expander("Help / Quick Start", expanded=False):
st.markdown(
"""
**Start in 60 seconds**
1. Set a symbol like `AAPL` or `BTC-USD`.
2. Choose `Timeframe` (`1d` is a good default) and `Period` (`6mo`).
3. Keep **Ignore potentially live last bar** enabled.
4. Review trend status and markers:
- Green triangle: `real_bull`
- Red triangle: `real_bear`
- `fake` bars are noise and ignored by trend logic
5. Use **Export** to download CSV/PDF outputs.
**Rule summary**
- `real_bull`: close > previous high
- `real_bear`: close < previous low
- `fake`: close inside previous range
- Trend starts/reverses only after 2 consecutive real bars in that direction.
"""
)
with st.sidebar:
st.header("Data Settings")
if st.button("Help / Quick Start", use_container_width=True):
help_dialog()
st.divider()
query_params = st.query_params
persisted_settings = load_web_settings()