潮流特區

焦點文章

「心寒影滙2」大人細路齊齊玩盡Halloween!

創意生活
Cheers!・2025-10-08

2025嘅萬聖節,澳門新濠影滙為您準備好「驚嚇」同「歡樂」雙重體驗~膽大王必挑戰嘅「心寒片場」可以一站式體驗以七大電影為主題的解謎鬼屋,沉浸式感受被鬼嚇住動腦!一家大細都啱玩嘅「玩『懼』萬聖節」Toy Story 主題攤位遊戲、面部彩繪、免費爆谷同「Trick or Treat」等緊您! 9月19日至11月2日,無論您係膽大王定係家庭樂,總有一樣啱您玩! 門票現已發售,立即預訂:https://s.ctm.net/jpvOc 心寒片場 (Creepy Studio) |解謎鬼屋等您體驗 平時睇鬼片就多,親身被鬼追您又試過未?今個萬聖節,新濠影滙「心寒片場」解謎鬼屋強勢回歸!七大恐怖電影主題輪住嚇您 —《娃鬼回魂》、《鬼新娘迎親》、《邪靈娃娃》…… 一邊解謎一邊逃出,包您嚇到尖叫唔停口! 日期:2025年9月19日至11月2日(逢星期五至日)時段: 下午 15:00 - 19:00(最後入場 18:30) 晚上 19:00 - 23:00(最後入場 22:30) 地點:澳門新濠影滙一樓(巨星酒廊對面) 膽大王想獨闖「心寒片場」?標準門票MOP $144即刻出發,新濠會員更可享9折優惠!如果自問「膽細細又驚又要玩」,不如拉埋班FD一齊挑戰四人行套票,MOP 444(原價MOP $576)平均每人只需MOP $111,抵玩又刺激~ 專程從香港過來玩?咁就一定要訂「港澳船票套票」啦!MOP $404即包「心寒片場」門票1張同香港澳門來回船票1套(「香港-澳門」船票僅限於門票當天或提前一日使用;「澳門-香港」需於門票當日計算七日內來使用。),原價成MOP $584,而家慳咗成百幾蚊真係超值! 玩完驚到餓?我哋幫您諗埋~「心寒片場」星滙自助午餐套票 MOP $404(原價MOP $538),包「心寒片場」門票同自助午餐1位 (*僅限門票當日或翌日使用) ;或者選擇「嘉宴」餐飲禮券套票 MOP 404(原價MOP $544),包「心寒片場」門票同MOP $400餐飲禮券,等您可以盡情補充能量!(*僅限活動期間使用) 無論獨闖、組隊、定係遠道而來,總有一套啱您玩!立即預訂:https://s.ctm.net/jpvOc 玩「懼」萬聖節|一家大細放心玩~遊戲攤位+面部彩繪+免費爆谷! 萬聖節小朋友Cos得咁得意,點可以唔帶佢哋出嚟威?今個萬聖節就去「澳門新濠影滙一樓時代廣場」!場內設有多個《反斗奇兵》主題攤位遊戲,MOP$20換一個遊戲代幣,挑戰成功即有機會贏得Toy Story獨家禮品,大人細路都玩得開心! 新濠會員尊享優惠: MOP $200 換10個遊戲代幣,免費送「反斗奇兵三眼仔糖果桶」,小朋友可以拎住去Trick or Treat啦! 面部彩繪體驗:用2個遊戲代幣,即可為小朋友畫超應節嘅萬聖節彩繪,可愛定恐怖任您揀! 免費爆谷派發:喺時代廣場(DFS 附近)仲有免費派發爆谷,首200名客人更可獲《反斗奇兵》主題爆谷,送完即止! 遊戲攤位開放時間:2025年9月19日至11月2日(逢星期五至日) 時間:13:00 - 21:00 (面部彩繪體驗 & 爆谷派發時間:13:00 – 18:00 ) 地點:澳門新濠影滙1樓時代廣場 Trick or Treat?|糖果大放送! 小朋友最愛環節嚟啦!9月19日至11月2日期間(每日都有!),只要喺新濠影滙精選商戶門口大叫「Trick or Treat?」,即可免費拎糖!實現糖果大豐收~ 萬聖節點止有得玩?新濠影滙「搞怪美食」驚喜登場! 玩到肚餓?唔使驚!新濠影滙四間餐廳齊推萬聖節主題限定套餐,搞怪得黎又好味,絕對係視覺與味覺嘅雙重享受~ 載運美式餐室嘅木乃伊開心套餐 / 骷髏骨頭開心套餐 / 惡魔魅影開心套餐,隨餐仲送萬聖節公仔掛飾,即時補充您係鬼屋尖叫流失嘅能量! 甜品控必衝羅浮餅廊及輕食嘅造型蛋糕, 小編最期待嘅就係「骷髏朱古力紅桑子蛋糕」同「紅桑子十字架丹麥 」,造型驚悚但味道驚喜,保證您相機食先! 想同朋友Chill 住打卡? 巨星酒廊萬聖節Tea Set絕對唔輸蝕 — 「怪獸之眼」、「吸血南瓜」、「木乃伊手指」、「女巫紅寶石」… ……款款精緻得嚟帶點詭異,包您一邊驚一邊笑! 最後當然唔少得星滙餐廳自助餐,多款萬聖主題美食任您放題,仲有一系列可愛到「嚇死人」嘅甜品,等您視覺同味覺同時被攻擊! 驚聲睇好戲|萬聖節電影優惠 喺鬼屋尖叫完,點可以唔睇返場戲壓壓驚?活動期間憑當日「心寒片場」門票,或者著住萬聖節服飾,去「影滙戲院」買飛即享: 第二張戲飛只要 MOP 60元! (即場出示當日「心寒片場」門票或穿著萬聖節主題服飾即享優惠) 一邊食爆谷一邊睇戲,萬聖節之旅完美收官~ 活動日期:2025年9月19日至11月2日地點:澳門新濠影滙戲院票務處 今個萬聖節唔使諗去邊,新濠影滙包大人同細路盡興而歸!記得約實朋友、帶埋一家大細嚟玩啦!

最新文章

Spring Data 關聯型態 02

科技新知
MacauYeah・2024-08-09

Presist and Casecade 前次的文章,講了一些Spring Data最基本的關聯概念,但當要正式儲存或刪除,就有些考慮完整性問題。平常我們在處理資料庫的關聯表格時,也需要面Foreign Key的正確性問題。同樣地,Spring Data也有這方面的考量,但它有提份一個很方便的CascadeType選項,可以簡化一些流程。 假設你只能存取Parent Repo,那你需要在Parent中,加入CascadeType.All。當repo.save(parent)時,它就會順多把所有child的也一併進行Save,你也不需要有Child Repo的存在。 @OneToMany(mappedBy="parent", cascade = CascadeType.All) List children = new ArrayList(); 但在複雜的狀況下,例如你不想在更新parent的情況下,不小心弄到child,特別是經過public web下的API操作,你對web client的資料正確性有存疑,就不要使用CascadeType了。這也是筆者認為在大多數情況下,我們都會把Parent和Child的CRUD分開操作,然後根據需要使用各自的repo save。 如果你一定要用CascadeType.ALL (CascadeType.REMOVE),就要再留意刪除的問題。為什麼?因為刪除 parent,其實指的是某個parent不再存在,但不代表child也要一起刪除,child的parent連結可以變為null,也有重新連結其他parent的可能。 如果大家確定需要共同刪除,就可以用CascadeType.ALL 或 CascadeType.REMOVE。 還有一個新的選擇,orphanRemoval = true,也有類似效果。 @OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE) List children = new ArrayList(); // or @OneToMany(mappedBy="parent", orphanRemoval = true) List children = new ArrayList(); // or @OneToMany(mappedBy="parent", cascade = CascadeType.REMOVE, orphanRemoval = true) List children = new ArrayList(); 筆者測試過,混著用也是可以的。若大家看過其他教程,可能會覺得orphanRemoval = true 和 CascadeType 總是一起出現,但它們其實是分別操作的。單獨使用orphanRemoval = true,有時候則是為了不會出現無主的child,但這不代表parent和child的想要同步更新。 JPA Entity 的生命週期 Spring Data跟傳統的資料庫Selete,Create,Update,Delete SQL 語句有所不同。也就是這個不同,它的CascadeType比資料庫的Cascade Update和Cascade Delete更強大。 Spring Data 預設其實是使用 jakarta.persistence.EntityManager,每個Entity主要分為四個狀態 Transient / New - 不在EntityManager的掌控中 Managed - 在EntityManager的掌控中,將會在下次flush時,變成sql create或update statement Detached - 脫離EntityManager的掌控,不受flush影響 Removed - 在EntityManager的掌控中,將會在下次flush時,變成sql delete statement 在Spring Data / Jpa 以前,我們若要直接操作Hibernate,經常見到persist, remove的寫法 entityManager.persist(entity); entityManager.remove(entity); entityManager.detach(entity); entityManager.merge(entity); 其實persist就是把處於Transient、Removed的entity,改為Managed。而remove就是把Managed改為Removed。detach,merge也類似,就是Managed,Detached之間互換。 EntityManager最強大的是,它可以讓程序員不需要再為Managed狀態下的entity操心,它會自動判別下次flush,應該create還是update,如果完全沒有改動的,連update也不會執行。 (註,flush和commit也有不同,flush就是從java寫到資料庫中,在資料庫commit前,還可以使用rollback放棄。) 而Spring Data,則是進一步簡化,它把persist改為save,remove改為delete,然後自動選擇flush的時機。 CascadeType 在解釋完Entity 的生命週期後,終於可以回到CascadeType了。這裏的CascadeType不是資料庫的Cascade操作,其實它是指EntityManager的狀態操作是否有傳遞關係。亦即是,persist(parent)時,要不要連同child也一起操作? 我們查看 CasecadeType 的原始碼,就可以發現可以被傳遞的操作共有以下這些 PERSIST MERGE REMOVE REFRESH DETACH ALL (以上全部) 這裏的 CasecadeType.PERSIST ,跟資料庫的 Cascade Update 是不一樣的。資料庫裏的 Cascade Update,是指當 Parent 的 Primary Key 有變,對應child的 Foreign Key也一起變。但因為 JPA Entity 的機制, Parent 的 Primary Key 不可以改變,理論上不會發生類似資料庫的 Cascade Update,頂多有 Cascade Delete。 CasecadeType.PERSIST 就像之前述的生命週期解說一樣, 把 parent和 child 一起拉到受管理的狀態。 註: CascadeType.REMOVE有點尷尬,似乎有更特別的使用規範。筆者測試過,在某些情況下,CascadeType.REMOVE無法處理ForeignKey問題,又或者是,刪除的順序不對。詳見 spring boot data deletion Reference entity-lifecycle-model spring boot data deletion

Spring Boot 06 - Spring Boot Web 調試工具

科技新知
MacauYeah・2024-08-02

之前兩節,都一直在講怎樣寫code,也介紹了Test Case的好。若為初次接觸,Spring有很多設定需要摸索,若開始時就設定錯誤,對不少人來講都會有很大打擊。在這裏,筆者就介紹一些vscode和spring的工具,可以讓IDE多幫忙一下,減少走歪路的機會。 vscode插件 以下兩個插件,都在於提示用戶設定。 Spring Boot Dashboard (vscjava.vscode-spring-boot-dashboard) 可以那它來運作spring boot app,省去找尋main 位置的麻煩 綜覽整個程式中的所有Bean (Bean是一個很重要的元素,日後會再提及) 若程式為Spring boot web,可以顯示所 http endpoint。 Spring Boot Tools (vmware.vscode-spring-boot) 檢查設定檔的設定值有沒有寫錯 (application*.properties, application*.yml) 綜覽檔案中的有以@為首的與spring相關的元素(檔案很大時就會有用) 可以在IDE運行spring時,查看@元素的bean資訊 (not works ?, 加了actuator也是沒有看見) Spring Initializr(vscjava.vscode-spring-initializr) 經網絡初始化spring 專案的依賴引用設定 Maven for Java (vscjava.vscode-maven) 若大家在使用Spring Initializr時,選取了maven作管理工具,那麼這插件就可以在後續幫忙更新引用。 若專案的Spring 及㡳層引用有變,vscode也需要它來引用更新。 這是java 開發工具包(vscjava)的其中一員,它的其他插件也可以順帶安裝。 調試工具 - open api / swagger-ui 如果我們在開發Web http API ,其實都是為了該某個客戶端使用。但如果該客端明白我們的API該怎樣使用,大家總不會逐個連結,自行編寫使用手冊及範例吧。所以就有了open api 和 swagger-ui 的旦生 。 open api,就是一個公認的使用手冊標準,我們只要在spring-web中加入 springdoc-openapi-starter-webmvc-ui 的程式庫,就可以自動為我們的controller 生成 open api 的說明檔。 更強大的是,這個程式庫可以利用剛生成的open api,配上 swagger-ui ,自動測生一個可供測試的頁面。這個頁面可以供碼農們直接操作,也會產生對應的 curl 指令,讓碼農們可以在任何的主機上重複。這樣,那麼是沒有太多解釋的說明文檔也可以使用。 做法很簡單,在pom.xml中加入依賴。 org.springdoc springdoc-openapi-starter-webmvc-ui 2.5.0 (由於安全性問題,上述程式碼未能完整顯示,請參見文末完成Source Code) 然後我們就可以加入Controller,運行 spring 後,我們可以在 http://localhost:8080/swagger-ui/index.html 找到 swagger 的頁面,然後就可以在 ui 上測試API了。 躲在Proxy背後的 swagger 如果你跟筆者一樣,使用 code-server 或 github codespaces ,你就不能很隨意地連接到 8080 端口。你只能經過Http Proxy去訪問。這樣 open api的原有的設定就不合用了。 這時我們需要自行修改 open api 的 bean,加入我們真正的根路徑。然後筆者使用 code-server,而IDE只會在port 9000上執行,它對外的前置路徑會是 http://localhost:9000/proxy/8080/。 @Bean public OpenAPI springShopOpenAPI() { Server server = new Server(); server.setUrl("http://localhost:9000/proxy/8080/"); return new OpenAPI().servers(List.of(server)); } (由於安全性問題,上述程式碼未能完整顯示,請參見文末完成Source Code) 然後訪問 http://localhost:9000/proxy/8080/swagger-ui/index.html,還會發現 "Failed to load remote configuration." 。但你可以在 "explore" 搜尋欄位內貼上 http://localhost:9000/proxy/8080/v3/api-docs,再一次搜尋檔案,就回復正常了。 註:如果你熟習Nginx這類Reverse Proxy ,你的環境有條件直接修改 Request Header,加入X-Forwarded-*,就不用煩惱寫Bean了,也不用手動在explore裏重新修正api-docs的位置。詳見 https://springdoc.org/index.html#how-can-i-deploy-springdoc-openapi-starter-webmvc-ui-behind-a-reverse-proxy Controller的繼承 Spring Controller的 @ 標記 (Annotation) ,其實支援繼承的。經Spring 生成的 api docs,也有如何效果。例如以下程式碼 public class ParentController { @GetMapping("/postfix") public String postfix(){ return "this is postfix"; } } @RestController @RequestMapping("/api") public class ChildController extends ParentController { @GetMapping("/direct") public String directCall() { return "direct result"; } } (由於安全性問題,上述程式碼未能完整顯示,請參見文末完成Source Code) 在ChildController的實例中,它會有兩個API,分別是 /api/direct /api/prefix 它支援Java Function Overwrite(覆寫),但不能改 @ 標記,以下就是一個錯的例子 @RestController @RequestMapping("/api") public class ChildController extends ParentController { @GetMapping("/Overwrite") // 把這個 @ 行刪了才能正常執行 public String postfix(){ return "this is Overwrite"; } }(由於安全性問題,上述程式碼未能完整顯示,請參見文末完成Source Code) Source Code spring boot web api doc

Swarm mode 上線 4 | IP 設定

科技新知
MacauYeah・2024-07-23

單機模式 IP設定 平常我們自己做測試,網絡功能通常用預設的就好。但當我們的Docker Container需要存取在區域網內的其他資源,避晚IP網段相衝是必需要的事。 大部份情況下,單機Docker使用的預設IP段會是 172.17.0.0/16 172.18.0.0/16 ... 若然現在區域網中,有一段172.18.0.0/24,大家不想Docker踩到其中,可以修改設定檔,加入預設的default-address-pools,以後它就只會從指定的區段使用。 # vim /etc/docker/daemon.json { "default-address-pools": [ { "base": "172.17.0.0/16", "size": 24 }, { "base": "172.19.0.0/16", "size": 24 }, { "base": "172.20.0.0/16", "size": 24 } ] } 其中base,是docker可以操作的總區域,size指的是Docker要自行分段的話,每段的大小是多少,上述的例子,就代表未來可能有以下Docker 網段。 172.17.0.0/24 172.17.1.0/24 ... 172.17.255.0/24 172.19.0.0/24 172.19.1.0/24 ... 172.19.255.0/24 172.20.0.0/24 172.20.1.0/24 ... 172.20.255.0/24 修改完設定後,重啟Docker就會生效。當然,重啟前,先刪除所有不在預設範圍的所有Container。 Swarm模式 IP設定 Swarm模式,與單機差不多,它需要在初始化Swarm就要定義,而且它不能與單機的網段有重疊。單機會預設使用Private IPv4 Class B,Swarm則是預設使用Private IPv4 Class A段,所以我們若就更改,就使用10.x.x.x吧。 docker swarm init --default-addr-pool 10.1.0.0/16 --default-addr-pool-mask-length 24 經上述例子初始化的 ingress 網段,將會是 10.1.0.0/24,隨後每個stack 則會是 10.1.1.0/24 10.1.2.0/24 10.1.3.0/24 重置Swarm 跟單機的情況類似,如果已建立Swarm後才修改網段,還是要整個刪掉重來。 每個節點都要執行以下指令。 docker swarm leave --force 實測swarm leave這個指令也會把所有運行中的stack刪掉。 各節點重新建立swarm # in node 1, init new swarm with new ip docker swarm init --default-addr-pool 10.1.0.0/16 --default-addr-pool-mask-length 24 # in node 1, get new manager token docker swarm join-token manager # in node 2 and node 3, join node 1 with new token docker swarm join --token XXXXX YOUR_NEW_NODE1_IP:2377 雙管齊下 如果大家同想要修定單機及Swarm的網段,還要留意有一個特別的網段docker_gwbridge。它雖然是Swarm的附帶產物,但它則是受單機的網段控制。也就是,如果大家有需要同時修改單機及Swarm的網段,則需要手動刪除Swarm及docker_gwbridge 在每個節點先刪掉舊有的Swarm及docker_gwbridge,並關掉docker docker swarm leave --force docker network rm docker_gwbridge 在每個節點為docker_gwbridge修改設定,然後重起docker # vim /etc/docker/daemon.json { "default-address-pools": [ { "base": "172.17.0.0/16", "size": 24 } ] } 然後像前述一樣,重起Swarm。

【James Altucher的Unilateral Pairs Trading 策略- 5年回報627.75%】

創富坊
程式交易 www.quants.hk (導師: 財經書藉作家: 麥振威)・2024-07-22

在金融市場中有一個人物頗具爭議性,他曾經身家由逾千萬美元跌至一無所有,其後又輾轉變得富有。他便是James Altucher,現在大家基本上經常看到他演講的內容都是與個人成長及心靈有關。 James Altucher試過銀行戶口只剩下143美元,但其後又把身家翻至1500萬美元,要說心靈的故事他自然有很多東西可以發表。但他除了創業做生意外,由於曾在多家對沖基金工作,所以一直都有投資股票及加密貨幣。 對他有興趣的讀者也可以留意他的blog: https://jamesaltucher.com/blog/ (圖一) 他blog內的內容有些其實也有參考價值,例如他寫過一篇題目為《THE PERFECT INVESTMENT STRATEGY》的文章,他會說自己本質上是一個非常簡單的人,並不真的喜歡投資,只喜歡學習,喜歡遊戲,喜歡看電視,寫作,做播客等等,還有非常喜歡睡覺。他認為完美的交易策略就是在生活中找到最具潛力的領域,然後買入這個行業中全部的股票。 例如你在1970年到1990年間看好電腦行業,然後你投資每一家即將要上市的公司,假設你買入了合共100家電腦公司股份,每家用1,000美元去買,那你總共投資了10萬美元。但這100家公司中有98家公司最後都破產,不過這並不重要,因為存活下來的2家公司能把你的資產翻至350萬美元。若超過兩家公司能存活,你能賺取的利潤會更多。 除了blog外,他還寫過兩本書,分別是《Trade Like a Hedge Fund》及《Choose Yourself!》。(圖二) 《Trade Like a Hedge Fund》這本書在2004年已出版,筆者就頗為喜歡,若中文版的名稱應該較多人聽過,中文譯名為《20招成功交易策略》,書中他提及的分析方法其實很值得參考。首先他認為交易策略應該簡單的策略才是交易中最穩鍵的。但所謂簡單的策略,又不是像Larry Connors那種初級班的策略。 可以說James Altucher的策略是由複雜的策略進行簡化,目的就是提高真實交易時的執行加,這與那些RSI(2)超賣再超賣的策略並不相同。 James Altucher在《Trade Like a Hedge Fund》中曾經提及一套名為「Unilateral Pairs Trading」的策略便很值得參考,筆者研究Pair Trade的方法已經很久,而James Altucher也在書中道出了Pair Trade的關鍵,他認為Pair Trade雖然對市場的方向是中立的,意思是你沒有估市況升/跌,但實際上對兩個產品的差價是有偏見的,做Pair Trade的人是在估計差價會擴大還是縮小,所以也會要去估,而非完全沒有任何預測就能賺錢。 但Pair Trade的問題是,炒家同時運用兩個「工具」來做交易,在真實交易時當兩個工具的價格也變得不尋常之時,炒家就不只面臨一個資產的風險,要處理的事情就會更多,而且也存在兩個資產同時虧損的可能性。(圖三) 不過,運用Pair Trade的人都是十分擅長去估計兩個工具之間的差價,有些人擅長預測股價,有些人則會認為預測兩個工具的差價變動會較容易。不過,差價的波動會較股價為少,而且若市場越來越多的人在做Pair Trade,差價會變得更少,因此炒家若要追求更高的回報,就需要利用槓桿,但風險也會因而提升。 所以James Altucher認為,其實可以進行單邊對沖交易,那就是雖然觀察兩個資產的價格差距來做交易,但最終只會買入或沽空其中一個資產,他認為這樣做其實更好,因為其中一個資產的價格變得很不合理時,炒家會預測價格會回歸正常值,假設兩個資產中更為波動的一個資產偏高時便直接沽空,偏低時便直接買入,根本不用兩個資產同時交易,最終也是能達到預測價差收窄的結果,但進行單邊對沖交易就更加有靈活性,而且風險相對較低。 但「Unilateral Pairs Trading」不只適合交易正股,若用以交易流動性更大的ETF如QQQ及SPY等,James Altucher認為效果會更佳。筆者則把相關策略修改,再用SQQQ的數據作測試,結果也證實不俗。下一篇文章會告訴大家 James Altucher所研究的「Unilateral Pairs Trading」詳細內容。 (來看Patreon文章) 筆者Patreon: https://www.patreon.com/quantshk 網頁:www.quants.hk

將583.15元翻至1000萬的Ross Cameron 用了什麼交易策略? (一)

創富坊
程式交易 www.quants.hk (導師: 財經書藉作家: 麥振威)・2024-07-16

在Youtube上大家經常看到很多教授交易策略的外國炒家,如2016年曾贏得「The Million Dollar Trader Competition」的Nial Fuller、聲稱將583.15元翻至1000萬的Ross Cameron,還有TradingMarkets.com創辦人Larry Connors等。 之前已介紹過Larry Connors的R3 Trading Strategy,今天想跟大家討論有關Ross Cameron的事跡及他的交易策略。 Ross Cameron的影片相信很多讀者都在Youtube看過,看他的樣子就看不出已經58歲,若稱它為Daytrade大師應該大家也很同意,他的Youtube名稱就稱為「DaytradeWarrior」,中文就是「日內交易戰士」。 https://www.youtube.com/@DaytradeWarrior Ross Cameron曾經透過Daytrade將約400萬翻至1000萬。而且他出售的一套交易系統,大約有超過2萬人各用了數千美元購買,不過後來就被聯邦貿易委員會指他的公司通過虛假和不切實際的承諾銷售系統,而購買的客戶大多數也沒有任何投資上的回報。 筆者沒有買過他的系統,也絕不建議大家去買,他的影片提及很多的交易技巧其實也只是「老生常談」,首先他認為Daytrade是最有利可圖的交易類型,因為即市開倉及平倉就避免了持倉過夜出現重大虧損的機會。但他強調Daytrade也必需有良好的風險管理,你要賺得更多,自然就需要冒更大的風險,所以在有限風險下不可能期望市場會每天給你很驚人的回報,甚至有很多日子根本是沒有回報的。 另外他強調,若要做Daytrade,本金至少要有25000美元,大約是195000港元。因為市場是一個很「嚴格」的領域,而且Daytrade不可能每天都賺錢,若本金根本不足,可能很也便會輸光離場。 雖然他主力是做「low float stock」,不過他提出的交易策略也有一些是值得參考的,有些準則也適合應用在一些熱門股之上。 1)交易中的3-5-7規則: 先要計算價格持續上漲或下跌的天數、若用小時圖則看有多少個小時,若用5分鐘圖則看有多少個5分鐘,然後大家會發現,在第三、第五或第七根陰陽燭上,都會是相反方向出現的時間。 筆者就覺得若應用在Daytrade上,例如觀察5分鐘圖表,連續3根陰陽燭的價格都在下跌是經常出現的情況,故此未必有參考價值,而連續5根陰陽燭的價格也在下跌,這種情況雖較少,但也不一定會出現反彈,但若連續7根陰陽燭的價格都在下跌,基本上很少出現,但若出現時,價格確實很大機會反彈。 如(圖一)是Tesla(US:TSLA)的5分鐘圖,2023年12月14日當日便出現這種情況: 2) 3天法則 由於每個投資者都必須在三個工作日內結算他們的證券交易。他的意思是用現金戶口,這個結算週期被稱為「T + 3」,若用孖展戶口則可以「T+0」。而現金戶口的「T+3」規則意味著當投資者購入股票後,證券交易商必須在交易執行後的三個工作日內收到投資者的付款。但若投資者未能付款就必需賣出股份,故此股市中若連續上升三天後都大多會調整,又或者出現升勢放緩。 但筆者就認為,這個3天法則在「弱市」中才較多出現,在市況十分強的情況,3天法則並不適用。 如2022年與2023年便有很大的分別,2022年屬於「弱市」,2023年則屬於「強市」,3天法則在2022年就經常出現。 例子: (圖二) Apple(US:AAPL)的日線圖上,2022年經常會看到上升3天後便升勢放緩的情況: 但同樣看Apple的日線圖,在2023年裏3天法則並不適用: (圖三) 除了以上的兩個法則,Ross Cameron也講解過如何去選股,以及何時應該入市,他稱之為「momentum-day-trading-strategy」。 這套交易策略每天只會交易兩個小時,但交易前卻要有很多的準備,首先就是要找出具備「動能」的股票,要透過Daytrade實現利潤,Ross Cameron認為選股的過程十分重要,一些具備「動能」的股票甚至可以一天內上升20%至30%。但這些股票幾乎每天都有的,只是大家能否找到它們。 Patreon 的文章有介紹他的momentum-day-trading-strategy,以及他的交易策略若用Trading View寫出來後的backtest結果如何。(來自Patreon文章) 筆者Patreon: https://www.patreon.com/quantshk

Spring Data 關聯型態 01

科技新知
MacauYeah・2024-07-16

筆者身邊的朋友,首次接觸 ORM 的關聯型態時都會覺得很難,筆者自己也是。但在好好地理順它的設計時,就會覺得其實很簡單。 因為篇輻很長,我們先以Code First的角度,先體驗一下ORM程式讀取的便捷性,以及解決一個常見的序列化問題。 雙向存取 例如一個Parent,有好幾個Child @Entity public class Parent { // ... Parent Primay Key @OneToMany(mappedBy="parent") List children = new ArrayList(); // TODO add remove } @Entity public class Child { // ... Child Primay Key @ManyToOne Parent parent; } 上述的寫法很簡潔,ORM會為你自動加入join column,處理關聯的載入。在讀取Parent時,它的所有Children就可以直接在Java層面讀取,在讀取Child時,它的Parent也隨時取得。也就是,開發人員只要經SQL準備其中一方的資料,另一方並不需要手動準備,它就可以自動按需載入。 RESTFul API 坑-雙向存取 Spring Data在Java層面的雙向存取,已經做到很方便。但經常坑到我們的是Spring Data與RESTFul API的混合應用。當我們嘗試經API回傳我們的Parent Json時,API會很聰明地把關聯的Children也變成Json回傳。但他也會把child中的parent不斷重複變成json,變成無限輪迴。 坊間有兩種不同的解決方案,可以防止無限輪迴。 讓Json可以認得已經序列化的元素。@JsonIdentityInfo 讓Json只可以單向序列化(serialization)。@JsonManagedReference, @JsonBackReference, @JsonIgnore 筆者兩個方向都試過,但首個方法並不通用,至少它不能算是一般常見的無腦Json結構。它需要伺服器、客戶端都懂這如何經IdentityInfo認得重複出現的元素。 而單向序列化,是筆者現時的通用解。在設計RESTFul READ API時,筆者就會決定到底是Parent自動回傳Child,還是Child自動回傳Parent。決策的考慮因素,主要在於是否可以簡化Client的API調用次數。通常從Parent出發,自動回傳Child,可以節省API調用。但如果是選項性的結果(List of Value),就倒過來。有時候,遇著API需要雙向設計,就只好自己設計DTO資料傳輸對象 (Data transfer object, DTO)。 例如Parent API,就原封不動回傳原本的元素 @Entity public class Parent{ // ... Parent Primay Key @OneToMany(mappedBy="parent") List children = new ArrayList(); } @Entity public class Child { // ... Child Primay Key @ManyToOne @JsonIgnore Parent parent; } Child API,就反過來引用。 public class ParentDTO { // ... Parent Other fields except children } public class ChildDTO { ParentDTO parent; // ... Child Other fields } 這種DTO,看起來很麻煩。但其實Spring有提供一個簡便的複制DTO功能,它可以把自動複制兩個class中有同一名稱、同一型別的欄位到另一個class上,不需要逐個欄位明文寫出來。 BeanUtils.copy(child, childDTO); BeanUtils.copy(parent, parentDTO); childDTO.setParent(parentDTO) // 因為child、childDTO中的parent欄位型別不同,BeanUtils.copy會自動忽略,其他欄位就會自動複制。 註: 其實古早的網頁系統設計,DTO的概念一直存取。只是現在RESTFul API的流行,很多框架已經提向便捷的Json轉換。若然平時只需Json單向存取,筆者還是省略DTO的建立。

如何提高保歷加通道交易策略的勝算

創富坊
程式交易 www.quants.hk (導師: 財經書藉作家: 麥振威)・2024-07-10

我們已學習了一些pine script的基本語法,相信大家經練習後已能逐漸掌握。這篇則會講解一些比較深入的分析技巧。 有一種策略是很多新手都經常問我的,若要寫股價跌穿保歷加通道底部便買入,升穿保歷加通道頂部便造淡,應該怎樣用pine script 寫出來? 以下的入市準則是,收市價低於保歷加通道底部便買入,然後待收市價跌穿保歷加通道中軸便平倉,相反,收市價高於保歷加通道頂部則造淡,然後待收市價升穿保歷加通道中軸平倉。 //@version=5 strategy("升穿bollinger's band錯誤用法", overlay=true, margin_long=100, margin_short=100) sma20=ta.sma(close,20) mult=ta.stdev(close,20) upper=sma20+2*mult lower=sma20-2*mult noposition=strategy.position_size==0 var bool traded =false buyCond=close<lower and close<sma20 shortCond=close>upper and close>sma20 buycloseCond=ta.crossover(close,sma20) shortcloseCond=ta.crossunder(close,sma20) if buyCond and noposition and not traded strategy.entry("BUY",strategy.long) traded:=true if buycloseCond and not noposition strategy.close("BUY") if shortCond and noposition and not traded strategy.entry("SHORT",strategy.short) traded:=true if shortcloseCond and not noposition strategy.close("SHORT") if ta.change(time("D"))!=0 traded:=false 這是最簡單的寫法,但大家應會想到,若收市價低於保歷加通道底部便買入,那以下情況可能會出現「連續多次買入」,所以在策略中已設定了每天只交易一次,而且入市情況需要「沒有持倉」才會入市。 可以看到這類交易策略,交易一年後仍然要虧損,數據是用了Tesla(US:TSLA)的5分鐘數據,而這個策略在一年裏交易了259次,獲利的有151次,勝率大約是58.3%。 若要修改這類運用保歷加通道頂部及底部的策略,其實比較好的處理方法是,當股價升穿保歷加通道頂部後,等待股價再回落至保歷加通道之內才入市造淡,同樣地,若股價跌穿保歷加通道底部,也是等待股價回升至保歷加通道之內才入市造好。 寫這類策略的方法是運用ta.crossover 及ta.crossunder, 若寫成ta.crossover(close,lower),就是代表了股價由保歷加通道底部以下,升穿保歷加通道底部之時便會入市買入。 可以想想,要出現這種情況必然是股價之前已經跌穿了保歷加通道底部才會發生,那便既符合跌穿保歷加通道底部的要求,同時又符合了股價再回升至保歷加通道內的要求。 而升穿保歷加通道頂部後再待股價回落至通道之內的寫法應大家現在也懂得怎樣寫,那便是ta.crossunder(close,upper)。 以下是整個策略的完整寫法: //@version=5 strategy("升穿bollinger's band及跌穿bollinger's band策略", overlay=true, margin_long=100, margin_short=100) sma20=ta.sma(close,20) mult=ta.stdev(close,20) upper=sma20+2*mult lower=sma20-2*mult noposition=strategy.position_size==0 var bool traded =false buyCond=ta.crossover(close,lower) and close<sma20 shortCond=ta.crossunder(close,upper) and close>sma20 buycloseCond=ta.crossover(close,sma20) shortcloseCond=ta.crossunder(close,sma20) if buyCond and noposition and not traded strategy.entry("BUY",strategy.long) traded:=true if buycloseCond and not noposition strategy.close("BUY") if shortCond and noposition and not traded strategy.entry("SHORT",strategy.short) traded:=true if shortcloseCond and not noposition strategy.close("SHORT") if ta.change(time("D"))!=0 traded:=false 從backtest report可以看到勝率會較第一個的策略為高,一年裏交易了258次,獲利的有167次,勝率提高至64.73%,但最重要的是原本是虧損的策略已變成輕微獲利。 不過,獲利確實不多,那又有沒有方法可以改得更好? 最常見的做法是觀察圖表上的入市訊號,特別是留意出現裂口高開或裂口低開的情況,因為不少人都會認為出現裂口高開或裂口低開會引發上日持倉過夜的炒家的平倉盤,但這並不代表當日即市的走勢,只會在開市初段產生短暫影響。 而我們在圖表上觀察這個策略的入市訊號時,又確實發現有些日子的造淡的訊號會因為當日出現裂口高開,因而升穿了保歷加通道頂部,其後股價重返保歷加通道之內便入市造淡,不過最後卻出現虧損。 那若我們剔除因裂口高開而出現的入市造淡訊號又會怎樣? 但這種修改方法又很可能把原本能獲利的訊號也剔除的,結果是交易表現可能更差。 筆者就建議可以試試與平均線配合作修改,例如運用「Hull Moving Average,HMA」,這是一種了特別重視最近價格變動的加權移動平均線,有點像「Weighted Moving Average,WMA」,但HMA的滯後情況會較WMA少。 我們試試加上一些新的條件,買入時必需HMA比上一支陰陽燭的HMA為高,造淡時則必需HMA比上一支陰陽燭的HMA為低。 以下便是修改後的版本,在主圖上的紅線便是HMA。 //@version=5 strategy("升穿bollinger's band 改良版", overlay=true, margin_long=100, margin_short=100) sma20=ta.sma(close,20) mult=ta.stdev(close,20) upper=sma20+2*mult lower=sma20-2*mult noposition=strategy.position_size==0 var bool traded =false hmaValue=ta.hma(close,10) buyCond=ta.crossover(close,lower) and close<sma20 shortCond=ta.crossunder(close,upper) and close>sma20 buycloseCond=ta.crossover(close,sma20) shortcloseCond=ta.crossunder(close,sma20) buyCond2=hmaValue>hmaValue[1] shortCond2=hmaValue<hmaValue[1] if buyCond and noposition and not traded and buyCond2 strategy.entry("BUY",strategy.long) traded:=true if buycloseCond and not noposition strategy.close("BUY") if shortCond and noposition and not traded and shortCond2 strategy.entry("SHORT",strategy.short) traded:=true if shortcloseCond and not noposition strategy.close("SHORT") if ta.change(time("D"))!=0 traded:=false plot(hmaValue,title="HMA",color=color.red,linewidth=1) 結果可以看到訊號大幅減少,但勝率再提升至75%,但看最大獲利的最大虧損的比例達到3:1,這類策略即使遇上最壞的情況也是虧損有限。但問題就是交易次數真的真的太小的,一年只有8次入市機會,雖然獲利的次數有6次,但交易次數太小也會令最終的回報有限。 但大家可以想想,若你把「10個」本來只有六成中的交易策略,修改至七成中以上,而且Maximum Drawdown不大,盈虧比更大幅提升至3比1,那麼你獲利的機會根本便很大。 然後我們這10個策略同時執行,那你的交易次數就不會少了,而回報也會因而增加。若為了達到有足夠多交易次數的目的而勉強去運用一些勝率較低,盈虧比又較低的交易策略,那最終的回報反而不會太好。 網頁: www.quants.hk Youtube: https://www.youtube.com/@markchunwai Facebook專頁: https://www.facebook.com/quantshk/ Patreon: https://www.patreon.com/quantshk

Docker Image打包建議

科技新知
MacauYeah・2024-07-10

之前筆者有分享過兩個不同的Docker Image打包方式 App直接打包成Image 只把底層程式打包在Image中(例如Tomcat),再用Docker Volume的方式讓Container可以起動App。 筆者就兩種方式做了一個條列式的對比。詳見連結 https://macauyeah.github.io/AProgrammerPrepares/VMDockerNotes/DeployDockerClusterCN.html 因為兩種方式筆者都有實作過,也算用了很段時間,所以也有一些實際經驗可以分享。 如果大家上正式的Docker課程,Docker導師通常會推薦為每個App打包成獨立Image,因為底層程式的Overhead通常不大,例如底層程式是Tomcat、Apache、Nignx這類網頁伺服器,重量級的開銷並不是因為多幾個Web Engine的分身造成,通常都是因為業務本身。但如果你講的底層程式是資料庫等的大型程式,才可能會有明顯的差異。 但實務上的建議,就是必需考慮自身的經驗,到底那個方案自己比較有把握。獨立打包App,在正式環境也需要考慮跟蹤問題的情況,多個不同App要溝通,也是了解Container網絡。如果打包底層程式,所有App都可以當成是本機下運行,更有信心追蹤問題,也是一個很好的出發點,到了有需要彈性改變不同App的需要,才轉向獨立打包的做法。 筆者最初也是走這個打包底層程式的方向,到了自己有信心試用Docker Swarm,才走向獨立打包的做法。筆者親身經驗,因為到了Docker Swarm,網段會變得暴增,這跟公司現有的內部網絡相衝的機會就會變多。在Swarm起立初時,筆者並沒有意識到這件事,所以當初排查問題,也花了一些時間才知道要向網段衝突上著手。 另一個出自Docker導師實務上的建議,就是正式環境中不要做用Docker compose,應該使用Docker Swarm。那怕Swarm只有一個節點,也應該用Swarm,導師的主要理據是Swarm有Rolling Update (滾動更新)的機制。同一個node也可以有多個分身,每個分身輪流更新,就不會出現大中斷的情況。筆者就自身經驗,Tomcat可以同時容納一個App的多個版本,Nginx也有Failover(故障轉移)等,如果你很熟這些功能,不一定要需要靠Swarm去提供。可以按自己步調去慢慢適應。

型別對程式語言的重要性

科技新知
MacauYeah・2024-07-08

JavaScript等程式語言的流行,好大一個原因是因為它很簡潔。而筆者認為,動態語言的特性,即是可以省略型別,是讓它簡潔的一個很大原因。(動態、靜態與強型別、弱型別並一定對等,詳見Ref) 動態語言的特性,就是同一個變數,在不同時候可能代表不同的數據類型,有時候是String,有時候是Integer。所以編寫時,乾脆就不寫數據類型,因為寫了也可能是白寫。 因此初學者並不需要處理大多導入(import)問題,也不用考慮很多compile error問題,至少程式可以運行一半,到了最後出錯的地方才停下,也就是不會因為型別問題而整個程式開不了。 不過筆者在接觸了JavaScript後,始終沒有大量使用。一來因為筆者慣用的Java,有著更大的基礎套件,改用JavaScript未必有優勢。而且動態語言還有一個長久的管理問題,我們該如何知道更新的影響有多大? 測試用例不是萬能藥 有一部份的人認為,動態語言管理難,是因為大家不愛寫測試用例。的確,若然大家寫的測試覆蓋率足夠多,一定可以預先發現問題。但筆者在Java上實踐了寫測試的習慣一段時間,依賴測試報錯,其實也是後知後覺。 IDE的界入 筆者認為,若想好好地管理程式碼,光寫測試是不夠的,我們還需要好好地讓IDE了解我們的程式碼,認它可以很有效地重構我們的程式碼。更強的IDE,還有機會可以提醒我們有一些設計上問題。 老實講,寫Java多的朋友,都可能都知道Intellij Ultimate的名字。筆者試用後,的確很有幫助。相較之下,vscode對於Java的支援,並不十分智能。但這裏筆者還覺得vscode對於java的編寫、重構、測試,在免費的情況下,都已經足夠是足夠佛心。對於網頁應用來講,vscode差的是對javascript的支援。 vscode對javascript的支援有限,其實不能怪它不夠努力。你想多一個免費的IDE怎樣去了解你的javascript程式? 我們連型別都沒有寫出來,它能怎樣推敲? 實時去模擬各種輸入?CPU又會不會耗乾?那麼寫到一半的程式碼又怎樣輸入? 直到最近筆者採用TypeScript之後,筆者看到曙光了 TypeScript - 一個變相的JavaScript的靜態型別 原本的JavaScript其實也有型別的,只是不強制。若想IDE支援,需要以特定型式寫註解。但這樣寫註解,工作量並不比引用靜態型别來得輕鬆。所以最後,筆者還是覺得直接套用TypeScript,讓自己在每一次引用參數,都要好好地先了解函數的輸入輸出型別寫法。 說實在,從JavaScript到TypeScript並不輕鬆。一些原本很無腦的Axios, Promise, Vue語句,TypeScript寫起上來,都變得很複雜。但這個套用,對於IDE來講,真的很大幫忙。它就像突然讀懂了我們的程式一樣,可以跳入跳出,可以知道在多少處被引用。重構也變得更有信心,而不是等待事後測試報錯。 有一點要補充,TypeScript並不像Java那般需要完全預先宣告型別。例如函數的回傳結果,TypeScript就不會強制要求寫出型別,因為它可以有限度地猜得出來。當然,如果大家願意宣告,就更好。 總結 總括來講,型別就像厠所的衛生情況一樣。初期當然什麼都不處理也可以,但越用越久也沒有人理會,大家也不想用下去。若然大家都願意努力維持它的品質,大家會更有意願重複使用。 參考資訊 「靜態型別 vs. 動態型別」與「強型別 vs. 弱型別」 https://blog.tarswork.com/post/programming-language-type-system Typed JavaScript https://depth-first.com/articles/2021/11/03/typed-javascript/

Spring Boot 05 - 為 http json api 加入登入要求

科技新知
MacauYeah・2024-07-02

本節,我們將為之前的http服務,加入認證機制,只有在資料庫現存的用戶可以登入及訪問我們的json api。 下戴模版 慣例,我們用Spring Initializr (Maven) 下載模版,Dependency主要選擇 Spring Web Spring Boot DevTools Spring Security Controller 跟上節一樣,我們起一個Controller,為簡化測試,我們只做http GET api。 由於本blog對於Source Code的顯示不太友好,有需要看source code的,請到Github查看 //src/main/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/controller/HomeController.java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class HomeController { @GetMapping("/someRecord/{uuid}") public Map readSomeRecord(@PathVariable String uuid) { return Map.of("ret", "your uuid:" + uuid); } } 準備我們的test case,但這次我們預期它應該要出現登入失敗的結果。 //src/test/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/controller/HomeControllerTest.java @SpringBootTest @AutoConfigureMockMvc public class HomeControllerTest { @Autowired private MockMvc mockMvc; @Test void testNoLogin() throws Exception { RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/api/someRecord/1234") .contentType(MediaType.APPLICATION_JSON); this.mockMvc.perform(requestBuilder) .andExpect(MockMvcResultMatchers.status().is4xxClientError()) .andExpect(MockMvcResultMatchers.jsonPath("$.ret").doesNotExist()) .andDo(MockMvcResultHandlers.print()); } } 在我們執行上述的測試,test case 成功過了。我們的基本設定跟上一節其實沒有多大改動,為何現在http api會回傳狀態 401? 那是因為我們在依賴中加了,Spring Security,它配合了Spring Web,就會自動為所有api加入權限檢測。我們的測試中,沒有任何用戶登入,當然會出現 http 401。為了讓我們可以好好管理誰可以使用api,我們就來設定一定Security。 我們加一個WebSecurityConfig.java,暫時指定所有的訪問路徑都必需有USER權限,並且用 http basic的方式登入。 //src/main/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/config/WebSecurityConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorizeHttpRequests -> { authorizeHttpRequests.requestMatchers("/**").hasRole("USER"); // 所有的訪問路徑都必需有USER權限 }); http.httpBasic(Customizer.withDefaults()); // 使用http basic作為登入認證的方式 return http.build(); } } 上述例子,只是擋了沒有權限的人,我們還需要讓有登入身份的用戶可以成得取限User權限。 我們繼續修改,WebSecurityConfig,加入只在記憶體有效的InMemoryUser import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; public class WebSecurityConfig { //.. @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 我們的密碼不應該明文儲,比較保險,我們使用BCrypt演算法,為密碼做單向加密。 } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withUsername("admin") .password(passwordEncoder().encode("pass")) .roles("USER").build(); // 我們在記憶中體,加入一個測試用的User,它的名字為admin,密碼為pass,權限為User return new InMemoryUserDetailsManager(user); } 然後加入新的測試,直接模擬Role。結果是通過的。 //src/test/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/controller/HomeControllerTest.java @Test void testLoginWithRoles() throws Exception { RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/api/someRecord/1234") .contentType(MediaType.APPLICATION_JSON).with( SecurityMockMvcRequestPostProcessors.user("someone") .roles("USER", "ADMIN")); // 沒有使用密碼,只使用Role this.mockMvc.perform(requestBuilder) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.jsonPath("$.ret").value("your uuid:1234")) .andDo(MockMvcResultHandlers.print()); } 再來一個測試,改用密碼登入,分別輸入錯的和正確的密碼。 @Test void testLoginWithWrongPasswordAndNoRole() throws Exception { RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/api/someRecord/1234") .header("Authorization", "Basic randompass") // 輸入錯的密碼,應該回傳http 401 Unauthorized .contentType(MediaType.APPLICATION_JSON); this.mockMvc.perform(requestBuilder) .andExpect(MockMvcResultMatchers.status().is4xxClientError()) .andDo(MockMvcResultHandlers.print()); } @Test void testLoginWithPassword() throws Exception { RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/api/someRecord/1234") .header("Authorization", "Basic YWRtaW46cGFzcw==") // http basic 就是把 admin:pass 轉成base64 .contentType(MediaType.APPLICATION_JSON); this.mockMvc.perform(requestBuilder) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) .andExpect(MockMvcResultMatchers.jsonPath("$.ret").value("your uuid:1234")) .andDo(MockMvcResultHandlers.print()); } 最後,當然是正確的密碼才能通過。若果大家還是半信半疑,我們可以跑起真的正服務(IDE RUN或mvn spring-boot:run),然後用curl去試。 curl http://localhost:8080/api/someRecord/1234 // failed with 401 curl -u "admin:pass" http://localhost:8080/api/someRecord/1234 // successed 使用SQL Database讀取用戶登入資訊 一般而言,我們不可能把所有用戶登資訊打在InMemoryUser中,通常背後有一個資料庫儲存所有的用戶資訊,我們在登入時,讀取它來做對比檢證。 為此,我們在maven中,加入 Spring Data JPA h2 database (或任何你的資料庫,如mysql 、 sql server) 最後一步,我們把InMemoryUser去掉,改為從資料庫讀取。因為原始碼太多,就不全部貼上。最主要的是WebSecurityConfig.java要關掉之前的UserDetailsService,改為提供一個UserServiceImpl類,它會實現UserDetailsService的功能。 @Configuration @EnableWebSecurity public class WebSecurityConfig { // 把原來的Bean先變成註解,其他不變 // @Bean // public UserDetailsService userDetailsService() { // UserDetails user = User.withUsername("admin") // .password(passwordEncoder().encode("pass")) // .roles("USER").build(); // return new InMemoryUserDetailsManager(user); // } } // spring-boot-tutorial/spring-boot-web-api-data/src/main/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/config/UserServiceImpl.java // other import import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; @Service public class UserServiceImpl implements UserDetailsService { @Autowired PasswordEncoder passwordEncoder; @Autowired UserRepo userRepo; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 因為我們資料庫沒有資料,為了方便測試密碼的加密,我們在java code上直接插入一筆資料。 UserEntity defaultUser = new UserEntity(); defaultUser.setUsername("admin"); defaultUser.setPassword(passwordEncoder.encode("pass")); defaultUser.setRole("USER"); defaultUser.setUuid(UUID.randomUUID().toString()); userRepo.save(defaultUser); // 上述為測試用插入資料,不應該出現在正式使用環境中。 UserEntity user = userRepo.findOneByUsername(username) .orElseThrow(() -> new UsernameNotFoundException(username + " not found")); // 找找資料庫有沒有正在登入的該名使用者username List authorities = List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole())); LOG.debug("got user uuid:{}, username:{}, role:{} from database", user.getUuid(), username, user.getRole()); // 如果前面的 findOneByUsername 有結果回傳,我們就給它一個ROLE_XXX的權限。 return new User(username, user.getPassword(), authorities); // 這裏從沒有檢查過密碼是否有匹配,全部交給Spring Security去做 } } //spring-boot-tutorial/spring-boot-web-api-data/src/main/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/entity/UserEntity.java // spring-boot-tutorial/spring-boot-web-api-data/src/main/java/io/github/macauyeah/springboot/tutorial/springbootwebapidata/repo/UserRepo.java 上述段落中,筆者省略了UserEntity和UserRepo,它們只是一般的spring-data-jpa概念,有需要可以經文末的連結查看完全原始碼。最需要注意的,是UserEntity的password欄位,在資料庫中是以加密的方式儲存。我們在配匹登入者與資料庫記錄時,也沒有自行檢驗密碼的需要。我們只是在加密過的密碼回傳給Spring Security,Spring框架會自行把登入者輸入的密碼與加密了的密碼作比較。