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

This commit is contained in:
Matt Bruce 2026-02-14 12:20:51 -06:00
parent 9453036d8e
commit cd042f105b
27 changed files with 1154 additions and 0 deletions

View File

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

View File

@ -0,0 +1,110 @@
{
"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.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

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

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

View File

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

@ -25,6 +25,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ManeshTraderMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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; }; EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ManeshTraderMacUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -35,6 +36,11 @@
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
EAB661012F3FD5C100ED41BA /* AppMobile */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppMobile;
sourceTree = "<group>";
};
EAB6607C2F3FD5C100ED41BA /* AppTests */ = { EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = AppTests; path = AppTests;
@ -55,6 +61,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EAB661032F3FD5C100ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660762F3FD5C100ED41BA /* Frameworks */ = { EAB660762F3FD5C100ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -76,6 +89,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAB6606E2F3FD5C000ED41BA /* App */, EAB6606E2F3FD5C000ED41BA /* App */,
EAB661012F3FD5C100ED41BA /* AppMobile */,
EAB6607C2F3FD5C100ED41BA /* AppTests */, EAB6607C2F3FD5C100ED41BA /* AppTests */,
EAB660862F3FD5C100ED41BA /* AppUITests */, EAB660862F3FD5C100ED41BA /* AppUITests */,
EAB6606D2F3FD5C000ED41BA /* Products */, EAB6606D2F3FD5C000ED41BA /* Products */,
@ -86,6 +100,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */, EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */,
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */, EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */, EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
); );
@ -117,6 +132,28 @@
productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */; productReference = EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */;
productType = "com.apple.product-type.application"; 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 */ = { EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */; buildConfigurationList = EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */;
@ -176,6 +213,9 @@
EAB6606B2F3FD5C000ED41BA = { EAB6606B2F3FD5C000ED41BA = {
CreatedOnToolsVersion = 26.3; CreatedOnToolsVersion = 26.3;
}; };
EAB661022F3FD5C100ED41BA = {
CreatedOnToolsVersion = 26.3;
};
EAB660782F3FD5C100ED41BA = { EAB660782F3FD5C100ED41BA = {
CreatedOnToolsVersion = 26.3; CreatedOnToolsVersion = 26.3;
TestTargetID = EAB6606B2F3FD5C000ED41BA; TestTargetID = EAB6606B2F3FD5C000ED41BA;
@ -201,6 +241,7 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */, EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */,
EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */,
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */, EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */, EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */,
); );
@ -215,6 +256,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EAB661042F3FD5C100ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660772F3FD5C100ED41BA /* Resources */ = { EAB660772F3FD5C100ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -239,6 +287,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
EAB661052F3FD5C100ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAB660752F3FD5C100ED41BA /* Sources */ = { EAB660752F3FD5C100ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -457,6 +512,64 @@
}; };
name = Release; 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 */ = { EAB660912F3FD5C100ED41BA /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@ -558,6 +671,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; 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" */ = { EAB660902F3FD5C100ED41BA /* Build configuration list for PBXNativeTarget "ManeshTraderMacTests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

View File

@ -0,0 +1,78 @@
<?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

@ -27,6 +27,12 @@ From repo root:
3. Build/Run scheme: 3. Build/Run scheme:
- default: project name (or set `MAC_SCHEME` in scripts) - 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 ## Notes
- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`). - 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. - Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory.