Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,35 @@ jobs:
run: python scripts/p0_smoke.py
- name: Contract sample
run: python scripts/validate_contract_sample.py
- name: iOS commercial gates (offline)
run: python ios/scripts/verify_commercial_gates.py

ios-unit:
runs-on: macos-14
if: github.event_name == 'pull_request' || contains(github.event.head_commit.message, 'ios')
steps:
- uses: actions/checkout@v4
- name: Install xcodegen
run: brew install xcodegen
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app/Contents/Developer
- name: Generate + AppAccess unit tests
run: |
cd ios
xcodegen generate
grep -E 'objectVersion = ' QuantRadar.xcodeproj/project.pbxproj | head -1
# Prefer iPhone 15 on Xcode 15 runners
DEST=$(xcrun simctl list devices available | awk -F '[()]' '/iPhone 15[ )]/ && !/Pro/ {print $2; exit}')
if [ -z "$DEST" ]; then DEST=$(xcrun simctl list devices available | awk -F '[()]' '/iPhone 15/ {print $2; exit}'); fi
echo "Using simulator udid=$DEST"
xcodebuild test \
-project QuantRadar.xcodeproj \
-scheme QuantRadar \
-destination "platform=iOS Simulator,id=$DEST" \
-only-testing:QuantRadarTests/AppAccessTests \
-only-testing:QuantRadarTests/OrganicFunnelTests \
-only-testing:QuantRadarTests/BarsCacheTests \
-only-testing:QuantRadarTests/DepthEngineTests \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ venv/
.ruff_cache/
data/
!data/.gitkeep
ios/*.xcodeproj
ios/*.xcworkspace
ios/DerivedData/
ios/build/
.tools/
**/xcuserdata/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ python3 scripts/validate_contract_sample.py
| **Ship v0.4.0** | ✅ magic link、Stripe checkout、Docker/Render/Fly、`bootstrap_env`、CI |
| **Trust Gate v0.5.0** | ✅ 单 `primary_score`、诚实落地页、demo 不计费、methodology/pricing/legal、waitlist |
| 下一优先 | **域名 cutover** + Trust Gate 线上 PASS → live charts → 再谈 Pro 收款 |
| **iOS App Store(独立运营)** | [`ios/`](./ios/) — **免费下载 + $9.99 一次性 Unlock** · **零 COGS(Yahoo 设备端)** · Web 仍走 Massive;互不解锁 |

### Trust Gate

Expand Down
4 changes: 3 additions & 1 deletion app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ def do_GET(self) -> None: # noqa: N802
"/track": "track.html",
"/terms": "terms.html",
"/privacy": "privacy.html",
"/terms-ios": "terms-ios.html",
"/privacy-ios": "privacy-ios.html",
"/refund": "refund.html",
"/btn-demos": "btn-demos.html", # internal preview; noindex
}
Expand Down Expand Up @@ -868,7 +870,7 @@ def do_GET(self) -> None: # noqa: N802
share_lines = "".join(f"/r/{t}\n" for t in sorted(DEMO_TICKERS))
body = (
"/\n/methodology\n/pricing\n/track\n"
"/terms\n/privacy\n/refund\n/login\n"
"/terms\n/privacy\n/terms-ios\n/privacy-ios\n/refund\n/login\n"
+ share_lines
).encode()
self._send(200, body, content_type="text/plain")
Expand Down
Binary file added docs/audit/2026-08-02/prod-fixed-375.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions ios/QuantRadar/App/QuantRadarApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SwiftUI

@main
struct QuantRadarApp: App {
@StateObject private var radar = RadarService()
@StateObject private var watchlist = WatchlistStore()
@StateObject private var purchases = PurchaseStore()
@AppStorage("hasSeenOnboarding") private var hasSeenOnboarding = false

var body: some Scene {
WindowGroup {
Group {
if hasSeenOnboarding {
RootTabView()
} else {
OnboardingView(hasSeenOnboarding: $hasSeenOnboarding)
}
}
.environmentObject(radar)
.environmentObject(watchlist)
.environmentObject(purchases)
.preferredColorScheme(.dark)
.onAppear { ReviewPrompt.recordLaunch() }
}
}
}
32 changes: 32 additions & 0 deletions ios/QuantRadar/App/RootTabView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import SwiftUI

enum QRTheme {
static let bg = Color(red: 0.043, green: 0.059, blue: 0.055)
static let panel = Color(red: 0.07, green: 0.10, blue: 0.09)
static let radar = Color(red: 0.18, green: 0.80, blue: 0.44)
static let radarDim = Color(red: 0.18, green: 0.80, blue: 0.44).opacity(0.18)
static let text = Color(red: 0.92, green: 0.95, blue: 0.93)
static let muted = Color(red: 0.55, green: 0.62, blue: 0.58)
static let danger = Color(red: 0.95, green: 0.35, blue: 0.32)
static let warn = Color(red: 0.95, green: 0.72, blue: 0.25)
}

struct RootTabView: View {
@EnvironmentObject private var purchases: PurchaseStore

var body: some View {
TabView {
TodayView()
.tabItem { Label("Today", systemImage: "dot.radiowaves.left.and.right") }
SearchView()
.tabItem { Label("Scan", systemImage: "magnifyingglass") }
if purchases.effectiveUnlocked {
WatchlistView()
.tabItem { Label("Watch", systemImage: "eye") }
}
SettingsView()
.tabItem { Label("Settings", systemImage: "gearshape") }
}
.tint(QRTheme.radar)
}
}
52 changes: 52 additions & 0 deletions ios/QuantRadar/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>QuantRadar</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>LaunchBackground</string>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>NSUserNotificationsUsageDescription</key>
<string>QuantRadar can send a weekday morning reminder when today’s SPY radar is ready, and posture-change alerts for tickers you watch.</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>
161 changes: 161 additions & 0 deletions ios/QuantRadar/Models/RadarVerdict.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import Foundation

struct RadarVerdict: Codable, Identifiable, Hashable {
var id: String { ticker }

let ok: Bool
let ticker: String
let companyName: String?
let sector: String?
let primaryScore: PrimaryScore?
let primary: PrimaryAction?
let summary: String?
let engagement: Engagement?
let dataQuality: DataQuality?
let market: MarketSnapshot?
let meta: Meta?
let gate: Gate?
let warnings: [String]?
let depth: Depth?

enum CodingKeys: String, CodingKey {
case ok, ticker, sector, summary, engagement, market, meta, gate, warnings, depth
case companyName = "company_name"
case primaryScore = "primary_score"
case primary
case dataQuality = "data_quality"
}

struct PrimaryScore: Codable, Hashable {
let value: Double?
let scale: Double?
let label: String?
let withheld: Bool?
let note: String?
}

struct PrimaryAction: Codable, Hashable {
let action: String?
let label: String?
let reason: String?
}

struct Engagement: Codable, Hashable {
let avoidedLine: String?
let freezeLabel: String?
let postureNote: String?

enum CodingKeys: String, CodingKey {
case avoidedLine = "avoided_line"
case freezeLabel = "freeze_label"
case postureNote = "posture_note"
}
}

struct DataQuality: Codable, Hashable {
let usable: Bool?
let reliability: String?
let optionsActionable: Bool?

enum CodingKeys: String, CodingKey {
case usable, reliability
case optionsActionable = "options_actionable"
}
}

struct MarketSnapshot: Codable, Hashable {
let marketState: String?
let spyChangePct: Double?
let sectorEtf: String?
let sectorChangePct: Double?
let vixCurrent: Double?
let vixTrend: String?

enum CodingKeys: String, CodingKey {
case marketState = "market_state"
case spyChangePct = "spy_change_pct"
case sectorEtf = "sector_etf"
case sectorChangePct = "sector_change_pct"
case vixCurrent = "vix_current"
case vixTrend = "vix_trend"
}
}

struct Meta: Codable, Hashable {
let mode: String?
let fetchTime: String?
let disclaimer: String?
let dataPath: String?

enum CodingKeys: String, CodingKey {
case mode, disclaimer
case fetchTime = "fetch_time"
case dataPath = "data_path"
}
}

struct Gate: Codable, Hashable {
let market: String?
let sector: String?
let stock: String?
}

/// Local 90-day replay + forward stats. Optional so bundled JSON still decodes.
struct Depth: Codable, Hashable {
/// Oldest → newest daily action codes (SETUP / WAIT / NO).
let history: [String]
let setupCount: Int?
let medianForward5dPct: Double?
let medianForward20dPct: Double?
let lastSetupAgoDays: Int?
let lastSetupForwardPct: Double?
let earningsDays: Int?
let earningsForcedWait: Bool?
let sectorEtf: String?

enum CodingKeys: String, CodingKey {
case history
case setupCount = "setup_count"
case medianForward5dPct = "median_forward_5d_pct"
case medianForward20dPct = "median_forward_20d_pct"
case lastSetupAgoDays = "last_setup_ago_days"
case lastSetupForwardPct = "last_setup_forward_pct"
case earningsDays = "earnings_days"
case earningsForcedWait = "earnings_forced_wait"
case sectorEtf = "sector_etf"
}
}

var actionCode: String {
(primary?.action ?? "WAIT").uppercased()
}

var scoreText: String {
if primaryScore?.withheld == true { return "—" }
guard let v = primaryScore?.value else { return "—" }
return String(format: "%.0f", v)
}

var isWithheld: Bool {
primaryScore?.withheld == true
}

var shareText: String {
let action = primary?.label ?? actionCode
let reason = primary?.reason ?? ""
let flexed: String
switch actionCode {
case "WAIT", "NO", "AVOID":
flexed = "Today I didn't force a trade — and that was the point."
default:
flexed = reason
}
return """
\(ticker) · QuantRadar
Score \(scoreText) · \(action)
\(flexed)

Educational only — not investment advice.
"""
}
}
5 changes: 5 additions & 0 deletions ios/QuantRadar/QuantRadar.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.443",
"green" : "0.800",
"red" : "0.180"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions ios/QuantRadar/Resources/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Loading