diff --git a/mac/src/App/EmbeddedBackend/WebBackend b/mac/src/App/EmbeddedBackend/WebBackend
index c31469b..c3e8c62 100755
Binary files a/mac/src/App/EmbeddedBackend/WebBackend and b/mac/src/App/EmbeddedBackend/WebBackend differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json b/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/mac/src/AppMobile/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..dfa108c
--- /dev/null
+++ b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -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
+ }
+}
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png
new file mode 100644
index 0000000..bb9b5b9
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-1024-marketing.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png
new file mode 100644
index 0000000..466520a
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@1x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png
new file mode 100644
index 0000000..e40a4f1
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png
new file mode 100644
index 0000000..e40a4f1
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png
new file mode 100644
index 0000000..2b8117a
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png
new file mode 100644
index 0000000..b988d74
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@1x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png
new file mode 100644
index 0000000..f25b929
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png
new file mode 100644
index 0000000..f25b929
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png
new file mode 100644
index 0000000..8230a77
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png
new file mode 100644
index 0000000..e40a4f1
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@1x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png
new file mode 100644
index 0000000..649c20b
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
new file mode 100644
index 0000000..649c20b
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png
new file mode 100644
index 0000000..df31be1
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
new file mode 100644
index 0000000..df31be1
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
new file mode 100644
index 0000000..2f58cc3
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png
new file mode 100644
index 0000000..7f79f23
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-76@2x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png
new file mode 100644
index 0000000..b824e4e
Binary files /dev/null and b/mac/src/AppMobile/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x-ipad.png differ
diff --git a/mac/src/AppMobile/Assets.xcassets/Contents.json b/mac/src/AppMobile/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/mac/src/AppMobile/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/mac/src/AppMobile/Help/help.html b/mac/src/AppMobile/Help/help.html
new file mode 100644
index 0000000..027daa5
--- /dev/null
+++ b/mac/src/AppMobile/Help/help.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+ Help & Quick Start
+
+
+
+
+ Help & Quick Start
+ A quick guide to reading signals, choosing settings, and troubleshooting.
+
+
+ Start in 60 Seconds
+
+ - Set a symbol like
AAPL or BTC-USD.
+ - Choose
Timeframe (1d is a good default) and Period (6mo).
+ - Keep
Ignore potentially live last bar enabled.
+ - Review trend status and chart markers.
+ - Use Export to download CSV/PDF outputs.
+
+
+
+
+ Signal Rules
+
+ real_bull close above previous high
+ real_bear close below previous low
+ fake close inside previous range
+
+
+ - Trend starts after 2 consecutive real bars in the same direction.
+ - Trend reverses only after 2 consecutive opposite real bars.
+ - Fake bars are noise and do not reverse trend.
+
+
+
+
+ Data Settings
+ Core fields
+
+ Symbol: ticker or pair, e.g. AAPL, MSFT, BTC-USD.
+ Timeframe: candle size. Start with 1d for cleaner swings.
+ Period: amount of history to load. Start with 6mo.
+ Max bars: limits loaded candles for speed and chart readability.
+
+ Optional filters
+
+ Use previous body range: ignores wick-only breakouts.
+ Enable volume filter: treats low-volume bars as fake.
+ Hide market-closed gaps: recommended ON for stocks.
+ Enable auto-refresh: useful for live monitoring only.
+
+
+
+
+ Chart Reading
+
+ - Green triangle-up markers show
real_bull bars.
+ - Red triangle-down markers show
real_bear bars.
+ - Gray candles (if enabled) de-emphasize fake/noise bars.
+ - Volume bars are color-coded by trend state.
+
+
+
+
+ Troubleshooting
+
+ - If no data appears, verify ticker format (for example
BTC-USD, not BTCUSD).
+ - If results look noisy, switch to
1d and reduce optional filters.
+ - If trend seems delayed, remember trend transitions require two real bars.
+ - This tool is analysis-only and does not place trades.
+
+
+
+
+
diff --git a/mac/src/AppMobile/ManeshTraderMobileApp.swift b/mac/src/AppMobile/ManeshTraderMobileApp.swift
new file mode 100644
index 0000000..153c164
--- /dev/null
+++ b/mac/src/AppMobile/ManeshTraderMobileApp.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+@main
+struct ManeshTraderMobileApp: App {
+ var body: some Scene {
+ WindowGroup {
+ MobileContentView()
+ }
+ }
+}
diff --git a/mac/src/AppMobile/MobileContentView.swift b/mac/src/AppMobile/MobileContentView.swift
new file mode 100644
index 0000000..2578b00
--- /dev/null
+++ b/mac/src/AppMobile/MobileContentView.swift
@@ -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()
+}
diff --git a/mac/src/MacShell.xcodeproj/project.pbxproj b/mac/src/MacShell.xcodeproj/project.pbxproj
index 045f0a6..16d097e 100644
--- a/mac/src/MacShell.xcodeproj/project.pbxproj
+++ b/mac/src/MacShell.xcodeproj/project.pbxproj
@@ -25,6 +25,7 @@
/* 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 */
@@ -35,6 +36,11 @@
path = App;
sourceTree = "";
};
+ EAB661012F3FD5C100ED41BA /* AppMobile */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = AppMobile;
+ sourceTree = "";
+ };
EAB6607C2F3FD5C100ED41BA /* AppTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = AppTests;
@@ -55,6 +61,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ EAB661032F3FD5C100ED41BA /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
EAB660762F3FD5C100ED41BA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -76,6 +89,7 @@
isa = PBXGroup;
children = (
EAB6606E2F3FD5C000ED41BA /* App */,
+ EAB661012F3FD5C100ED41BA /* AppMobile */,
EAB6607C2F3FD5C100ED41BA /* AppTests */,
EAB660862F3FD5C100ED41BA /* AppUITests */,
EAB6606D2F3FD5C000ED41BA /* Products */,
@@ -86,6 +100,7 @@
isa = PBXGroup;
children = (
EAB6606C2F3FD5C000ED41BA /* ManeshTraderMac.app */,
+ EAB661002F3FD5C100ED41BA /* ManeshTraderMobile.app */,
EAB660792F3FD5C100ED41BA /* ManeshTraderMacTests.xctest */,
EAB660832F3FD5C100ED41BA /* ManeshTraderMacUITests.xctest */,
);
@@ -117,6 +132,28 @@
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" */;
@@ -176,6 +213,9 @@
EAB6606B2F3FD5C000ED41BA = {
CreatedOnToolsVersion = 26.3;
};
+ EAB661022F3FD5C100ED41BA = {
+ CreatedOnToolsVersion = 26.3;
+ };
EAB660782F3FD5C100ED41BA = {
CreatedOnToolsVersion = 26.3;
TestTargetID = EAB6606B2F3FD5C000ED41BA;
@@ -201,6 +241,7 @@
projectRoot = "";
targets = (
EAB6606B2F3FD5C000ED41BA /* ManeshTraderMac */,
+ EAB661022F3FD5C100ED41BA /* ManeshTraderMobile */,
EAB660782F3FD5C100ED41BA /* ManeshTraderMacTests */,
EAB660822F3FD5C100ED41BA /* ManeshTraderMacUITests */,
);
@@ -215,6 +256,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ EAB661042F3FD5C100ED41BA /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
EAB660772F3FD5C100ED41BA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -239,6 +287,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ EAB661052F3FD5C100ED41BA /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
EAB660752F3FD5C100ED41BA /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -457,6 +512,64 @@
};
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 = {
@@ -558,6 +671,15 @@
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 = (
diff --git a/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme b/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme
new file mode 100644
index 0000000..6f4fa4a
--- /dev/null
+++ b/mac/src/MacShell.xcodeproj/xcshareddata/xcschemes/ManeshTraderMobile.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mac/src/README.md b/mac/src/README.md
index 3ea9bd7..e2cf7b0 100644
--- a/mac/src/README.md
+++ b/mac/src/README.md
@@ -27,6 +27,12 @@ From repo root:
3. Build/Run scheme:
- default: project name (or set `MAC_SCHEME` in scripts)
+## iOS/iPadOS Target
+- New target: `ManeshTraderMobile` (same Xcode project: `mac/src/MacShell.xcodeproj`).
+- The mobile target is a web wrapper and does **not** launch the embedded backend executable.
+- In the app, open `Setup` and provide a backend URL reachable from the device/simulator (for example `http://:8501` or an HTTPS host).
+- The same symbol/timeframe/period preferences are passed as query params to the backend URL.
+
## Notes
- Web source of truth is `web/src/` (`web/src/app.py`, `web/src/web_core/`).
- Embedded backend binary is copied into the first `EmbeddedBackend/` folder discovered under the selected project directory.